ETH Price: $2,502.22 (+1.04%)

Transaction Decoder

Block:
15890889 at Nov-03-2022 05:09:47 PM +UTC
Transaction Fee:
0.00388869399416184 ETH $9.73
Gas Used:
149,385 Gas / 26.031355184 Gwei

Emitted Events:

Account State Difference:

  Address   Before After State Difference Code
0x0711A976...5C584f450
0x3326554a...A8e2B21A6
0.033059952725928233 Eth
Nonce: 472
0.029171258731766393 Eth
Nonce: 473
0.00388869399416184
(Lido: Execution Layer Rewards Vault)
217.839828600053012442 Eth217.840127370053012442 Eth0.00029877
0x942BC2d3...Fd82F5C8a
0xA7b76C13...933e2619f

Execution Trace

MinterMerkleV2.purchaseTo( ) => ( tokenId=2575 )
  • MinterFilterV1.mint( _to=0xc7daF473C103aa2B112FE2F773E3A508A6999BB6, _projectId=0, sender=0x3326554a61f6eD0b14e012237264219A8e2B21A6 ) => ( _tokenId=2575 )
    • GenArt721CoreV3_Explorations.mint_Ecf( _to=0xc7daF473C103aa2B112FE2F773E3A508A6999BB6, _projectId=0, _by=0x3326554a61f6eD0b14e012237264219A8e2B21A6 ) => ( _tokenId=2575 )
      • 0xa7b76c135d213bb95375713ea3d4687933e2619f.b628171a( )
        • GenArt721CoreV3_Explorations.setTokenHash_8PT( _tokenId=2575, _hashSeed=60225B7226C5FD63DBB6F2A815413BD68167092BD5A936EBDCB4E9FEECB9FF09 )
          File 1 of 3: MinterMerkleV2
          // SPDX-License-Identifier: LGPL-3.0-only
          // Created By: Art Blocks Inc.
          import "../../../interfaces/0.8.x/IGenArt721CoreContractV3.sol";
          import "../../../interfaces/0.8.x/IMinterFilterV0.sol";
          import "../../../interfaces/0.8.x/IFilteredMinterMerkleV0.sol";
          import "../../../interfaces/0.8.x/IDelegationRegistry.sol";
          import "@openzeppelin-4.7/contracts/utils/cryptography/MerkleProof.sol";
          import "@openzeppelin-4.7/contracts/token/ERC20/IERC20.sol";
          import "@openzeppelin-4.7/contracts/security/ReentrancyGuard.sol";
          pragma solidity 0.8.17;
          /**
           * @title Filtered Minter contract that allows tokens to be minted with ETH
           * for addresses in a Merkle allowlist.
           * This is designed to be used with IGenArt721CoreContractV3 contracts.
           * @author Art Blocks Inc.
           * @notice Privileged Roles and Ownership:
           * This contract is designed to be managed, with limited powers.
           * Privileged roles and abilities are controlled by the project's artist, which
           * can be modified by the core contract's Admin ACL contract. Both of these
           * roles hold extensive power and can modify minter details.
           * Care must be taken to ensure that the admin ACL contract and artist
           * addresses are secure behind a multi-sig or other access control mechanism.
           * ----------------------------------------------------------------------------
           * The following functions are restricted to a project's artist:
           * - updateMerkleRoot
           * - updatePricePerTokenInWei
           * - setProjectInvocationsPerAddress
           * ----------------------------------------------------------------------------
           * Additional admin and artist privileged roles may be described on other
           * contracts that this minter integrates with.
           * ----------------------------------------------------------------------------
           * This contract allows vaults to configure token-level or wallet-level
           * delegation of minting privileges. This allows a vault on an allowlist to
           * delegate minting privileges to a wallet that is not on the allowlist,
           * enabling the vault to remain air-gapped while still allowing minting. The
           * delegation registry contract is responsible for managing these delegations,
           * and is available at the address returned by the public constant
           * `DELEGATION_REGISTRY_ADDRESS`. At the time of writing, the delegation
           * registry enables easy delegation configuring at https://delegate.cash/.
           * Art Blocks does not guarentee the security of the delegation registry, and
           * users should take care to ensure that the delegation registry is secure.
           * Token-level delegations are configured by the vault owner, and contract-
           * level delegations must be configured for the core token contract as returned
           * by the public immutable variable `genArt721CoreAddress`.
           */
          contract MinterMerkleV2 is ReentrancyGuard, IFilteredMinterMerkleV0 {
              using MerkleProof for bytes32[];
              /// Delegation registry address
              address public constant DELEGATION_REGISTRY_ADDRESS =
                  0x00000000000076A84feF008CDAbe6409d2FE638B;
              /// Delegation registry address
              IDelegationRegistry private immutable delegationRegistryContract;
              /// Core contract address this minter interacts with
              address public immutable genArt721CoreAddress;
              /// This contract handles cores with interface IV3
              IGenArt721CoreContractV3 private immutable genArtCoreContract;
              /// Minter filter address this minter interacts with
              address public immutable minterFilterAddress;
              /// Minter filter this minter may interact with.
              IMinterFilterV0 private immutable minterFilter;
              /// minterType for this minter
              string public constant minterType = "MinterMerkleV2";
              /// project minter configuration keys used by this minter
              bytes32 private constant CONFIG_MERKLE_ROOT = "merkleRoot";
              bytes32 private constant CONFIG_USE_MAX_INVOCATIONS_PER_ADDRESS_OVERRIDE =
                  "useMaxMintsPerAddrOverride"; // shortened to fit in 32 bytes
              bytes32 private constant CONFIG_MAX_INVOCATIONS_OVERRIDE =
                  "maxMintsPerAddrOverride"; // shortened to match format of previous key
              uint256 constant ONE_MILLION = 1_000_000;
              uint256 public constant DEFAULT_MAX_INVOCATIONS_PER_ADDRESS = 1;
              struct ProjectConfig {
                  bool maxHasBeenInvoked;
                  bool priceIsConfigured;
                  // initial value is false, so by default, projects limit allowlisted
                  // addresses to a mint qty of `DEFAULT_MAX_INVOCATIONS_PER_ADDRESS`
                  bool useMaxInvocationsPerAddressOverride;
                  // a value of 0 means no limit
                  // (only used if `useMaxInvocationsPerAddressOverride` is true)
                  uint24 maxInvocationsPerAddressOverride;
                  uint24 maxInvocations;
                  uint256 pricePerTokenInWei;
              }
              mapping(uint256 => ProjectConfig) public projectConfig;
              /// projectId => merkle root
              mapping(uint256 => bytes32) public projectMerkleRoot;
              /// projectId => purchaser address => qty of mints purchased for project
              mapping(uint256 => mapping(address => uint256))
                  public projectUserMintInvocations;
              modifier onlyArtist(uint256 _projectId) {
                  require(
                      msg.sender ==
                          genArtCoreContract.projectIdToArtistAddress(_projectId),
                      "Only Artist"
                  );
                  _;
              }
              /**
               * @notice Initializes contract to be a Filtered Minter for
               * `_minterFilter`, integrated with Art Blocks core contract
               * at address `_genArt721Address`.
               * @param _genArt721Address Art Blocks core contract address for
               * which this contract will be a minter.
               * @param _minterFilter Minter filter for which this will be a
               * filtered minter.
               */
              constructor(address _genArt721Address, address _minterFilter)
                  ReentrancyGuard()
              {
                  genArt721CoreAddress = _genArt721Address;
                  genArtCoreContract = IGenArt721CoreContractV3(_genArt721Address);
                  delegationRegistryContract = IDelegationRegistry(
                      DELEGATION_REGISTRY_ADDRESS
                  );
                  minterFilterAddress = _minterFilter;
                  minterFilter = IMinterFilterV0(_minterFilter);
                  require(
                      minterFilter.genArt721CoreAddress() == _genArt721Address,
                      "Illegal contract pairing"
                  );
                  // broadcast default max invocations per address for this minter
                  emit DefaultMaxInvocationsPerAddress(
                      DEFAULT_MAX_INVOCATIONS_PER_ADDRESS
                  );
              }
              /**
               * @notice Update the Merkle root for project `_projectId`.
               * @param _projectId Project ID to be updated.
               * @param _root root of Merkle tree defining addresses allowed to mint
               * on project `_projectId`.
               */
              function updateMerkleRoot(uint256 _projectId, bytes32 _root)
                  external
                  onlyArtist(_projectId)
              {
                  require(_root != bytes32(0), "Root must be provided");
                  projectMerkleRoot[_projectId] = _root;
                  emit ConfigValueSet(_projectId, CONFIG_MERKLE_ROOT, _root);
              }
              /**
               * @notice Returns hashed address (to be used as merkle tree leaf).
               * Included as a public function to enable users to calculate their hashed
               * address in Solidity when generating proofs off-chain.
               * @param _address address to be hashed
               * @return bytes32 hashed address, via keccak256 (using encodePacked)
               */
              function hashAddress(address _address) public pure returns (bytes32) {
                  return keccak256(abi.encodePacked(_address));
              }
              /**
               * @notice Verify if address is allowed to mint on project `_projectId`.
               * @param _projectId Project ID to be checked.
               * @param _proof Merkle proof for address.
               * @param _address Address to check.
               * @return inAllowlist true only if address is allowed to mint and valid
               * Merkle proof was provided
               */
              function verifyAddress(
                  uint256 _projectId,
                  bytes32[] calldata _proof,
                  address _address
              ) public view returns (bool) {
                  return
                      _proof.verifyCalldata(
                          projectMerkleRoot[_projectId],
                          hashAddress(_address)
                      );
              }
              /**
               * @notice Sets maximum allowed invocations per allowlisted address for
               * project `_project` to `limit`. If `limit` is set to 0, allowlisted
               * addresses will be able to mint as many times as desired, until the
               * project reaches its maximum invocations.
               * Default is a value of 1 if never configured by artist.
               * @param _projectId Project ID to toggle the mint limit.
               * @param _maxInvocationsPerAddress Maximum allowed invocations per
               * allowlisted address.
               * @dev default value stated above must be updated if the value of
               * CONFIG_USE_MAX_INVOCATIONS_PER_ADDRESS_OVERRIDE is changed.
               */
              function setProjectInvocationsPerAddress(
                  uint256 _projectId,
                  uint24 _maxInvocationsPerAddress
              ) external onlyArtist(_projectId) {
                  ProjectConfig storage _projectConfig = projectConfig[_projectId];
                  // use override value instead of the contract's default
                  // @dev this never changes from true to false; default value is only
                  // used if artist has never configured project invocations per address
                  _projectConfig.useMaxInvocationsPerAddressOverride = true;
                  // update the override value
                  _projectConfig
                      .maxInvocationsPerAddressOverride = _maxInvocationsPerAddress;
                  // generic events
                  emit ConfigValueSet(
                      _projectId,
                      CONFIG_USE_MAX_INVOCATIONS_PER_ADDRESS_OVERRIDE,
                      true
                  );
                  emit ConfigValueSet(
                      _projectId,
                      CONFIG_MAX_INVOCATIONS_OVERRIDE,
                      uint256(_maxInvocationsPerAddress)
                  );
              }
              /**
               * @notice Syncs local maximum invocations of project `_projectId` based on
               * the value currently defined in the core contract. Only used for gas
               * optimization of mints after maxInvocations has been reached.
               * @param _projectId Project ID to set the maximum invocations for.
               * @dev this enables gas reduction after maxInvocations have been reached -
               * core contracts shall still enforce a maxInvocation check during mint.
               * @dev function is intentionally not gated to any specific access control;
               * it only syncs a local state variable to the core contract's state.
               */
              function setProjectMaxInvocations(uint256 _projectId) external {
                  uint256 maxInvocations;
                  (, maxInvocations, , , , ) = genArtCoreContract.projectStateData(
                      _projectId
                  );
                  // update storage with results
                  projectConfig[_projectId].maxInvocations = uint24(maxInvocations);
              }
              /**
               * @notice Warning: Disabling purchaseTo is not supported on this minter.
               * This method exists purely for interface-conformance purposes.
               */
              function togglePurchaseToDisabled(uint256 _projectId)
                  external
                  view
                  onlyArtist(_projectId)
              {
                  revert("Action not supported");
              }
              /**
               * @notice projectId => has project reached its maximum number of
               * invocations? Note that this returns a local cache of the core contract's
               * state, and may be out of sync with the core contract. This is
               * intentional, as it only enables gas optimization of mints after a
               * project's maximum invocations has been reached. A false negative will
               * only result in a gas cost increase, since the core contract will still
               * enforce a maxInvocation check during minting. A false positive is not
               * possible because the V3 core contract only allows maximum invocations
               * to be reduced, not increased. Based on this rationale, we intentionally
               * do not do input validation in this method as to whether or not the input
               * `_projectId` is an existing project ID.
               */
              function projectMaxHasBeenInvoked(uint256 _projectId)
                  external
                  view
                  returns (bool)
              {
                  return projectConfig[_projectId].maxHasBeenInvoked;
              }
              /**
               * @notice projectId => project's maximum number of invocations.
               * Optionally synced with core contract value, for gas optimization.
               * Note that this returns a local cache of the core contract's
               * state, and may be out of sync with the core contract. This is
               * intentional, as it only enables gas optimization of mints after a
               * project's maximum invocations has been reached.
               * @dev A number greater than the core contract's project max invocations
               * will only result in a gas cost increase, since the core contract will
               * still enforce a maxInvocation check during minting. A number less than
               * the core contract's project max invocations is only possible when the
               * project's max invocations have not been synced on this minter, since the
               * V3 core contract only allows maximum invocations to be reduced, not
               * increased. When this happens, the minter will enable minting, allowing
               * the core contract to enforce the max invocations check. Based on this
               * rationale, we intentionally do not do input validation in this method as
               * to whether or not the input `_projectId` is an existing project ID.
               */
              function projectMaxInvocations(uint256 _projectId)
                  external
                  view
                  returns (uint256)
              {
                  return uint256(projectConfig[_projectId].maxInvocations);
              }
              /**
               * @notice Updates this minter's price per token of project `_projectId`
               * to be '_pricePerTokenInWei`, in Wei.
               * This price supersedes any legacy core contract price per token value.
               * @dev Note that it is intentionally supported here that the configured
               * price may be explicitly set to `0`.
               */
              function updatePricePerTokenInWei(
                  uint256 _projectId,
                  uint256 _pricePerTokenInWei
              ) external onlyArtist(_projectId) {
                  projectConfig[_projectId].pricePerTokenInWei = _pricePerTokenInWei;
                  projectConfig[_projectId].priceIsConfigured = true;
                  emit PricePerTokenInWeiUpdated(_projectId, _pricePerTokenInWei);
              }
              /**
               * @notice Inactive function - requires Merkle proof to purchase.
               */
              function purchase(uint256) external payable returns (uint256) {
                  revert("Must provide Merkle proof");
              }
              /**
               * @notice Inactive function - requires Merkle proof to purchase.
               */
              function purchaseTo(address, uint256) public payable returns (uint256) {
                  revert("Must provide Merkle proof");
              }
              /**
               * @notice Purchases a token from project `_projectId`.
               * @param _projectId Project ID to mint a token on.
               * @param _proof Merkle proof.
               * @return tokenId Token ID of minted token
               */
              function purchase(uint256 _projectId, bytes32[] calldata _proof)
                  external
                  payable
                  returns (uint256 tokenId)
              {
                  tokenId = purchaseTo_kem(msg.sender, _projectId, _proof, address(0));
                  return tokenId;
              }
              /**
               * @notice gas-optimized version of purchase(uint256,bytes32[]).
               */
              function purchase_gD5(uint256 _projectId, bytes32[] calldata _proof)
                  external
                  payable
                  returns (uint256 tokenId)
              {
                  tokenId = purchaseTo_kem(msg.sender, _projectId, _proof, address(0));
                  return tokenId;
              }
              /**
               * @notice Purchases a token from project `_projectId` and sets
               * the token's owner to `_to`.
               * @param _to Address to be the new token's owner.
               * @param _projectId Project ID to mint a token on.
               * @param _proof Merkle proof.
               * @return tokenId Token ID of minted token
               */
              function purchaseTo(
                  address _to,
                  uint256 _projectId,
                  bytes32[] calldata _proof
              ) external payable returns (uint256 tokenId) {
                  return purchaseTo_kem(_to, _projectId, _proof, address(0));
              }
              /**
               * @notice Purchases a token from project `_projectId` and sets
               *         the token's owner to `_to`, as a delegate, (the `msg.sender`)
               *         on behalf of an explicitly defined vault.
               * @param _to Address to be the new token's owner.
               * @param _projectId Project ID to mint a token on.
               * @param _proof Merkle proof.
               * @param _vault Vault being purchased on behalf of.
               * @return tokenId Token ID of minted token
               */
              function purchaseTo(
                  address _to,
                  uint256 _projectId,
                  bytes32[] calldata _proof,
                  address _vault
              ) external payable returns (uint256 tokenId) {
                  return purchaseTo_kem(_to, _projectId, _proof, _vault);
              }
              /**
               * @notice gas-optimized version of
               * purchaseTo(address,uint256,bytes32[],address).
               * @param _to Address to be the new token's owner.
               * @param _projectId Project ID to mint a token on.
               * @param _proof Merkle proof. Must be a valid proof of either `msg.sender`
               * if `_vault` is `address(0)`, or `_vault` if `_vault` is not `address(0)`.
               * @param _vault Vault being purchased on behalf of. Acceptable to be
               * address(0) if no vault.
               */
              function purchaseTo_kem(
                  address _to,
                  uint256 _projectId,
                  bytes32[] calldata _proof,
                  address _vault // acceptable to be `address(0)` if no vault
              ) public payable nonReentrant returns (uint256 tokenId) {
                  // CHECKS
                  ProjectConfig storage _projectConfig = projectConfig[_projectId];
                  // Note that `maxHasBeenInvoked` is only checked here to reduce gas
                  // consumption after a project has been fully minted.
                  // `_projectConfig.maxHasBeenInvoked` is locally cached to reduce
                  // gas consumption, but if not in sync with the core contract's value,
                  // the core contract also enforces its own max invocation check during
                  // minting.
                  require(
                      !_projectConfig.maxHasBeenInvoked,
                      "Maximum number of invocations reached"
                  );
                  // load price of token into memory
                  uint256 _pricePerTokenInWei = _projectConfig.pricePerTokenInWei;
                  require(
                      msg.value >= _pricePerTokenInWei,
                      "Must send minimum value to mint!"
                  );
                  // require artist to have configured price of token on this minter
                  require(_projectConfig.priceIsConfigured, "Price not configured");
                  // no contract filter since Merkle tree controls allowed addresses
                  // NOTE: delegate-vault handling **begins here**.
                  // handle that the vault may be either the `msg.sender` in the case
                  // that there is not a true vault, or may be `_vault` if one is
                  // provided explicitly (and it is valid).
                  address vault = msg.sender;
                  if (_vault != address(0)) {
                      // If a vault is provided, it must be valid, otherwise throw rather
                      // than optimistically-minting with original `msg.sender`.
                      // Note, we do not check `checkDelegateForAll` as well, as it is known
                      // to be implicitly checked by calling `checkDelegateForContract`.
                      bool isValidDelegee = delegationRegistryContract
                          .checkDelegateForContract(
                              msg.sender, // delegate
                              _vault, // vault
                              genArt721CoreAddress // contract
                          );
                      require(isValidDelegee, "Invalid delegate-vault pairing");
                      vault = _vault;
                  }
                  // require valid Merkle proof
                  require(
                      verifyAddress(_projectId, _proof, vault),
                      "Invalid Merkle proof"
                  );
                  // limit mints per address by project
                  uint256 _maxProjectInvocationsPerAddress = _projectConfig
                      .useMaxInvocationsPerAddressOverride
                      ? _projectConfig.maxInvocationsPerAddressOverride
                      : DEFAULT_MAX_INVOCATIONS_PER_ADDRESS;
                  // note that mint limits index off of the `vault` (when applicable)
                  require(
                      projectUserMintInvocations[_projectId][vault] <
                          _maxProjectInvocationsPerAddress ||
                          _maxProjectInvocationsPerAddress == 0,
                      "Maximum number of invocations per address reached"
                  );
                  // EFFECTS
                  // increment user's invocations for this project
                  unchecked {
                      // this will never overflow since user's invocations on a project
                      // are limited by the project's max invocations
                      projectUserMintInvocations[_projectId][vault]++;
                  }
                  // mint token
                  tokenId = minterFilter.mint(_to, _projectId, vault);
                  // NOTE: delegate-vault handling **ends here**.
                  // okay if this underflows because if statement will always eval false.
                  // this is only for gas optimization (core enforces maxInvocations).
                  unchecked {
                      if (tokenId % ONE_MILLION == _projectConfig.maxInvocations - 1) {
                          _projectConfig.maxHasBeenInvoked = true;
                      }
                  }
                  // INTERACTIONS
                  _splitFundsETH(_projectId, _pricePerTokenInWei);
                  return tokenId;
              }
              /**
               * @dev splits ETH funds between sender (if refund), foundation,
               * artist, and artist's additional payee for a token purchased on
               * project `_projectId`.
               * @dev possible DoS during splits is acknowledged, and mitigated by
               * business practices, including end-to-end testing on mainnet, and
               * admin-accepted artist payment addresses.
               */
              function _splitFundsETH(uint256 _projectId, uint256 _pricePerTokenInWei)
                  internal
              {
                  if (msg.value > 0) {
                      bool success_;
                      // send refund to sender
                      uint256 refund = msg.value - _pricePerTokenInWei;
                      if (refund > 0) {
                          (success_, ) = msg.sender.call{value: refund}("");
                          require(success_, "Refund failed");
                      }
                      // split remaining funds between foundation, artist, and artist's
                      // additional payee
                      (
                          uint256 artblocksRevenue_,
                          address payable artblocksAddress_,
                          uint256 artistRevenue_,
                          address payable artistAddress_,
                          uint256 additionalPayeePrimaryRevenue_,
                          address payable additionalPayeePrimaryAddress_
                      ) = genArtCoreContract.getPrimaryRevenueSplits(
                              _projectId,
                              _pricePerTokenInWei
                          );
                      // Art Blocks payment
                      if (artblocksRevenue_ > 0) {
                          (success_, ) = artblocksAddress_.call{value: artblocksRevenue_}(
                              ""
                          );
                          require(success_, "Art Blocks payment failed");
                      }
                      // artist payment
                      if (artistRevenue_ > 0) {
                          (success_, ) = artistAddress_.call{value: artistRevenue_}("");
                          require(success_, "Artist payment failed");
                      }
                      // additional payee payment
                      if (additionalPayeePrimaryRevenue_ > 0) {
                          (success_, ) = additionalPayeePrimaryAddress_.call{
                              value: additionalPayeePrimaryRevenue_
                          }("");
                          require(success_, "Additional Payee payment failed");
                      }
                  }
              }
              /**
               * @notice projectId => maximum invocations per allowlisted address. If a
               * a value of 0 is returned, there is no limit on the number of mints per
               * allowlisted address.
               * Default behavior is limit 1 mint per address.
               * This value can be changed at any time by the artist.
               * @dev default value stated above must be updated if the value of
               * CONFIG_USE_MAX_INVOCATIONS_PER_ADDRESS_OVERRIDE is changed.
               */
              function projectMaxInvocationsPerAddress(uint256 _projectId)
                  public
                  view
                  returns (uint256)
              {
                  ProjectConfig storage _projectConfig = projectConfig[_projectId];
                  if (_projectConfig.useMaxInvocationsPerAddressOverride) {
                      return uint256(_projectConfig.maxInvocationsPerAddressOverride);
                  } else {
                      return DEFAULT_MAX_INVOCATIONS_PER_ADDRESS;
                  }
              }
              /**
               * @notice Returns remaining invocations for a given address.
               * If `projectLimitsMintInvocationsPerAddress` is false, individual
               * addresses are only limited by the project's maximum invocations, and a
               * dummy value of zero is returned for `mintInvocationsRemaining`.
               * If `projectLimitsMintInvocationsPerAddress` is true, the quantity of
               * remaining mint invocations for address `_address` is returned as
               * `mintInvocationsRemaining`.
               * Note that mint invocations per address can be changed at any time by the
               * artist of a project.
               * Also note that all mint invocations are limited by a project's maximum
               * invocations as defined on the core contract. This function may return
               * a value greater than the project's remaining invocations.
               */
              function projectRemainingInvocationsForAddress(
                  uint256 _projectId,
                  address _address
              )
                  external
                  view
                  returns (
                      bool projectLimitsMintInvocationsPerAddress,
                      uint256 mintInvocationsRemaining
                  )
              {
                  uint256 maxInvocationsPerAddress = projectMaxInvocationsPerAddress(
                      _projectId
                  );
                  if (maxInvocationsPerAddress == 0) {
                      // project does not limit mint invocations per address, so leave
                      // `projectLimitsMintInvocationsPerAddress` at solidity initial
                      // value of false. Also leave `mintInvocationsRemaining` at
                      // solidity initial value of zero, as indicated in this function's
                      // documentation.
                  } else {
                      projectLimitsMintInvocationsPerAddress = true;
                      uint256 userMintInvocations = projectUserMintInvocations[
                          _projectId
                      ][_address];
                      // if user has not reached max invocations per address, return
                      // remaining invocations
                      if (maxInvocationsPerAddress > userMintInvocations) {
                          unchecked {
                              // will never underflow due to the check above
                              mintInvocationsRemaining =
                                  maxInvocationsPerAddress -
                                  userMintInvocations;
                          }
                      }
                      // else user has reached their maximum invocations, so leave
                      // `mintInvocationsRemaining` at solidity initial value of zero
                  }
              }
              /**
               * @notice Process proof for an address. Returns Merkle root. Included to
               * enable users to easily verify a proof's validity.
               * @param _proof Merkle proof for address.
               * @param _address Address to process.
               * @return merkleRoot Merkle root for `_address` and `_proof`
               */
              function processProofForAddress(bytes32[] calldata _proof, address _address)
                  external
                  pure
                  returns (bytes32)
              {
                  return _proof.processProofCalldata(hashAddress(_address));
              }
              /**
               * @notice Gets if price of token is configured, price of minting a
               * token on project `_projectId`, and currency symbol and address to be
               * used as payment. Supersedes any core contract price information.
               * @param _projectId Project ID to get price information for.
               * @return isConfigured true only if token price has been configured on
               * this minter
               * @return tokenPriceInWei current price of token on this minter - invalid
               * if price has not yet been configured
               * @return currencySymbol currency symbol for purchases of project on this
               * minter. This minter always returns "ETH"
               * @return currencyAddress currency address for purchases of project on
               * this minter. This minter always returns null address, reserved for ether
               */
              function getPriceInfo(uint256 _projectId)
                  external
                  view
                  returns (
                      bool isConfigured,
                      uint256 tokenPriceInWei,
                      string memory currencySymbol,
                      address currencyAddress
                  )
              {
                  ProjectConfig storage _projectConfig = projectConfig[_projectId];
                  isConfigured = _projectConfig.priceIsConfigured;
                  tokenPriceInWei = _projectConfig.pricePerTokenInWei;
                  currencySymbol = "ETH";
                  currencyAddress = address(0);
              }
          }
          // SPDX-License-Identifier: LGPL-3.0-only
          // Created By: Art Blocks Inc.
          pragma solidity ^0.8.0;
          import "./IAdminACLV0.sol";
          /// use the Royalty Registry's IManifold interface for token royalties
          import "./IManifold.sol";
          interface IGenArt721CoreContractV3 is IManifold {
              /**
               * @notice Token ID `_tokenId` minted to `_to`.
               */
              event Mint(address indexed _to, uint256 indexed _tokenId);
              /**
               * @notice currentMinter updated to `_currentMinter`.
               * @dev Implemented starting with V3 core
               */
              event MinterUpdated(address indexed _currentMinter);
              /**
               * @notice Platform updated on bytes32-encoded field `_field`.
               */
              event PlatformUpdated(bytes32 indexed _field);
              /**
               * @notice Project ID `_projectId` updated on bytes32-encoded field
               * `_update`.
               */
              event ProjectUpdated(uint256 indexed _projectId, bytes32 indexed _update);
              event ProposedArtistAddressesAndSplits(
                  uint256 indexed _projectId,
                  address _artistAddress,
                  address _additionalPayeePrimarySales,
                  uint256 _additionalPayeePrimarySalesPercentage,
                  address _additionalPayeeSecondarySales,
                  uint256 _additionalPayeeSecondarySalesPercentage
              );
              event AcceptedArtistAddressesAndSplits(uint256 indexed _projectId);
              // version and type of the core contract
              // coreVersion is a string of the form "0.x.y"
              function coreVersion() external view returns (string memory);
              // coreType is a string of the form "GenArt721CoreV3"
              function coreType() external view returns (string memory);
              // owner (pre-V3 was named admin) of contract
              // this is expected to be an Admin ACL contract for V3
              function owner() external view returns (address);
              // Admin ACL contract for V3, will be at the address owner()
              function adminACLContract() external returns (IAdminACLV0);
              // backwards-compatible (pre-V3) admin - equal to owner()
              function admin() external view returns (address);
              /**
               * Function determining if _sender is allowed to call function with
               * selector _selector on contract `_contract`. Intended to be used with
               * peripheral contracts such as minters, as well as internally by the
               * core contract itself.
               */
              function adminACLAllowed(
                  address _sender,
                  address _contract,
                  bytes4 _selector
              ) external returns (bool);
              // getter function of public variable
              function nextProjectId() external view returns (uint256);
              // getter function of public mapping
              function tokenIdToProjectId(uint256 tokenId)
                  external
                  view
                  returns (uint256 projectId);
              // @dev this is not available in V0
              function isMintWhitelisted(address minter) external view returns (bool);
              function projectIdToArtistAddress(uint256 _projectId)
                  external
                  view
                  returns (address payable);
              function projectIdToAdditionalPayeePrimarySales(uint256 _projectId)
                  external
                  view
                  returns (address payable);
              function projectIdToAdditionalPayeePrimarySalesPercentage(
                  uint256 _projectId
              ) external view returns (uint256);
              // @dev new function in V3
              function getPrimaryRevenueSplits(uint256 _projectId, uint256 _price)
                  external
                  view
                  returns (
                      uint256 artblocksRevenue_,
                      address payable artblocksAddress_,
                      uint256 artistRevenue_,
                      address payable artistAddress_,
                      uint256 additionalPayeePrimaryRevenue_,
                      address payable additionalPayeePrimaryAddress_
                  );
              // @dev new function in V3
              function projectStateData(uint256 _projectId)
                  external
                  view
                  returns (
                      uint256 invocations,
                      uint256 maxInvocations,
                      bool active,
                      bool paused,
                      uint256 completedTimestamp,
                      bool locked
                  );
              // @dev Art Blocks primary sales payment address
              function artblocksPrimarySalesAddress()
                  external
                  view
                  returns (address payable);
              /**
               * @notice Backwards-compatible (pre-V3) function returning Art Blocks
               * primary sales payment address (now called artblocksPrimarySalesAddress).
               */
              function artblocksAddress() external view returns (address payable);
              // @dev Percentage of primary sales allocated to Art Blocks
              function artblocksPrimarySalesPercentage() external view returns (uint256);
              /**
               * @notice Backwards-compatible (pre-V3) function returning Art Blocks
               * primary sales percentage (now called artblocksPrimarySalesPercentage).
               */
              function artblocksPercentage() external view returns (uint256);
              // @dev Art Blocks secondary sales royalties payment address
              function artblocksSecondarySalesAddress()
                  external
                  view
                  returns (address payable);
              // @dev Basis points of secondary sales allocated to Art Blocks
              function artblocksSecondarySalesBPS() external view returns (uint256);
              // function to set a token's hash (must be guarded)
              function setTokenHash_8PT(uint256 _tokenId, bytes32 _hash) external;
              // @dev gas-optimized signature in V3 for `mint`
              function mint_Ecf(
                  address _to,
                  uint256 _projectId,
                  address _by
              ) external returns (uint256 tokenId);
              /**
               * @notice Backwards-compatible (pre-V3) function  that gets artist +
               * artist's additional payee royalty data for token ID `_tokenId`.
               * WARNING: Does not include Art Blocks portion of royalties.
               */
              function getRoyaltyData(uint256 _tokenId)
                  external
                  view
                  returns (
                      address artistAddress,
                      address additionalPayee,
                      uint256 additionalPayeePercentage,
                      uint256 royaltyFeeByID
                  );
          }
          // SPDX-License-Identifier: CC0-1.0
          pragma solidity ^0.8.17;
          /// @dev Source: https://github.com/0xfoobar/delegation-registry/blob/main/src/IDelegationRegistry.sol
          /**
           * @title An immutable registry contract to be deployed as a standalone primitive
           * @dev See EIP-5639, new project launches can read previous cold wallet -> hot wallet delegations
           * from here and integrate those permissions into their flow
           */
          interface IDelegationRegistry {
              /// @notice Delegation type
              enum DelegationType {
                  NONE,
                  ALL,
                  CONTRACT,
                  TOKEN
              }
              /// @notice Info about a single delegation, used for onchain enumeration
              struct DelegationInfo {
                  DelegationType type_;
                  address vault;
                  address delegate;
                  address contract_;
                  uint256 tokenId;
              }
              /// @notice Info about a single contract-level delegation
              struct ContractDelegation {
                  address contract_;
                  address delegate;
              }
              /// @notice Info about a single token-level delegation
              struct TokenDelegation {
                  address contract_;
                  uint256 tokenId;
                  address delegate;
              }
              /// @notice Emitted when a user delegates their entire wallet
              event DelegateForAll(address vault, address delegate, bool value);
              /// @notice Emitted when a user delegates a specific contract
              event DelegateForContract(
                  address vault,
                  address delegate,
                  address contract_,
                  bool value
              );
              /// @notice Emitted when a user delegates a specific token
              event DelegateForToken(
                  address vault,
                  address delegate,
                  address contract_,
                  uint256 tokenId,
                  bool value
              );
              /// @notice Emitted when a user revokes all delegations
              event RevokeAllDelegates(address vault);
              /// @notice Emitted when a user revoes all delegations for a given delegate
              event RevokeDelegate(address vault, address delegate);
              /**
               * -----------  WRITE -----------
               */
              /**
               * @notice Allow the delegate to act on your behalf for all contracts
               * @param delegate The hotwallet to act on your behalf
               * @param value Whether to enable or disable delegation for this address, true for setting and false for revoking
               */
              function delegateForAll(address delegate, bool value) external;
              /**
               * @notice Allow the delegate to act on your behalf for a specific contract
               * @param delegate The hotwallet to act on your behalf
               * @param contract_ The address for the contract you're delegating
               * @param value Whether to enable or disable delegation for this address, true for setting and false for revoking
               */
              function delegateForContract(
                  address delegate,
                  address contract_,
                  bool value
              ) external;
              /**
               * @notice Allow the delegate to act on your behalf for a specific token
               * @param delegate The hotwallet to act on your behalf
               * @param contract_ The address for the contract you're delegating
               * @param tokenId The token id for the token you're delegating
               * @param value Whether to enable or disable delegation for this address, true for setting and false for revoking
               */
              function delegateForToken(
                  address delegate,
                  address contract_,
                  uint256 tokenId,
                  bool value
              ) external;
              /**
               * @notice Revoke all delegates
               */
              function revokeAllDelegates() external;
              /**
               * @notice Revoke a specific delegate for all their permissions
               * @param delegate The hotwallet to revoke
               */
              function revokeDelegate(address delegate) external;
              /**
               * @notice Remove yourself as a delegate for a specific vault
               * @param vault The vault which delegated to the msg.sender, and should be removed
               */
              function revokeSelf(address vault) external;
              /**
               * -----------  READ -----------
               */
              /**
               * @notice Returns all active delegations a given delegate is able to claim on behalf of
               * @param delegate The delegate that you would like to retrieve delegations for
               * @return info Array of DelegationInfo structs
               */
              function getDelegationsByDelegate(address delegate)
                  external
                  view
                  returns (DelegationInfo[] memory);
              /**
               * @notice Returns an array of wallet-level delegates for a given vault
               * @param vault The cold wallet who issued the delegation
               * @return addresses Array of wallet-level delegates for a given vault
               */
              function getDelegatesForAll(address vault)
                  external
                  view
                  returns (address[] memory);
              /**
               * @notice Returns an array of contract-level delegates for a given vault and contract
               * @param vault The cold wallet who issued the delegation
               * @param contract_ The address for the contract you're delegating
               * @return addresses Array of contract-level delegates for a given vault and contract
               */
              function getDelegatesForContract(address vault, address contract_)
                  external
                  view
                  returns (address[] memory);
              /**
               * @notice Returns an array of contract-level delegates for a given vault's token
               * @param vault The cold wallet who issued the delegation
               * @param contract_ The address for the contract holding the token
               * @param tokenId The token id for the token you're delegating
               * @return addresses Array of contract-level delegates for a given vault's token
               */
              function getDelegatesForToken(
                  address vault,
                  address contract_,
                  uint256 tokenId
              ) external view returns (address[] memory);
              /**
               * @notice Returns all contract-level delegations for a given vault
               * @param vault The cold wallet who issued the delegations
               * @return delegations Array of ContractDelegation structs
               */
              function getContractLevelDelegations(address vault)
                  external
                  view
                  returns (ContractDelegation[] memory delegations);
              /**
               * @notice Returns all token-level delegations for a given vault
               * @param vault The cold wallet who issued the delegations
               * @return delegations Array of TokenDelegation structs
               */
              function getTokenLevelDelegations(address vault)
                  external
                  view
                  returns (TokenDelegation[] memory delegations);
              /**
               * @notice Returns true if the address is delegated to act on the entire vault
               * @param delegate The hotwallet to act on your behalf
               * @param vault The cold wallet who issued the delegation
               */
              function checkDelegateForAll(address delegate, address vault)
                  external
                  view
                  returns (bool);
              /**
               * @notice Returns true if the address is delegated to act on your behalf for a token contract or an entire vault
               * @param delegate The hotwallet to act on your behalf
               * @param contract_ The address for the contract you're delegating
               * @param vault The cold wallet who issued the delegation
               */
              function checkDelegateForContract(
                  address delegate,
                  address vault,
                  address contract_
              ) external view returns (bool);
              /**
               * @notice Returns true if the address is delegated to act on your behalf for a specific token, the token's contract or an entire vault
               * @param delegate The hotwallet to act on your behalf
               * @param contract_ The address for the contract you're delegating
               * @param tokenId The token id for the token you're delegating
               * @param vault The cold wallet who issued the delegation
               */
              function checkDelegateForToken(
                  address delegate,
                  address vault,
                  address contract_,
                  uint256 tokenId
              ) external view returns (bool);
          }
          // SPDX-License-Identifier: LGPL-3.0-only
          // Created By: Art Blocks Inc.
          import "./IFilteredMinterV1.sol";
          pragma solidity ^0.8.0;
          /**
           * @title This interface extends the IFilteredMinterV1 interface in order to
           * add support for including Merkle proofs when purchasing.
           * @author Art Blocks Inc.
           */
          interface IFilteredMinterMerkleV0 is IFilteredMinterV1 {
              /**
               * @notice Notifies of the contract's default maximum mints allowed per
               * user for a given project, on this minter. This value can be overridden
               * by the artist of any project at any time.
               */
              event DefaultMaxInvocationsPerAddress(
                  uint256 defaultMaxInvocationsPerAddress
              );
              // Triggers a purchase of a token from the desired project, to the
              // TX-sending address. Requires Merkle proof.
              function purchase(uint256 _projectId, bytes32[] memory _proof)
                  external
                  payable
                  returns (uint256 tokenId);
              // Triggers a purchase of a token from the desired project, to the specified
              // receiving address. Requires Merkle proof.
              function purchaseTo(
                  address _to,
                  uint256 _projectId,
                  bytes32[] memory _proof
              ) external payable returns (uint256 tokenId);
          }
          // SPDX-License-Identifier: LGPL-3.0-only
          // Created By: Art Blocks Inc.
          pragma solidity ^0.8.0;
          interface IMinterFilterV0 {
              /**
               * @notice Approved minter `_minterAddress`.
               */
              event MinterApproved(address indexed _minterAddress, string _minterType);
              /**
               * @notice Revoked approval for minter `_minterAddress`
               */
              event MinterRevoked(address indexed _minterAddress);
              /**
               * @notice Minter `_minterAddress` of type `_minterType`
               * registered for project `_projectId`.
               */
              event ProjectMinterRegistered(
                  uint256 indexed _projectId,
                  address indexed _minterAddress,
                  string _minterType
              );
              /**
               * @notice Any active minter removed for project `_projectId`.
               */
              event ProjectMinterRemoved(uint256 indexed _projectId);
              function genArt721CoreAddress() external returns (address);
              function setMinterForProject(uint256, address) external;
              function removeMinterForProject(uint256) external;
              function mint(
                  address _to,
                  uint256 _projectId,
                  address sender
              ) external returns (uint256);
              function getMinterForProject(uint256) external view returns (address);
              function projectHasMinter(uint256) external view returns (bool);
          }
          // SPDX-License-Identifier: MIT
          // OpenZeppelin Contracts v4.4.1 (security/ReentrancyGuard.sol)
          pragma solidity ^0.8.0;
          /**
           * @dev Contract module that helps prevent reentrant calls to a function.
           *
           * Inheriting from `ReentrancyGuard` will make the {nonReentrant} modifier
           * available, which can be applied to functions to make sure there are no nested
           * (reentrant) calls to them.
           *
           * Note that because there is a single `nonReentrant` guard, functions marked as
           * `nonReentrant` may not call one another. This can be worked around by making
           * those functions `private`, and then adding `external` `nonReentrant` entry
           * points to them.
           *
           * TIP: If you would like to learn more about reentrancy and alternative ways
           * to protect against it, check out our blog post
           * https://blog.openzeppelin.com/reentrancy-after-istanbul/[Reentrancy After Istanbul].
           */
          abstract contract ReentrancyGuard {
              // Booleans are more expensive than uint256 or any type that takes up a full
              // word because each write operation emits an extra SLOAD to first read the
              // slot's contents, replace the bits taken up by the boolean, and then write
              // back. This is the compiler's defense against contract upgrades and
              // pointer aliasing, and it cannot be disabled.
              // The values being non-zero value makes deployment a bit more expensive,
              // but in exchange the refund on every call to nonReentrant will be lower in
              // amount. Since refunds are capped to a percentage of the total
              // transaction's gas, it is best to keep them low in cases like this one, to
              // increase the likelihood of the full refund coming into effect.
              uint256 private constant _NOT_ENTERED = 1;
              uint256 private constant _ENTERED = 2;
              uint256 private _status;
              constructor() {
                  _status = _NOT_ENTERED;
              }
              /**
               * @dev Prevents a contract from calling itself, directly or indirectly.
               * Calling a `nonReentrant` function from another `nonReentrant`
               * function is not supported. It is possible to prevent this from happening
               * by making the `nonReentrant` function external, and making it call a
               * `private` function that does the actual work.
               */
              modifier nonReentrant() {
                  // On the first call to nonReentrant, _notEntered will be true
                  require(_status != _ENTERED, "ReentrancyGuard: reentrant call");
                  // Any calls to nonReentrant after this point will fail
                  _status = _ENTERED;
                  _;
                  // By storing the original value once again, a refund is triggered (see
                  // https://eips.ethereum.org/EIPS/eip-2200)
                  _status = _NOT_ENTERED;
              }
          }
          // SPDX-License-Identifier: MIT
          // OpenZeppelin Contracts (last updated v4.7.0) (utils/cryptography/MerkleProof.sol)
          pragma solidity ^0.8.0;
          /**
           * @dev These functions deal with verification of Merkle Tree proofs.
           *
           * The proofs can be generated using the JavaScript library
           * https://github.com/miguelmota/merkletreejs[merkletreejs].
           * Note: the hashing algorithm should be keccak256 and pair sorting should be enabled.
           *
           * See `test/utils/cryptography/MerkleProof.test.js` for some examples.
           *
           * WARNING: You should avoid using leaf values that are 64 bytes long prior to
           * hashing, or use a hash function other than keccak256 for hashing leaves.
           * This is because the concatenation of a sorted pair of internal nodes in
           * the merkle tree could be reinterpreted as a leaf value.
           */
          library MerkleProof {
              /**
               * @dev Returns true if a `leaf` can be proved to be a part of a Merkle tree
               * defined by `root`. For this, a `proof` must be provided, containing
               * sibling hashes on the branch from the leaf to the root of the tree. Each
               * pair of leaves and each pair of pre-images are assumed to be sorted.
               */
              function verify(
                  bytes32[] memory proof,
                  bytes32 root,
                  bytes32 leaf
              ) internal pure returns (bool) {
                  return processProof(proof, leaf) == root;
              }
              /**
               * @dev Calldata version of {verify}
               *
               * _Available since v4.7._
               */
              function verifyCalldata(
                  bytes32[] calldata proof,
                  bytes32 root,
                  bytes32 leaf
              ) internal pure returns (bool) {
                  return processProofCalldata(proof, leaf) == root;
              }
              /**
               * @dev Returns the rebuilt hash obtained by traversing a Merkle tree up
               * from `leaf` using `proof`. A `proof` is valid if and only if the rebuilt
               * hash matches the root of the tree. When processing the proof, the pairs
               * of leafs & pre-images are assumed to be sorted.
               *
               * _Available since v4.4._
               */
              function processProof(bytes32[] memory proof, bytes32 leaf) internal pure returns (bytes32) {
                  bytes32 computedHash = leaf;
                  for (uint256 i = 0; i < proof.length; i++) {
                      computedHash = _hashPair(computedHash, proof[i]);
                  }
                  return computedHash;
              }
              /**
               * @dev Calldata version of {processProof}
               *
               * _Available since v4.7._
               */
              function processProofCalldata(bytes32[] calldata proof, bytes32 leaf) internal pure returns (bytes32) {
                  bytes32 computedHash = leaf;
                  for (uint256 i = 0; i < proof.length; i++) {
                      computedHash = _hashPair(computedHash, proof[i]);
                  }
                  return computedHash;
              }
              /**
               * @dev Returns true if the `leaves` can be proved to be a part of a Merkle tree defined by
               * `root`, according to `proof` and `proofFlags` as described in {processMultiProof}.
               *
               * _Available since v4.7._
               */
              function multiProofVerify(
                  bytes32[] memory proof,
                  bool[] memory proofFlags,
                  bytes32 root,
                  bytes32[] memory leaves
              ) internal pure returns (bool) {
                  return processMultiProof(proof, proofFlags, leaves) == root;
              }
              /**
               * @dev Calldata version of {multiProofVerify}
               *
               * _Available since v4.7._
               */
              function multiProofVerifyCalldata(
                  bytes32[] calldata proof,
                  bool[] calldata proofFlags,
                  bytes32 root,
                  bytes32[] memory leaves
              ) internal pure returns (bool) {
                  return processMultiProofCalldata(proof, proofFlags, leaves) == root;
              }
              /**
               * @dev Returns the root of a tree reconstructed from `leaves` and the sibling nodes in `proof`,
               * consuming from one or the other at each step according to the instructions given by
               * `proofFlags`.
               *
               * _Available since v4.7._
               */
              function processMultiProof(
                  bytes32[] memory proof,
                  bool[] memory proofFlags,
                  bytes32[] memory leaves
              ) internal pure returns (bytes32 merkleRoot) {
                  // This function rebuild the root hash by traversing the tree up from the leaves. The root is rebuilt by
                  // consuming and producing values on a queue. The queue starts with the `leaves` array, then goes onto the
                  // `hashes` array. At the end of the process, the last hash in the `hashes` array should contain the root of
                  // the merkle tree.
                  uint256 leavesLen = leaves.length;
                  uint256 totalHashes = proofFlags.length;
                  // Check proof validity.
                  require(leavesLen + proof.length - 1 == totalHashes, "MerkleProof: invalid multiproof");
                  // The xxxPos values are "pointers" to the next value to consume in each array. All accesses are done using
                  // `xxx[xxxPos++]`, which return the current value and increment the pointer, thus mimicking a queue's "pop".
                  bytes32[] memory hashes = new bytes32[](totalHashes);
                  uint256 leafPos = 0;
                  uint256 hashPos = 0;
                  uint256 proofPos = 0;
                  // At each step, we compute the next hash using two values:
                  // - a value from the "main queue". If not all leaves have been consumed, we get the next leaf, otherwise we
                  //   get the next hash.
                  // - depending on the flag, either another value for the "main queue" (merging branches) or an element from the
                  //   `proof` array.
                  for (uint256 i = 0; i < totalHashes; i++) {
                      bytes32 a = leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++];
                      bytes32 b = proofFlags[i] ? leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++] : proof[proofPos++];
                      hashes[i] = _hashPair(a, b);
                  }
                  if (totalHashes > 0) {
                      return hashes[totalHashes - 1];
                  } else if (leavesLen > 0) {
                      return leaves[0];
                  } else {
                      return proof[0];
                  }
              }
              /**
               * @dev Calldata version of {processMultiProof}
               *
               * _Available since v4.7._
               */
              function processMultiProofCalldata(
                  bytes32[] calldata proof,
                  bool[] calldata proofFlags,
                  bytes32[] memory leaves
              ) internal pure returns (bytes32 merkleRoot) {
                  // This function rebuild the root hash by traversing the tree up from the leaves. The root is rebuilt by
                  // consuming and producing values on a queue. The queue starts with the `leaves` array, then goes onto the
                  // `hashes` array. At the end of the process, the last hash in the `hashes` array should contain the root of
                  // the merkle tree.
                  uint256 leavesLen = leaves.length;
                  uint256 totalHashes = proofFlags.length;
                  // Check proof validity.
                  require(leavesLen + proof.length - 1 == totalHashes, "MerkleProof: invalid multiproof");
                  // The xxxPos values are "pointers" to the next value to consume in each array. All accesses are done using
                  // `xxx[xxxPos++]`, which return the current value and increment the pointer, thus mimicking a queue's "pop".
                  bytes32[] memory hashes = new bytes32[](totalHashes);
                  uint256 leafPos = 0;
                  uint256 hashPos = 0;
                  uint256 proofPos = 0;
                  // At each step, we compute the next hash using two values:
                  // - a value from the "main queue". If not all leaves have been consumed, we get the next leaf, otherwise we
                  //   get the next hash.
                  // - depending on the flag, either another value for the "main queue" (merging branches) or an element from the
                  //   `proof` array.
                  for (uint256 i = 0; i < totalHashes; i++) {
                      bytes32 a = leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++];
                      bytes32 b = proofFlags[i] ? leafPos < leavesLen ? leaves[leafPos++] : hashes[hashPos++] : proof[proofPos++];
                      hashes[i] = _hashPair(a, b);
                  }
                  if (totalHashes > 0) {
                      return hashes[totalHashes - 1];
                  } else if (leavesLen > 0) {
                      return leaves[0];
                  } else {
                      return proof[0];
                  }
              }
              function _hashPair(bytes32 a, bytes32 b) private pure returns (bytes32) {
                  return a < b ? _efficientHash(a, b) : _efficientHash(b, a);
              }
              function _efficientHash(bytes32 a, bytes32 b) private pure returns (bytes32 value) {
                  /// @solidity memory-safe-assembly
                  assembly {
                      mstore(0x00, a)
                      mstore(0x20, b)
                      value := keccak256(0x00, 0x40)
                  }
              }
          }
          // SPDX-License-Identifier: MIT
          // OpenZeppelin Contracts (last updated v4.6.0) (token/ERC20/IERC20.sol)
          pragma solidity ^0.8.0;
          /**
           * @dev Interface of the ERC20 standard as defined in the EIP.
           */
          interface IERC20 {
              /**
               * @dev Emitted when `value` tokens are moved from one account (`from`) to
               * another (`to`).
               *
               * Note that `value` may be zero.
               */
              event Transfer(address indexed from, address indexed to, uint256 value);
              /**
               * @dev Emitted when the allowance of a `spender` for an `owner` is set by
               * a call to {approve}. `value` is the new allowance.
               */
              event Approval(address indexed owner, address indexed spender, uint256 value);
              /**
               * @dev Returns the amount of tokens in existence.
               */
              function totalSupply() external view returns (uint256);
              /**
               * @dev Returns the amount of tokens owned by `account`.
               */
              function balanceOf(address account) external view returns (uint256);
              /**
               * @dev Moves `amount` tokens from the caller's account to `to`.
               *
               * Returns a boolean value indicating whether the operation succeeded.
               *
               * Emits a {Transfer} event.
               */
              function transfer(address to, uint256 amount) external returns (bool);
              /**
               * @dev Returns the remaining number of tokens that `spender` will be
               * allowed to spend on behalf of `owner` through {transferFrom}. This is
               * zero by default.
               *
               * This value changes when {approve} or {transferFrom} are called.
               */
              function allowance(address owner, address spender) external view returns (uint256);
              /**
               * @dev Sets `amount` as the allowance of `spender` over the caller's tokens.
               *
               * Returns a boolean value indicating whether the operation succeeded.
               *
               * IMPORTANT: Beware that changing an allowance with this method brings the risk
               * that someone may use both the old and the new allowance by unfortunate
               * transaction ordering. One possible solution to mitigate this race
               * condition is to first reduce the spender's allowance to 0 and set the
               * desired value afterwards:
               * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
               *
               * Emits an {Approval} event.
               */
              function approve(address spender, uint256 amount) external returns (bool);
              /**
               * @dev Moves `amount` tokens from `from` to `to` using the
               * allowance mechanism. `amount` is then deducted from the caller's
               * allowance.
               *
               * Returns a boolean value indicating whether the operation succeeded.
               *
               * Emits a {Transfer} event.
               */
              function transferFrom(
                  address from,
                  address to,
                  uint256 amount
              ) external returns (bool);
          }
          // SPDX-License-Identifier: MIT
          pragma solidity ^0.8.0;
          /// @dev Royalty Registry interface, used to support the Royalty Registry.
          /// @dev Source: https://github.com/manifoldxyz/royalty-registry-solidity/blob/main/contracts/specs/IManifold.sol
          /// @author: manifold.xyz
          /**
           * @dev Royalty interface for creator core classes
           */
          interface IManifold {
              /**
               * @dev Get royalites of a token.  Returns list of receivers and basisPoints
               *
               *  bytes4(keccak256('getRoyalties(uint256)')) == 0xbb3bafd6
               *
               *  => 0xbb3bafd6 = 0xbb3bafd6
               */
              function getRoyalties(uint256 tokenId)
                  external
                  view
                  returns (address payable[] memory, uint256[] memory);
          }
          // SPDX-License-Identifier: LGPL-3.0-only
          // Created By: Art Blocks Inc.
          pragma solidity ^0.8.0;
          interface IAdminACLV0 {
              /**
               * @notice Token ID `_tokenId` minted to `_to`.
               * @param previousSuperAdmin The previous superAdmin address.
               * @param newSuperAdmin The new superAdmin address.
               * @param genArt721CoreAddressesToUpdate Array of genArt721Core
               * addresses to update to the new superAdmin, for indexing purposes only.
               */
              event SuperAdminTransferred(
                  address indexed previousSuperAdmin,
                  address indexed newSuperAdmin,
                  address[] genArt721CoreAddressesToUpdate
              );
              /// Type of the Admin ACL contract, e.g. "AdminACLV0"
              function AdminACLType() external view returns (string memory);
              /// super admin address
              function superAdmin() external view returns (address);
              /**
               * @notice Calls transferOwnership on other contract from this contract.
               * This is useful for updating to a new AdminACL contract.
               * @dev this function should be gated to only superAdmin-like addresses.
               */
              function transferOwnershipOn(address _contract, address _newAdminACL)
                  external;
              /**
               * @notice Calls renounceOwnership on other contract from this contract.
               * @dev this function should be gated to only superAdmin-like addresses.
               */
              function renounceOwnershipOn(address _contract) external;
              /**
               * @notice Checks if sender `_sender` is allowed to call function with selector
               * `_selector` on contract `_contract`.
               */
              function allowed(
                  address _sender,
                  address _contract,
                  bytes4 _selector
              ) external returns (bool);
          }
          // SPDX-License-Identifier: LGPL-3.0-only
          // Created By: Art Blocks Inc.
          import "./IFilteredMinterV0.sol";
          pragma solidity ^0.8.0;
          /**
           * @title This interface extends the IFilteredMinterV0 interface in order to
           * add support for generic project minter configuration updates.
           * @dev keys represent strings of finite length encoded in bytes32 to minimize
           * gas.
           * @author Art Blocks Inc.
           */
          interface IFilteredMinterV1 is IFilteredMinterV0 {
              /// ANY
              /**
               * @notice Generic project minter configuration event. Removes key `_key`
               * for project `_projectId`.
               */
              event ConfigKeyRemoved(uint256 indexed _projectId, bytes32 _key);
              /// BOOL
              /**
               * @notice Generic project minter configuration event. Sets value of key
               * `_key` to `_value` for project `_projectId`.
               */
              event ConfigValueSet(uint256 indexed _projectId, bytes32 _key, bool _value);
              /// UINT256
              /**
               * @notice Generic project minter configuration event. Sets value of key
               * `_key` to `_value` for project `_projectId`.
               */
              event ConfigValueSet(
                  uint256 indexed _projectId,
                  bytes32 _key,
                  uint256 _value
              );
              /**
               * @notice Generic project minter configuration event. Adds value `_value`
               * to the set of uint256 at key `_key` for project `_projectId`.
               */
              event ConfigValueAddedToSet(
                  uint256 indexed _projectId,
                  bytes32 _key,
                  uint256 _value
              );
              /**
               * @notice Generic project minter configuration event. Removes value
               * `_value` to the set of uint256 at key `_key` for project `_projectId`.
               */
              event ConfigValueRemovedFromSet(
                  uint256 indexed _projectId,
                  bytes32 _key,
                  uint256 _value
              );
              /// ADDRESS
              /**
               * @notice Generic project minter configuration event. Sets value of key
               * `_key` to `_value` for project `_projectId`.
               */
              event ConfigValueSet(
                  uint256 indexed _projectId,
                  bytes32 _key,
                  address _value
              );
              /**
               * @notice Generic project minter configuration event. Adds value `_value`
               * to the set of addresses at key `_key` for project `_projectId`.
               */
              event ConfigValueAddedToSet(
                  uint256 indexed _projectId,
                  bytes32 _key,
                  address _value
              );
              /**
               * @notice Generic project minter configuration event. Removes value
               * `_value` to the set of addresses at key `_key` for project `_projectId`.
               */
              event ConfigValueRemovedFromSet(
                  uint256 indexed _projectId,
                  bytes32 _key,
                  address _value
              );
              /// BYTES32
              /**
               * @notice Generic project minter configuration event. Sets value of key
               * `_key` to `_value` for project `_projectId`.
               */
              event ConfigValueSet(
                  uint256 indexed _projectId,
                  bytes32 _key,
                  bytes32 _value
              );
              /**
               * @notice Generic project minter configuration event. Adds value `_value`
               * to the set of bytes32 at key `_key` for project `_projectId`.
               */
              event ConfigValueAddedToSet(
                  uint256 indexed _projectId,
                  bytes32 _key,
                  bytes32 _value
              );
              /**
               * @notice Generic project minter configuration event. Removes value
               * `_value` to the set of bytes32 at key `_key` for project `_projectId`.
               */
              event ConfigValueRemovedFromSet(
                  uint256 indexed _projectId,
                  bytes32 _key,
                  bytes32 _value
              );
              /**
               * @dev Strings not supported. Recommend conversion of (short) strings to
               * bytes32 to remain gas-efficient.
               */
          }
          // SPDX-License-Identifier: LGPL-3.0-only
          // Created By: Art Blocks Inc.
          pragma solidity ^0.8.0;
          interface IFilteredMinterV0 {
              /**
               * @notice Price per token in wei updated for project `_projectId` to
               * `_pricePerTokenInWei`.
               */
              event PricePerTokenInWeiUpdated(
                  uint256 indexed _projectId,
                  uint256 indexed _pricePerTokenInWei
              );
              /**
               * @notice Currency updated for project `_projectId` to symbol
               * `_currencySymbol` and address `_currencyAddress`.
               */
              event ProjectCurrencyInfoUpdated(
                  uint256 indexed _projectId,
                  address indexed _currencyAddress,
                  string _currencySymbol
              );
              /// togglePurchaseToDisabled updated
              event PurchaseToDisabledUpdated(
                  uint256 indexed _projectId,
                  bool _purchaseToDisabled
              );
              // getter function of public variable
              function minterType() external view returns (string memory);
              function genArt721CoreAddress() external returns (address);
              function minterFilterAddress() external returns (address);
              // Triggers a purchase of a token from the desired project, to the
              // TX-sending address.
              function purchase(uint256 _projectId)
                  external
                  payable
                  returns (uint256 tokenId);
              // Triggers a purchase of a token from the desired project, to the specified
              // receiving address.
              function purchaseTo(address _to, uint256 _projectId)
                  external
                  payable
                  returns (uint256 tokenId);
              // Toggles the ability for `purchaseTo` to be called directly with a
              // specified receiving address that differs from the TX-sending address.
              function togglePurchaseToDisabled(uint256 _projectId) external;
              // Called to make the minter contract aware of the max invocations for a
              // given project.
              function setProjectMaxInvocations(uint256 _projectId) external;
              // Gets if token price is configured, token price in wei, currency symbol,
              // and currency address, assuming this is project's minter.
              // Supersedes any defined core price.
              function getPriceInfo(uint256 _projectId)
                  external
                  view
                  returns (
                      bool isConfigured,
                      uint256 tokenPriceInWei,
                      string memory currencySymbol,
                      address currencyAddress
                  );
          }
          

          File 2 of 3: GenArt721CoreV3_Explorations
          // SPDX-License-Identifier: LGPL-3.0-only
          pragma solidity 0.8.17;
          // Created By: Art Blocks Inc.
          import "../interfaces/0.8.x/IRandomizerV2.sol";
          import "../interfaces/0.8.x/IAdminACLV0.sol";
          import "../interfaces/0.8.x/IGenArt721CoreContractV3.sol";
          import "../interfaces/0.8.x/IManifold.sol";
          import "@openzeppelin-4.7/contracts/utils/Strings.sol";
          import "@openzeppelin-4.7/contracts/access/Ownable.sol";
          import "../libs/0.8.x/ERC721_PackedHashSeed.sol";
          import "../libs/0.8.x/BytecodeStorage.sol";
          import "../libs/0.8.x/Bytes32Strings.sol";
          /**
           * @title Art Blocks ERC-721 core contract, V3_Explorations.
           * @author Art Blocks Inc.
           * @notice Privileged Roles and Ownership:
           * This contract is designed to be managed, with progressively limited powers
           * as a project progresses from active to locked.
           * Privileged roles and abilities are controlled by the admin ACL contract and
           * artists. Both of these roles hold extensive power and can arbitrarily
           * control and modify portions of projects, dependent upon project state. After
           * a project is locked, important project metadata fields are locked including
           * the project name, artist name, and script and display details. Edition size
           * can never be increased.
           * Care must be taken to ensure that the admin ACL contract and artist
           * addresses are secure behind a multi-sig or other access control mechanism.
           * ----------------------------------------------------------------------------
           * The following functions are restricted to the Admin ACL contract:
           * - updateArtblocksDependencyRegistryAddress
           * - updateArtblocksPrimarySalesAddress
           * - updateArtblocksSecondarySalesAddress
           * - updateArtblocksPrimarySalesPercentage (up to 100%)
           * - updateArtblocksSecondarySalesBPS (up to 100%)
           * - updateMinterContract
           * - updateRandomizerAddress
           * - toggleProjectIsActive
           * - addProject
           * - forbidNewProjects (forever forbidding new projects)
           * - updateDefaultBaseURI (used to initialize new project base URIs)
           * ----------------------------------------------------------------------------
           * The following functions are restricted to either the the Artist address or
           * the Admin ACL contract, only when the project is not locked:
           * - updateProjectName
           * - updateProjectArtistName
           * - updateProjectLicense
           * - Change project script via addProjectScript, updateProjectScript,
           *   and removeProjectLastScript
           * - updateProjectScriptType
           * - updateProjectAspectRatio
           * ----------------------------------------------------------------------------
           * The following functions are restricted to only the Artist address:
           * - proposeArtistPaymentAddressesAndSplits (Note that this has to be accepted
           *   by adminAcceptArtistAddressesAndSplits to take effect, which is restricted
           *   to the Admin ACL contract, or the artist if the core contract owner has
           *   renounced ownership. Also note that a proposal will be automatically
           *   accepted if the artist only proposes changed payee percentages without
           *   modifying any payee addresses, or is only removing payee addresses.)
           * - toggleProjectIsPaused (note the artist can still mint while paused)
           * - updateProjectSecondaryMarketRoyaltyPercentage (up to
               ARTIST_MAX_SECONDARY_ROYALTY_PERCENTAGE percent)
           * - updateProjectWebsite
           * - updateProjectMaxInvocations (to a number greater than or equal to the
           *   current number of invocations, and less than current project maximum
           *   invocations)
           * - updateProjectBaseURI (controlling the base URI for tokens in the project)
           * ----------------------------------------------------------------------------
           * The following function is restricted to either the Admin ACL contract, or
           * the Artist address if the core contract owner has renounced ownership:
           * - adminAcceptArtistAddressesAndSplits
           * - updateProjectArtistAddress (owner ultimately controlling the project and
           *   its and-on revenue, unless owner has renounced ownership)
           * ----------------------------------------------------------------------------
           * The following function is restricted to the artist when a project is
           * unlocked, and only callable by Admin ACL contract when a project is locked:
           * - updateProjectDescription
           * ----------------------------------------------------------------------------
           * The following function is restricted to owner calling directly:
           * - transferOwnership
           * - renounceOwnership
           * ----------------------------------------------------------------------------
           * Additional admin and artist privileged roles may be described on minters,
           * registries, and other contracts that may interact with this core contract.
           */
          contract GenArt721CoreV3_Explorations is
              ERC721_PackedHashSeed,
              Ownable,
              IGenArt721CoreContractV3
          {
              using BytecodeStorage for string;
              using BytecodeStorage for address;
              using Bytes32Strings for bytes32;
              using Strings for uint256;
              using Strings for address;
              uint256 constant ONE_HUNDRED = 100;
              uint256 constant ONE_MILLION = 1_000_000;
              uint24 constant ONE_MILLION_UINT24 = 1_000_000;
              uint256 constant FOUR_WEEKS_IN_SECONDS = 2_419_200;
              uint8 constant AT_CHARACTER_CODE = uint8(bytes1("@")); // 0x40
              // numeric constants
              uint256 constant ART_BLOCKS_MAX_PRIMARY_SALES_PERCENTAGE = 100; // 100%
              uint256 constant ART_BLOCKS_MAX_SECONDARY_SALES_BPS = 10000; // 10_000 BPS = 100%
              uint256 constant ARTIST_MAX_SECONDARY_ROYALTY_PERCENTAGE = 95; // 95%
              // This contract emits generic events that contain fields that indicate
              // which parameter has been updated. This is sufficient for application
              // state management, while also simplifying the contract and indexing code.
              // This was done as an alternative to having custom events that emit what
              // field-values have changed for each event, given that changed values can
              // be introspected by indexers due to the design of this smart contract
              // exposing these state changes via publicly viewable fields.
              //
              // The following fields are used to indicate which contract-level parameter
              // has been updated in the `PlatformUpdated` event:
              bytes32 constant FIELD_NEXT_PROJECT_ID = "nextProjectId";
              bytes32 constant FIELD_NEW_PROJECTS_FORBIDDEN = "newProjectsForbidden";
              bytes32 constant FIELD_DEFAULT_BASE_URI = "defaultBaseURI";
              bytes32 constant FIELD_ARTBLOCKS_PRIMARY_SALES_ADDRESS =
                  "artblocksPrimarySalesAddress";
              bytes32 constant FIELD_ARTBLOCKS_SECONDARY_SALES_ADDRESS =
                  "artblocksSecondarySalesAddress";
              bytes32 constant FIELD_RANDOMIZER_ADDRESS = "randomizerAddress";
              // note: Curation registry address will never be updated on V3 Explorations
              bytes32 constant FIELD_ARTBLOCKS_CURATION_REGISTRY_ADDRESS =
                  "curationRegistryAddress";
              bytes32 constant FIELD_ARTBLOCKS_DEPENDENCY_REGISTRY_ADDRESS =
                  "dependencyRegistryAddress";
              bytes32 constant FIELD_ARTBLOCKS_PRIMARY_SALES_PERCENTAGE =
                  "artblocksPrimaryPercentage";
              bytes32 constant FIELD_ARTBLOCKS_SECONDARY_SALES_BPS =
                  "artblocksSecondaryBPS";
              // The following fields are used to indicate which project-level parameter
              // has been updated in the `ProjectUpdated` event:
              bytes32 constant FIELD_PROJECT_COMPLETED = "completed";
              bytes32 constant FIELD_PROJECT_ACTIVE = "active";
              bytes32 constant FIELD_PROJECT_ARTIST_ADDRESS = "artistAddress";
              bytes32 constant FIELD_PROJECT_PAUSED = "paused";
              bytes32 constant FIELD_PROJECT_CREATED = "created";
              bytes32 constant FIELD_PROJECT_NAME = "name";
              bytes32 constant FIELD_PROJECT_ARTIST_NAME = "artistName";
              bytes32 constant FIELD_PROJECT_SECONDARY_MARKET_ROYALTY_PERCENTAGE =
                  "royaltyPercentage";
              bytes32 constant FIELD_PROJECT_DESCRIPTION = "description";
              bytes32 constant FIELD_PROJECT_WEBSITE = "website";
              bytes32 constant FIELD_PROJECT_LICENSE = "license";
              bytes32 constant FIELD_PROJECT_MAX_INVOCATIONS = "maxInvocations";
              bytes32 constant FIELD_PROJECT_SCRIPT = "script";
              bytes32 constant FIELD_PROJECT_SCRIPT_TYPE = "scriptType";
              bytes32 constant FIELD_PROJECT_ASPECT_RATIO = "aspectRatio";
              bytes32 constant FIELD_PROJECT_BASE_URI = "baseURI";
              /// Curation registry is not relevant for Art Blocks Explorations, and will
              /// remain null for interface-conformance purposes only, specifically for
              /// indexing layers.
              address public artblocksCurationRegistryAddress;
              /// Dependency registry managed by Art Blocks
              address public artblocksDependencyRegistryAddress;
              /// current randomizer contract
              IRandomizerV2 public randomizerContract;
              /// append-only array of all randomizer contract addresses ever used by
              /// this contract
              address[] private _historicalRandomizerAddresses;
              /// admin ACL contract
              IAdminACLV0 public adminACLContract;
              struct Project {
                  uint24 invocations;
                  uint24 maxInvocations;
                  uint24 scriptCount;
                  // max uint64 ~= 1.8e19 sec ~= 570 billion years
                  uint64 completedTimestamp;
                  bool active;
                  bool paused;
                  string name;
                  string artist;
                  string description;
                  string website;
                  string license;
                  string projectBaseURI;
                  bytes32 scriptTypeAndVersion;
                  string aspectRatio;
                  // mapping from script index to address storing script in bytecode
                  mapping(uint256 => address) scriptBytecodeAddresses;
              }
              mapping(uint256 => Project) projects;
              /// packed struct containing project financial information
              struct ProjectFinance {
                  address payable additionalPayeePrimarySales;
                  // packed uint: max of 95, max uint8 = 255
                  uint8 secondaryMarketRoyaltyPercentage;
                  address payable additionalPayeeSecondarySales;
                  // packed uint: max of 100, max uint8 = 255
                  uint8 additionalPayeeSecondarySalesPercentage;
                  address payable artistAddress;
                  // packed uint: max of 100, max uint8 = 255
                  uint8 additionalPayeePrimarySalesPercentage;
              }
              // Project financials mapping
              mapping(uint256 => ProjectFinance) projectIdToFinancials;
              /// hash of artist's proposed payment updates to be approved by admin
              mapping(uint256 => bytes32) public proposedArtistAddressesAndSplitsHash;
              /// Art Blocks payment address for all primary sales revenues (packed)
              address payable public artblocksPrimarySalesAddress;
              /// Percentage of primary sales revenue allocated to Art Blocks (packed)
              // packed uint: max of 25, max uint8 = 255
              uint8 private _artblocksPrimarySalesPercentage = 10;
              /// Art Blocks payment address for all secondary sales royalty revenues
              address payable public artblocksSecondarySalesAddress;
              /// Basis Points of secondary sales royalties allocated to Art Blocks
              uint256 public artblocksSecondarySalesBPS = 250;
              /// single minter allowed for this core contract
              address public minterContract;
              /// starting (initial) project ID on this contract
              uint256 public immutable startingProjectId;
              /// next project ID to be created
              uint248 private _nextProjectId;
              /// bool indicating if adding new projects is forbidden;
              /// default behavior is to allow new projects
              bool public newProjectsForbidden;
              /// version & type of this core contract
              /// coreVersion is updated from Flagship V3 core due to minor changes
              /// implemented in the Explorations version of the contract.
              string public constant coreVersion = "v3.0.1";
              /// coreType remains consistent with flagship V3 core because external &
              /// public functions used for indexing are unchanged.
              string public constant coreType = "GenArt721CoreV3";
              /// default base URI to initialize all new project projectBaseURI values to
              string public defaultBaseURI;
              modifier onlyNonZeroAddress(address _address) {
                  require(_address != address(0), "Must input non-zero address");
                  _;
              }
              modifier onlyNonEmptyString(string memory _string) {
                  require(bytes(_string).length != 0, "Must input non-empty string");
                  _;
              }
              modifier onlyValidTokenId(uint256 _tokenId) {
                  require(_exists(_tokenId), "Token ID does not exist");
                  _;
              }
              modifier onlyValidProjectId(uint256 _projectId) {
                  require(
                      (_projectId >= startingProjectId) && (_projectId < _nextProjectId),
                      "Project ID does not exist"
                  );
                  _;
              }
              modifier onlyUnlocked(uint256 _projectId) {
                  // Note: calling `_projectUnlocked` enforces that the `_projectId`
                  //       passed in is valid.`
                  require(_projectUnlocked(_projectId), "Only if unlocked");
                  _;
              }
              modifier onlyAdminACL(bytes4 _selector) {
                  require(
                      adminACLAllowed(msg.sender, address(this), _selector),
                      "Only Admin ACL allowed"
                  );
                  _;
              }
              modifier onlyArtist(uint256 _projectId) {
                  require(
                      msg.sender == projectIdToFinancials[_projectId].artistAddress,
                      "Only artist"
                  );
                  _;
              }
              modifier onlyArtistOrAdminACL(uint256 _projectId, bytes4 _selector) {
                  require(
                      msg.sender == projectIdToFinancials[_projectId].artistAddress ||
                          adminACLAllowed(msg.sender, address(this), _selector),
                      "Only artist or Admin ACL allowed"
                  );
                  _;
              }
              /**
               * This modifier allows the artist of a project to call a function if the
               * owner of the contract has renounced ownership. This is to allow the
               * contract to continue to function if the owner decides to renounce
               * ownership.
               */
              modifier onlyAdminACLOrRenouncedArtist(
                  uint256 _projectId,
                  bytes4 _selector
              ) {
                  require(
                      adminACLAllowed(msg.sender, address(this), _selector) ||
                          (owner() == address(0) &&
                              msg.sender ==
                              projectIdToFinancials[_projectId].artistAddress),
                      "Only Admin ACL allowed, or artist if owner has renounced"
                  );
                  _;
              }
              /**
               * @notice Initializes contract.
               * @param _tokenName Name of token.
               * @param _tokenSymbol Token symbol.
               * @param _randomizerContract Randomizer contract.
               * @param _adminACLContract Address of admin access control contract, to be
               * set as contract owner.
               * @param _startingProjectId The initial next project ID.
               * @dev _startingProjectId should be set to a value much, much less than
               * max(uint248), but an explicit input type of `uint248` is used as it is
               * safer to cast up to `uint256` than it is to cast down for the purposes
               * of setting `_nextProjectId`.
               */
              constructor(
                  string memory _tokenName,
                  string memory _tokenSymbol,
                  address _randomizerContract,
                  address _adminACLContract,
                  uint248 _startingProjectId
              )
                  ERC721_PackedHashSeed(_tokenName, _tokenSymbol)
                  onlyNonZeroAddress(_randomizerContract)
              {
                  // record contracts starting project ID
                  // casting-up is safe
                  startingProjectId = uint256(_startingProjectId);
                  _updateArtblocksPrimarySalesAddress(msg.sender);
                  _updateArtblocksSecondarySalesAddress(msg.sender);
                  _updateRandomizerAddress(_randomizerContract);
                  // set AdminACL management contract as owner
                  _transferOwnership(_adminACLContract);
                  // initialize default base URI
                  _updateDefaultBaseURI(
                      string.concat(
                          "https://token.artblocks.io/",
                          address(this).toHexString(),
                          "/"
                      )
                  );
                  // initialize next project ID
                  _nextProjectId = _startingProjectId;
                  emit PlatformUpdated(FIELD_NEXT_PROJECT_ID);
              }
              /**
               * @notice Mints a token from project `_projectId` and sets the
               * token's owner to `_to`. Hash may or may not be assigned to the token
               * during the mint transaction, depending on the randomizer contract.
               * @param _to Address to be the minted token's owner.
               * @param _projectId Project ID to mint a token on.
               * @param _by Purchaser of minted token.
               * @return _tokenId The ID of the minted token.
               * @dev sender must be the allowed minterContract
               * @dev name of function is optimized for gas usage
               */
              function mint_Ecf(
                  address _to,
                  uint256 _projectId,
                  address _by
              ) external returns (uint256 _tokenId) {
                  // CHECKS
                  require(msg.sender == minterContract, "Must mint from minter contract");
                  Project storage project = projects[_projectId];
                  // load invocations into memory
                  uint24 invocationsBefore = project.invocations;
                  uint24 invocationsAfter;
                  unchecked {
                      // invocationsBefore guaranteed <= maxInvocations <= 1_000_000,
                      // 1_000_000 << max uint24, so no possible overflow
                      invocationsAfter = invocationsBefore + 1;
                  }
                  uint24 maxInvocations = project.maxInvocations;
                  require(
                      invocationsBefore < maxInvocations,
                      "Must not exceed max invocations"
                  );
                  require(
                      project.active ||
                          _by == projectIdToFinancials[_projectId].artistAddress,
                      "Project must exist and be active"
                  );
                  require(
                      !project.paused ||
                          _by == projectIdToFinancials[_projectId].artistAddress,
                      "Purchases are paused."
                  );
                  // EFFECTS
                  // increment project's invocations
                  project.invocations = invocationsAfter;
                  uint256 thisTokenId;
                  unchecked {
                      // invocationsBefore is uint24 << max uint256. In production use,
                      // _projectId * ONE_MILLION must be << max uint256, otherwise
                      // tokenIdToProjectId function become invalid.
                      // Therefore, no risk of overflow
                      thisTokenId = (_projectId * ONE_MILLION) + invocationsBefore;
                  }
                  // mark project as completed if hit max invocations
                  if (invocationsAfter == maxInvocations) {
                      _completeProject(_projectId);
                  }
                  // INTERACTIONS
                  _mint(_to, thisTokenId);
                  // token hash is updated by the randomizer contract on V3
                  randomizerContract.assignTokenHash(thisTokenId);
                  // Do not need to also log `projectId` in event, as the `projectId` for
                  // a given token can be derived from the `tokenId` with:
                  //   projectId = tokenId / 1_000_000
                  emit Mint(_to, thisTokenId);
                  return thisTokenId;
              }
              /**
               * @notice Sets the hash seed for a given token ID `_tokenId`.
               * May only be called by the current randomizer contract.
               * May only be called for tokens that have not already been assigned a
               * non-zero hash.
               * @param _tokenId Token ID to set the hash for.
               * @param _hashSeed Hash seed to set for the token ID. Only last 12 bytes
               * will be used.
               * @dev gas-optimized function name because called during mint sequence
               * @dev if a separate event is required when the token hash is set, e.g.
               * for indexing purposes, it must be emitted by the randomizer. This is to
               * minimize gas when minting.
               */
              function setTokenHash_8PT(uint256 _tokenId, bytes32 _hashSeed)
                  external
                  onlyValidTokenId(_tokenId)
              {
                  OwnerAndHashSeed storage ownerAndHashSeed = _ownersAndHashSeeds[
                      _tokenId
                  ];
                  require(
                      msg.sender == address(randomizerContract),
                      "Only randomizer may set"
                  );
                  require(
                      ownerAndHashSeed.hashSeed == bytes12(0),
                      "Token hash already set"
                  );
                  require(_hashSeed != bytes12(0), "No zero hash seed");
                  ownerAndHashSeed.hashSeed = bytes12(_hashSeed);
              }
              /**
               * @notice Allows owner (AdminACL) to revoke ownership of the contract.
               * Note that the contract is intended to continue to function after the
               * owner renounces ownership, but no new projects will be able to be added.
               * Renouncing ownership will leave the contract without an owner,
               * thereby removing any functionality that is only available to the
               * owner/AdminACL contract. The same is true for any dependent contracts
               * that also integrate with the owner/AdminACL contract (e.g. potentially
               * minter suite contracts, registry contracts, etc.).
               * After renouncing ownership, artists will be in control of updates to
               * their payment addresses and splits (see modifier
               * onlyAdminACLOrRenouncedArtist`).
               * While there is no currently intended reason to call this method based on
               * defined Art Blocks business practices, this method exists to allow
               * artists to continue to maintain the limited set of contract
               * functionality that exists post-project-lock in an environment in which
               * there is no longer an admin maintaining this smart contract.
               * @dev This function is intended to be called directly by the AdminACL,
               * not by an address allowed by the AdminACL contract.
               */
              function renounceOwnership() public override onlyOwner {
                  // broadcast that new projects are no longer allowed (if not already)
                  _forbidNewProjects();
                  // renounce ownership viw Ownable
                  Ownable.renounceOwnership();
              }
              /**
               * @notice Warning: Configuring Curation Registry contract is not supported
               * on V3 Explorations version of this contract.
               * This method exists only to maintain this portion of the contract
               * interface with other V3 cores.
               * @param _artblocksCurationRegistryAddress)
               */
              function updateArtblocksCurationRegistryAddress(
                  address _artblocksCurationRegistryAddress
              )
                  external
                  onlyAdminACL(this.updateArtblocksCurationRegistryAddress.selector)
                  onlyNonZeroAddress(_artblocksCurationRegistryAddress)
              {
                  revert("Action not supported");
              }
              /**
               * @notice Updates reference to Art Blocks Dependency Registry contract.
               * @param _artblocksDependencyRegistryAddress Address of new Dependency
               * Registry.
               */
              function updateArtblocksDependencyRegistryAddress(
                  address _artblocksDependencyRegistryAddress
              )
                  external
                  onlyAdminACL(this.updateArtblocksDependencyRegistryAddress.selector)
                  onlyNonZeroAddress(_artblocksDependencyRegistryAddress)
              {
                  artblocksDependencyRegistryAddress = _artblocksDependencyRegistryAddress;
                  emit PlatformUpdated(FIELD_ARTBLOCKS_DEPENDENCY_REGISTRY_ADDRESS);
              }
              /**
               * @notice Updates artblocksPrimarySalesAddress to
               * `_artblocksPrimarySalesAddress`.
               * @param _artblocksPrimarySalesAddress Address of new primary sales
               * payment address.
               */
              function updateArtblocksPrimarySalesAddress(
                  address payable _artblocksPrimarySalesAddress
              )
                  external
                  onlyAdminACL(this.updateArtblocksPrimarySalesAddress.selector)
                  onlyNonZeroAddress(_artblocksPrimarySalesAddress)
              {
                  _updateArtblocksPrimarySalesAddress(_artblocksPrimarySalesAddress);
              }
              /**
               * @notice Updates Art Blocks secondary sales royalty payment address to
               * `_artblocksSecondarySalesAddress`.
               * @param _artblocksSecondarySalesAddress Address of new secondary sales
               * payment address.
               */
              function updateArtblocksSecondarySalesAddress(
                  address payable _artblocksSecondarySalesAddress
              )
                  external
                  onlyAdminACL(this.updateArtblocksSecondarySalesAddress.selector)
                  onlyNonZeroAddress(_artblocksSecondarySalesAddress)
              {
                  _updateArtblocksSecondarySalesAddress(_artblocksSecondarySalesAddress);
              }
              /**
               * @notice Updates Art Blocks primary sales revenue percentage to
               * `artblocksPrimarySalesPercentage_`.
               * @param artblocksPrimarySalesPercentage_ New primary sales revenue
               * percentage.
               */
              function updateArtblocksPrimarySalesPercentage(
                  uint256 artblocksPrimarySalesPercentage_
              )
                  external
                  onlyAdminACL(this.updateArtblocksPrimarySalesPercentage.selector)
              {
                  require(
                      artblocksPrimarySalesPercentage_ <=
                          ART_BLOCKS_MAX_PRIMARY_SALES_PERCENTAGE,
                      "Max of ART_BLOCKS_MAX_PRIMARY_SALES_PERCENTAGE percent"
                  );
                  _artblocksPrimarySalesPercentage = uint8(
                      artblocksPrimarySalesPercentage_
                  );
                  emit PlatformUpdated(FIELD_ARTBLOCKS_PRIMARY_SALES_PERCENTAGE);
              }
              /**
               * @notice Updates Art Blocks secondary sales royalty Basis Points to
               * `_artblocksSecondarySalesBPS`.
               * @param _artblocksSecondarySalesBPS New secondary sales royalty Basis
               * points.
               * @dev Due to secondary royalties being ultimately enforced via social
               * consensus, no hard upper limit is imposed on the BPS value, other than
               * <= 100% royalty, which would not make mathematical sense. Realistically,
               * changing this value is expected to either never occur, or be a rare
               * occurrence.
               */
              function updateArtblocksSecondarySalesBPS(
                  uint256 _artblocksSecondarySalesBPS
              ) external onlyAdminACL(this.updateArtblocksSecondarySalesBPS.selector) {
                  require(
                      _artblocksSecondarySalesBPS <= ART_BLOCKS_MAX_SECONDARY_SALES_BPS,
                      "Max of ART_BLOCKS_MAX_SECONDARY_SALES_BPS BPS"
                  );
                  artblocksSecondarySalesBPS = _artblocksSecondarySalesBPS;
                  emit PlatformUpdated(FIELD_ARTBLOCKS_SECONDARY_SALES_BPS);
              }
              /**
               * @notice Updates minter to `_address`.
               * @param _address Address of new minter.
               */
              function updateMinterContract(address _address)
                  external
                  onlyAdminACL(this.updateMinterContract.selector)
                  onlyNonZeroAddress(_address)
              {
                  minterContract = _address;
                  emit MinterUpdated(_address);
              }
              /**
               * @notice Updates randomizer to `_randomizerAddress`.
               * @param _randomizerAddress Address of new randomizer.
               */
              function updateRandomizerAddress(address _randomizerAddress)
                  external
                  onlyAdminACL(this.updateRandomizerAddress.selector)
                  onlyNonZeroAddress(_randomizerAddress)
              {
                  _updateRandomizerAddress(_randomizerAddress);
              }
              /**
               * @notice Toggles project `_projectId` as active/inactive.
               * @param _projectId Project ID to be toggled.
               */
              function toggleProjectIsActive(uint256 _projectId)
                  external
                  onlyAdminACL(this.toggleProjectIsActive.selector)
                  onlyValidProjectId(_projectId)
              {
                  projects[_projectId].active = !projects[_projectId].active;
                  emit ProjectUpdated(_projectId, FIELD_PROJECT_ACTIVE);
              }
              /**
               * @notice Artist proposes updated set of artist address, additional payee
               * addresses, and percentage splits for project `_projectId`. Addresses and
               * percentages do not have to all be changed, but they must all be defined
               * as a complete set.
               * Note that if the artist is only proposing a change to the payee percentage
               * splits, without modifying the payee addresses, the proposal will be
               * automatically approved and the new splits will become active immediately.
               * Automatic approval will also be granted if the artist is only removing
               * additional payee addresses, without adding any new ones.
               * Also note that if the artist is proposing sending funds to the zero
               * address, this function will revert and the proposal will not be created.
               * @param _projectId Project ID.
               * @param _artistAddress Artist address that controls the project, and may
               * receive payments.
               * @param _additionalPayeePrimarySales Address that may receive a
               * percentage split of the artist's primary sales revenue.
               * @param _additionalPayeePrimarySalesPercentage Percent of artist's
               * portion of primary sale revenue that will be split to address
               * `_additionalPayeePrimarySales`.
               * @param _additionalPayeeSecondarySales Address that may receive a percentage
               * split of the secondary sales royalties.
               * @param _additionalPayeeSecondarySalesPercentage Percent of artist's portion
               * of secondary sale royalties that will be split to address
               * `_additionalPayeeSecondarySales`.
               * @dev `_artistAddress` must be a valid address (non-zero-address), but it
               * is intentionally allowable for `_additionalPayee{Primary,Secondaary}Sales`
               * and their associated percentages to be zero'd out by the controlling artist.
               */
              function proposeArtistPaymentAddressesAndSplits(
                  uint256 _projectId,
                  address payable _artistAddress,
                  address payable _additionalPayeePrimarySales,
                  uint256 _additionalPayeePrimarySalesPercentage,
                  address payable _additionalPayeeSecondarySales,
                  uint256 _additionalPayeeSecondarySalesPercentage
              )
                  external
                  onlyValidProjectId(_projectId)
                  onlyArtist(_projectId)
                  onlyNonZeroAddress(_artistAddress)
              {
                  ProjectFinance storage projectFinance = projectIdToFinancials[
                      _projectId
                  ];
                  // checks
                  require(
                      _additionalPayeePrimarySalesPercentage <= ONE_HUNDRED &&
                          _additionalPayeeSecondarySalesPercentage <= ONE_HUNDRED,
                      "Max of 100%"
                  );
                  require(
                      _additionalPayeePrimarySalesPercentage == 0 ||
                          _additionalPayeePrimarySales != address(0),
                      "Primary payee is zero address"
                  );
                  require(
                      _additionalPayeeSecondarySalesPercentage == 0 ||
                          _additionalPayeeSecondarySales != address(0),
                      "Secondary payee is zero address"
                  );
                  // effects
                  // emit event for off-chain indexing
                  // note: always emit a proposal event, even in the pathway of
                  // automatic approval, to simplify indexing expectations
                  emit ProposedArtistAddressesAndSplits(
                      _projectId,
                      _artistAddress,
                      _additionalPayeePrimarySales,
                      _additionalPayeePrimarySalesPercentage,
                      _additionalPayeeSecondarySales,
                      _additionalPayeeSecondarySalesPercentage
                  );
                  // automatically accept if no proposed addresses modifications, or if
                  // the proposal only removes payee addresses.
                  // store proposal hash on-chain, only if not automatic accept
                  bool automaticAccept;
                  {
                      // block scope to avoid stack too deep error
                      bool artistUnchanged = _artistAddress ==
                          projectFinance.artistAddress;
                      bool additionalPrimaryUnchangedOrRemoved = (_additionalPayeePrimarySales ==
                              projectFinance.additionalPayeePrimarySales) ||
                              (_additionalPayeePrimarySales == address(0));
                      bool additionalSecondaryUnchangedOrRemoved = (_additionalPayeeSecondarySales ==
                              projectFinance.additionalPayeeSecondarySales) ||
                              (_additionalPayeeSecondarySales == address(0));
                      automaticAccept =
                          artistUnchanged &&
                          additionalPrimaryUnchangedOrRemoved &&
                          additionalSecondaryUnchangedOrRemoved;
                  }
                  if (automaticAccept) {
                      // clear any previously proposed values
                      proposedArtistAddressesAndSplitsHash[_projectId] = bytes32(0);
                      // update storage
                      // (artist address cannot change during automatic accept)
                      projectFinance
                          .additionalPayeePrimarySales = _additionalPayeePrimarySales;
                      // safe to cast as uint8 as max is 100%, max uint8 is 255
                      projectFinance.additionalPayeePrimarySalesPercentage = uint8(
                          _additionalPayeePrimarySalesPercentage
                      );
                      projectFinance
                          .additionalPayeeSecondarySales = _additionalPayeeSecondarySales;
                      // safe to cast as uint8 as max is 100%, max uint8 is 255
                      projectFinance.additionalPayeeSecondarySalesPercentage = uint8(
                          _additionalPayeeSecondarySalesPercentage
                      );
                      // emit event for off-chain indexing
                      emit AcceptedArtistAddressesAndSplits(_projectId);
                  } else {
                      proposedArtistAddressesAndSplitsHash[_projectId] = keccak256(
                          abi.encode(
                              _artistAddress,
                              _additionalPayeePrimarySales,
                              _additionalPayeePrimarySalesPercentage,
                              _additionalPayeeSecondarySales,
                              _additionalPayeeSecondarySalesPercentage
                          )
                      );
                  }
              }
              /**
               * @notice Admin accepts a proposed set of updated artist address,
               * additional payee addresses, and percentage splits for project
               * `_projectId`. Addresses and percentages do not have to all be changed,
               * but they must all be defined as a complete set.
               * @param _projectId Project ID.
               * @param _artistAddress Artist address that controls the project, and may
               * receive payments.
               * @param _additionalPayeePrimarySales Address that may receive a
               * percentage split of the artist's primary sales revenue.
               * @param _additionalPayeePrimarySalesPercentage Percent of artist's
               * portion of primary sale revenue that will be split to address
               * `_additionalPayeePrimarySales`.
               * @param _additionalPayeeSecondarySales Address that may receive a percentage
               * split of the secondary sales royalties.
               * @param _additionalPayeeSecondarySalesPercentage Percent of artist's portion
               * of secondary sale royalties that will be split to address
               * `_additionalPayeeSecondarySales`.
               * @dev this must be called by the Admin ACL contract, and must only accept
               * the most recent proposed values for a given project (validated on-chain
               * by comparing the hash of the proposed and accepted values).
               * @dev `_artistAddress` must be a valid address (non-zero-address), but it
               * is intentionally allowable for `_additionalPayee{Primary,Secondaary}Sales`
               * and their associated percentages to be zero'd out by the controlling artist.
               */
              function adminAcceptArtistAddressesAndSplits(
                  uint256 _projectId,
                  address payable _artistAddress,
                  address payable _additionalPayeePrimarySales,
                  uint256 _additionalPayeePrimarySalesPercentage,
                  address payable _additionalPayeeSecondarySales,
                  uint256 _additionalPayeeSecondarySalesPercentage
              )
                  external
                  onlyValidProjectId(_projectId)
                  onlyAdminACLOrRenouncedArtist(
                      _projectId,
                      this.adminAcceptArtistAddressesAndSplits.selector
                  )
                  onlyNonZeroAddress(_artistAddress)
              {
                  // checks
                  require(
                      proposedArtistAddressesAndSplitsHash[_projectId] ==
                          keccak256(
                              abi.encode(
                                  _artistAddress,
                                  _additionalPayeePrimarySales,
                                  _additionalPayeePrimarySalesPercentage,
                                  _additionalPayeeSecondarySales,
                                  _additionalPayeeSecondarySalesPercentage
                              )
                          ),
                      "Must match artist proposal"
                  );
                  // effects
                  ProjectFinance storage projectFinance = projectIdToFinancials[
                      _projectId
                  ];
                  projectFinance.artistAddress = _artistAddress;
                  projectFinance
                      .additionalPayeePrimarySales = _additionalPayeePrimarySales;
                  projectFinance.additionalPayeePrimarySalesPercentage = uint8(
                      _additionalPayeePrimarySalesPercentage
                  );
                  projectFinance
                      .additionalPayeeSecondarySales = _additionalPayeeSecondarySales;
                  projectFinance.additionalPayeeSecondarySalesPercentage = uint8(
                      _additionalPayeeSecondarySalesPercentage
                  );
                  // clear proposed values
                  proposedArtistAddressesAndSplitsHash[_projectId] = bytes32(0);
                  // emit event for off-chain indexing
                  emit AcceptedArtistAddressesAndSplits(_projectId);
              }
              /**
               * @notice Updates artist of project `_projectId` to `_artistAddress`.
               * This is to only be used in the event that the artist address is
               * compromised or sanctioned.
               * @param _projectId Project ID.
               * @param _artistAddress New artist address.
               */
              function updateProjectArtistAddress(
                  uint256 _projectId,
                  address payable _artistAddress
              )
                  external
                  onlyValidProjectId(_projectId)
                  onlyAdminACLOrRenouncedArtist(
                      _projectId,
                      this.updateProjectArtistAddress.selector
                  )
                  onlyNonZeroAddress(_artistAddress)
              {
                  projectIdToFinancials[_projectId].artistAddress = _artistAddress;
                  emit ProjectUpdated(_projectId, FIELD_PROJECT_ARTIST_ADDRESS);
              }
              /**
               * @notice Toggles paused state of project `_projectId`.
               * @param _projectId Project ID to be toggled.
               */
              function toggleProjectIsPaused(uint256 _projectId)
                  external
                  onlyArtist(_projectId)
              {
                  projects[_projectId].paused = !projects[_projectId].paused;
                  emit ProjectUpdated(_projectId, FIELD_PROJECT_PAUSED);
              }
              /**
               * @notice Adds new project `_projectName` by `_artistAddress`.
               * @param _projectName Project name.
               * @param _artistAddress Artist's address.
               * @dev token price now stored on minter
               */
              function addProject(
                  string memory _projectName,
                  address payable _artistAddress
              )
                  external
                  onlyAdminACL(this.addProject.selector)
                  onlyNonEmptyString(_projectName)
                  onlyNonZeroAddress(_artistAddress)
              {
                  require(!newProjectsForbidden, "New projects forbidden");
                  uint256 projectId = _nextProjectId;
                  projectIdToFinancials[projectId].artistAddress = _artistAddress;
                  projects[projectId].name = _projectName;
                  projects[projectId].paused = true;
                  projects[projectId].maxInvocations = ONE_MILLION_UINT24;
                  projects[projectId].projectBaseURI = defaultBaseURI;
                  _nextProjectId = uint248(projectId) + 1;
                  emit ProjectUpdated(projectId, FIELD_PROJECT_CREATED);
              }
              /**
               * @notice Forever forbids new projects from being added to this contract.
               */
              function forbidNewProjects()
                  external
                  onlyAdminACL(this.forbidNewProjects.selector)
              {
                  require(!newProjectsForbidden, "Already forbidden");
                  _forbidNewProjects();
              }
              /**
               * @notice Updates name of project `_projectId` to be `_projectName`.
               * @param _projectId Project ID.
               * @param _projectName New project name.
               */
              function updateProjectName(uint256 _projectId, string memory _projectName)
                  external
                  onlyUnlocked(_projectId)
                  onlyArtistOrAdminACL(_projectId, this.updateProjectName.selector)
                  onlyNonEmptyString(_projectName)
              {
                  projects[_projectId].name = _projectName;
                  emit ProjectUpdated(_projectId, FIELD_PROJECT_NAME);
              }
              /**
               * @notice Updates artist name for project `_projectId` to be
               * `_projectArtistName`.
               * @param _projectId Project ID.
               * @param _projectArtistName New artist name.
               */
              function updateProjectArtistName(
                  uint256 _projectId,
                  string memory _projectArtistName
              )
                  external
                  onlyUnlocked(_projectId)
                  onlyArtistOrAdminACL(_projectId, this.updateProjectArtistName.selector)
                  onlyNonEmptyString(_projectArtistName)
              {
                  projects[_projectId].artist = _projectArtistName;
                  emit ProjectUpdated(_projectId, FIELD_PROJECT_ARTIST_NAME);
              }
              /**
               * @notice Updates artist secondary market royalties for project
               * `_projectId` to be `_secondMarketRoyalty` percent.
               * This DOES NOT include the secondary market royalty percentages collected
               * by Art Blocks; this is only the total percentage of royalties that will
               * be split to artist and additionalSecondaryPayee.
               * @param _projectId Project ID.
               * @param _secondMarketRoyalty Percent of secondary sales revenue that will
               * be split to artist and additionalSecondaryPayee. This must be less than
               * or equal to ARTIST_MAX_SECONDARY_ROYALTY_PERCENTAGE percent.
               */
              function updateProjectSecondaryMarketRoyaltyPercentage(
                  uint256 _projectId,
                  uint256 _secondMarketRoyalty
              ) external onlyArtist(_projectId) {
                  require(
                      _secondMarketRoyalty <= ARTIST_MAX_SECONDARY_ROYALTY_PERCENTAGE,
                      "Max of ARTIST_MAX_SECONDARY_ROYALTY_PERCENTAGE percent"
                  );
                  projectIdToFinancials[_projectId]
                      .secondaryMarketRoyaltyPercentage = uint8(_secondMarketRoyalty);
                  emit ProjectUpdated(
                      _projectId,
                      FIELD_PROJECT_SECONDARY_MARKET_ROYALTY_PERCENTAGE
                  );
              }
              /**
               * @notice Updates description of project `_projectId`.
               * Only artist may call when unlocked, only admin may call when locked.
               * @param _projectId Project ID.
               * @param _projectDescription New project description.
               */
              function updateProjectDescription(
                  uint256 _projectId,
                  string memory _projectDescription
              ) external {
                  // checks
                  require(
                      _projectUnlocked(_projectId)
                          ? msg.sender == projectIdToFinancials[_projectId].artistAddress
                          : adminACLAllowed(
                              msg.sender,
                              address(this),
                              this.updateProjectDescription.selector
                          ),
                      "Only artist when unlocked, owner when locked"
                  );
                  // effects
                  projects[_projectId].description = _projectDescription;
                  emit ProjectUpdated(_projectId, FIELD_PROJECT_DESCRIPTION);
              }
              /**
               * @notice Updates website of project `_projectId` to be `_projectWebsite`.
               * @param _projectId Project ID.
               * @param _projectWebsite New project website.
               * @dev It is intentionally allowed for this to be set to the empty string.
               */
              function updateProjectWebsite(
                  uint256 _projectId,
                  string memory _projectWebsite
              ) external onlyArtist(_projectId) {
                  projects[_projectId].website = _projectWebsite;
                  emit ProjectUpdated(_projectId, FIELD_PROJECT_WEBSITE);
              }
              /**
               * @notice Updates license for project `_projectId`.
               * @param _projectId Project ID.
               * @param _projectLicense New project license.
               */
              function updateProjectLicense(
                  uint256 _projectId,
                  string memory _projectLicense
              )
                  external
                  onlyUnlocked(_projectId)
                  onlyArtistOrAdminACL(_projectId, this.updateProjectLicense.selector)
                  onlyNonEmptyString(_projectLicense)
              {
                  projects[_projectId].license = _projectLicense;
                  emit ProjectUpdated(_projectId, FIELD_PROJECT_LICENSE);
              }
              /**
               * @notice Updates maximum invocations for project `_projectId` to
               * `_maxInvocations`. Maximum invocations may only be decreased by the
               * artist, and must be greater than or equal to current invocations.
               * New projects are created with maximum invocations of 1 million by
               * default.
               * @param _projectId Project ID.
               * @param _maxInvocations New maximum invocations.
               */
              function updateProjectMaxInvocations(
                  uint256 _projectId,
                  uint24 _maxInvocations
              ) external onlyArtist(_projectId) {
                  // CHECKS
                  Project storage project = projects[_projectId];
                  uint256 _invocations = project.invocations;
                  require(
                      (_maxInvocations < project.maxInvocations),
                      "maxInvocations may only be decreased"
                  );
                  require(
                      _maxInvocations >= _invocations,
                      "Only max invocations gte current invocations"
                  );
                  // EFFECTS
                  project.maxInvocations = _maxInvocations;
                  emit ProjectUpdated(_projectId, FIELD_PROJECT_MAX_INVOCATIONS);
                  // register completed timestamp if action completed the project
                  if (_maxInvocations == _invocations) {
                      _completeProject(_projectId);
                  }
              }
              /**
               * @notice Adds a script to project `_projectId`.
               * @param _projectId Project to be updated.
               * @param _script Script to be added.
               */
              function addProjectScript(uint256 _projectId, string memory _script)
                  external
                  onlyUnlocked(_projectId)
                  onlyArtistOrAdminACL(_projectId, this.addProjectScript.selector)
                  onlyNonEmptyString(_script)
              {
                  Project storage project = projects[_projectId];
                  // store script in contract bytecode
                  project.scriptBytecodeAddresses[project.scriptCount] = _script
                      .writeToBytecode();
                  project.scriptCount = project.scriptCount + 1;
                  emit ProjectUpdated(_projectId, FIELD_PROJECT_SCRIPT);
              }
              /**
               * @notice Updates script for project `_projectId` at script ID `_scriptId`.
               * @param _projectId Project to be updated.
               * @param _scriptId Script ID to be updated.
               * @param _script The updated script value.
               */
              function updateProjectScript(
                  uint256 _projectId,
                  uint256 _scriptId,
                  string memory _script
              )
                  external
                  onlyUnlocked(_projectId)
                  onlyArtistOrAdminACL(_projectId, this.updateProjectScript.selector)
                  onlyNonEmptyString(_script)
              {
                  Project storage project = projects[_projectId];
                  require(_scriptId < project.scriptCount, "scriptId out of range");
                  // purge old contract bytecode contract from the blockchain state
                  project.scriptBytecodeAddresses[_scriptId].purgeBytecode();
                  // store script in contract bytecode, replacing reference address from
                  // the contract that no longer exists with the newly created one
                  project.scriptBytecodeAddresses[_scriptId] = _script.writeToBytecode();
                  emit ProjectUpdated(_projectId, FIELD_PROJECT_SCRIPT);
              }
              /**
               * @notice Removes last script from project `_projectId`.
               * @param _projectId Project to be updated.
               */
              function removeProjectLastScript(uint256 _projectId)
                  external
                  onlyUnlocked(_projectId)
                  onlyArtistOrAdminACL(_projectId, this.removeProjectLastScript.selector)
              {
                  Project storage project = projects[_projectId];
                  require(project.scriptCount > 0, "there are no scripts to remove");
                  // purge old contract bytecode contract from the blockchain state
                  project.scriptBytecodeAddresses[project.scriptCount - 1].purgeBytecode();
                  // delete reference to contract address that no longer exists
                  delete project.scriptBytecodeAddresses[project.scriptCount - 1];
                  unchecked {
                      project.scriptCount = project.scriptCount - 1;
                  }
                  emit ProjectUpdated(_projectId, FIELD_PROJECT_SCRIPT);
              }
              /**
               * @notice Updates script type for project `_projectId`.
               * @param _projectId Project to be updated.
               * @param _scriptTypeAndVersion Script type and version e.g. "[email protected]",
               * as bytes32 encoded string.
               */
              function updateProjectScriptType(
                  uint256 _projectId,
                  bytes32 _scriptTypeAndVersion
              )
                  external
                  onlyUnlocked(_projectId)
                  onlyArtistOrAdminACL(_projectId, this.updateProjectScriptType.selector)
              {
                  Project storage project = projects[_projectId];
                  // require exactly one @ symbol in _scriptTypeAndVersion
                  require(
                      _scriptTypeAndVersion.containsExactCharacterQty(
                          AT_CHARACTER_CODE,
                          uint8(1)
                      ),
                      "must contain exactly one @"
                  );
                  project.scriptTypeAndVersion = _scriptTypeAndVersion;
                  emit ProjectUpdated(_projectId, FIELD_PROJECT_SCRIPT_TYPE);
              }
              /**
               * @notice Updates project's aspect ratio.
               * @param _projectId Project to be updated.
               * @param _aspectRatio Aspect ratio to be set. Intended to be string in the
               * format of a decimal, e.g. "1" for square, "1.77777778" for 16:9, etc.,
               * allowing for a maximum of 10 digits and one (optional) decimal separator.
               */
              function updateProjectAspectRatio(
                  uint256 _projectId,
                  string memory _aspectRatio
              )
                  external
                  onlyUnlocked(_projectId)
                  onlyArtistOrAdminACL(_projectId, this.updateProjectAspectRatio.selector)
                  onlyNonEmptyString(_aspectRatio)
              {
                  // Perform more detailed input validation for aspect ratio.
                  bytes memory aspectRatioBytes = bytes(_aspectRatio);
                  uint256 bytesLength = aspectRatioBytes.length;
                  require(bytesLength <= 11, "Aspect ratio format too long");
                  bool hasSeenDecimalSeparator = false;
                  bool hasSeenNumber = false;
                  for (uint256 i; i < bytesLength; i++) {
                      bytes1 character = aspectRatioBytes[i];
                      // Allow as many #s as desired.
                      if (character >= 0x30 && character <= 0x39) {
                          // 9-0
                          // We need to ensure there is at least 1 `9-0` occurrence.
                          hasSeenNumber = true;
                          continue;
                      }
                      if (character == 0x2E) {
                          // .
                          // Allow no more than 1 `.` occurrence.
                          if (!hasSeenDecimalSeparator) {
                              hasSeenDecimalSeparator = true;
                              continue;
                          }
                      }
                      revert("Improperly formatted aspect ratio");
                  }
                  require(hasSeenNumber, "Aspect ratio has no numbers");
                  projects[_projectId].aspectRatio = _aspectRatio;
                  emit ProjectUpdated(_projectId, FIELD_PROJECT_ASPECT_RATIO);
              }
              /**
               * @notice Updates base URI for project `_projectId` to `_newBaseURI`.
               * This is the controlling base URI for all tokens in the project. The
               * contract-level defaultBaseURI is only used when initializing new
               * projects.
               * @param _projectId Project to be updated.
               * @param _newBaseURI New base URI.
               */
              function updateProjectBaseURI(uint256 _projectId, string memory _newBaseURI)
                  external
                  onlyArtist(_projectId)
                  onlyNonEmptyString(_newBaseURI)
              {
                  projects[_projectId].projectBaseURI = _newBaseURI;
                  emit ProjectUpdated(_projectId, FIELD_PROJECT_BASE_URI);
              }
              /**
               * @notice Updates default base URI to `_defaultBaseURI`. The
               * contract-level defaultBaseURI is only used when initializing new
               * projects. Token URIs are determined by their project's `projectBaseURI`.
               * @param _defaultBaseURI New default base URI.
               */
              function updateDefaultBaseURI(string memory _defaultBaseURI)
                  external
                  onlyAdminACL(this.updateDefaultBaseURI.selector)
                  onlyNonEmptyString(_defaultBaseURI)
              {
                  _updateDefaultBaseURI(_defaultBaseURI);
              }
              /**
               * @notice Next project ID to be created on this contract.
               * @return uint256 Next project ID.
               */
              function nextProjectId() external view returns (uint256) {
                  return _nextProjectId;
              }
              /**
               * @notice Returns token hash for token ID `_tokenId`. Returns null if hash
               * has not been set.
               * @param _tokenId Token ID to be queried.
               * @return bytes32 Token hash.
               * @dev token hash is the keccak256 hash of the stored hash seed
               */
              function tokenIdToHash(uint256 _tokenId) external view returns (bytes32) {
                  bytes12 _hashSeed = _ownersAndHashSeeds[_tokenId].hashSeed;
                  if (_hashSeed == 0) {
                      return 0;
                  }
                  return keccak256(abi.encode(_hashSeed));
              }
              /**
               * @notice View function returning Art Blocks portion of primary sales, in
               * percent.
               * @return uint256 Art Blocks portion of primary sales, in percent.
               */
              function artblocksPrimarySalesPercentage() external view returns (uint256) {
                  return _artblocksPrimarySalesPercentage;
              }
              /**
               * @notice View function returning Artist's address for project
               * `_projectId`.
               * @param _projectId Project ID to be queried.
               * @return address Artist's address.
               */
              function projectIdToArtistAddress(uint256 _projectId)
                  external
                  view
                  returns (address payable)
              {
                  return projectIdToFinancials[_projectId].artistAddress;
              }
              /**
               * @notice View function returning Artist's secondary market royalty
               * percentage for project `_projectId`.
               * This does not include Art Blocks portion of secondary market royalties.
               * @param _projectId Project ID to be queried.
               * @return uint256 Artist's secondary market royalty percentage.
               */
              function projectIdToSecondaryMarketRoyaltyPercentage(uint256 _projectId)
                  external
                  view
                  returns (uint256)
              {
                  return
                      projectIdToFinancials[_projectId].secondaryMarketRoyaltyPercentage;
              }
              /**
               * @notice View function returning Artist's additional payee address for
               * primary sales, for project `_projectId`.
               * @param _projectId Project ID to be queried.
               * @return address Artist's additional payee address for primary sales.
               */
              function projectIdToAdditionalPayeePrimarySales(uint256 _projectId)
                  external
                  view
                  returns (address payable)
              {
                  return projectIdToFinancials[_projectId].additionalPayeePrimarySales;
              }
              /**
               * @notice View function returning Artist's additional payee primary sales
               * percentage, for project `_projectId`.
               * @param _projectId Project ID to be queried.
               * @return uint256 Artist's additional payee primary sales percentage.
               */
              function projectIdToAdditionalPayeePrimarySalesPercentage(
                  uint256 _projectId
              ) external view returns (uint256) {
                  return
                      projectIdToFinancials[_projectId]
                          .additionalPayeePrimarySalesPercentage;
              }
              /**
               * @notice View function returning Artist's additional payee address for
               * secondary sales, for project `_projectId`.
               * @param _projectId Project ID to be queried.
               * @return address payable Artist's additional payee address for secondary
               * sales.
               */
              function projectIdToAdditionalPayeeSecondarySales(uint256 _projectId)
                  external
                  view
                  returns (address payable)
              {
                  return projectIdToFinancials[_projectId].additionalPayeeSecondarySales;
              }
              /**
               * @notice View function returning Artist's additional payee secondary
               * sales percentage, for project `_projectId`.
               * @param _projectId Project ID to be queried.
               * @return uint256 Artist's additional payee secondary sales percentage.
               */
              function projectIdToAdditionalPayeeSecondarySalesPercentage(
                  uint256 _projectId
              ) external view returns (uint256) {
                  return
                      projectIdToFinancials[_projectId]
                          .additionalPayeeSecondarySalesPercentage;
              }
              /**
               * @notice Returns project details for project `_projectId`.
               * @param _projectId Project to be queried.
               * @return projectName Name of project
               * @return artist Artist of project
               * @return description Project description
               * @return website Project website
               * @return license Project license
               * @dev this function was named projectDetails prior to V3 core contract.
               */
              function projectDetails(uint256 _projectId)
                  external
                  view
                  returns (
                      string memory projectName,
                      string memory artist,
                      string memory description,
                      string memory website,
                      string memory license
                  )
              {
                  Project storage project = projects[_projectId];
                  projectName = project.name;
                  artist = project.artist;
                  description = project.description;
                  website = project.website;
                  license = project.license;
              }
              /**
               * @notice Returns project state data for project `_projectId`.
               * @param _projectId Project to be queried
               * @return invocations Current number of invocations
               * @return maxInvocations Maximum allowed invocations
               * @return active Boolean representing if project is currently active
               * @return paused Boolean representing if project is paused
               * @return completedTimestamp zero if project not complete, otherwise
               * timestamp of project completion.
               * @return locked Boolean representing if project is locked
               * @dev price and currency info are located on minter contracts
               */
              function projectStateData(uint256 _projectId)
                  external
                  view
                  returns (
                      uint256 invocations,
                      uint256 maxInvocations,
                      bool active,
                      bool paused,
                      uint256 completedTimestamp,
                      bool locked
                  )
              {
                  Project storage project = projects[_projectId];
                  invocations = project.invocations;
                  maxInvocations = project.maxInvocations;
                  active = project.active;
                  paused = project.paused;
                  completedTimestamp = project.completedTimestamp;
                  locked = !_projectUnlocked(_projectId);
              }
              /**
               * @notice Returns artist payment information for project `_projectId`.
               * @param _projectId Project to be queried
               * @return artistAddress Project Artist's address
               * @return additionalPayeePrimarySales Additional payee address for primary
               * sales
               * @return additionalPayeePrimarySalesPercentage Percentage of artist revenue
               * to be sent to the additional payee address for primary sales
               * @return additionalPayeeSecondarySales Additional payee address for secondary
               * sales royalties
               * @return additionalPayeeSecondarySalesPercentage Percentage of artist revenue
               * to be sent to the additional payee address for secondary sales royalties
               * @return secondaryMarketRoyaltyPercentage Royalty percentage to be sent to
               * combination of artist and additional payee. This does not include the
               * platform's percentage of secondary sales royalties, which is defined by
               * `artblocksSecondarySalesBPS`.
               */
              function projectArtistPaymentInfo(uint256 _projectId)
                  external
                  view
                  returns (
                      address artistAddress,
                      address additionalPayeePrimarySales,
                      uint256 additionalPayeePrimarySalesPercentage,
                      address additionalPayeeSecondarySales,
                      uint256 additionalPayeeSecondarySalesPercentage,
                      uint256 secondaryMarketRoyaltyPercentage
                  )
              {
                  ProjectFinance storage projectFinance = projectIdToFinancials[
                      _projectId
                  ];
                  artistAddress = projectFinance.artistAddress;
                  additionalPayeePrimarySales = projectFinance
                      .additionalPayeePrimarySales;
                  additionalPayeePrimarySalesPercentage = projectFinance
                      .additionalPayeePrimarySalesPercentage;
                  additionalPayeeSecondarySales = projectFinance
                      .additionalPayeeSecondarySales;
                  additionalPayeeSecondarySalesPercentage = projectFinance
                      .additionalPayeeSecondarySalesPercentage;
                  secondaryMarketRoyaltyPercentage = projectFinance
                      .secondaryMarketRoyaltyPercentage;
              }
              /**
               * @notice Returns script information for project `_projectId`.
               * @param _projectId Project to be queried.
               * @return scriptTypeAndVersion Project's script type and version
               * (e.g. "p5js(atSymbol)1.0.0")
               * @return aspectRatio Aspect ratio of project (e.g. "1" for square,
               * "1.77777778" for 16:9, etc.)
               * @return scriptCount Count of scripts for project
               */
              function projectScriptDetails(uint256 _projectId)
                  external
                  view
                  returns (
                      string memory scriptTypeAndVersion,
                      string memory aspectRatio,
                      uint256 scriptCount
                  )
              {
                  Project storage project = projects[_projectId];
                  scriptTypeAndVersion = project.scriptTypeAndVersion.toString();
                  aspectRatio = project.aspectRatio;
                  scriptCount = project.scriptCount;
              }
              /**
               * @notice Returns address with bytecode containing project script for
               * project `_projectId` at script index `_index`.
               */
              function projectScriptBytecodeAddressByIndex(
                  uint256 _projectId,
                  uint256 _index
              ) external view returns (address) {
                  return projects[_projectId].scriptBytecodeAddresses[_index];
              }
              /**
               * @notice Returns script for project `_projectId` at script index `_index`.
               * @param _projectId Project to be queried.
               * @param _index Index of script to be queried.
               */
              function projectScriptByIndex(uint256 _projectId, uint256 _index)
                  external
                  view
                  returns (string memory)
              {
                  Project storage project = projects[_projectId];
                  // If trying to access an out-of-index script, return the empty string.
                  if (_index >= project.scriptCount) {
                      return "";
                  }
                  return project.scriptBytecodeAddresses[_index].readFromBytecode();
              }
              /**
               * @notice Returns base URI for project `_projectId`.
               * @param _projectId Project to be queried.
               * @return projectBaseURI Base URI for project
               */
              function projectURIInfo(uint256 _projectId)
                  external
                  view
                  returns (string memory projectBaseURI)
              {
                  projectBaseURI = projects[_projectId].projectBaseURI;
              }
              /**
               * @notice Backwards-compatible (pre-V3) function returning if `_minter` is
               * minterContract.
               * @param _minter Address to be queried.
               * @return bool Boolean representing if `_minter` is minterContract.
               */
              function isMintWhitelisted(address _minter) external view returns (bool) {
                  return (minterContract == _minter);
              }
              /**
               * @notice Gets qty of randomizers in history of all randomizers used by
               * this core contract. If a randomizer is switched away from then back to,
               * it will show up in the history twice.
               * @return randomizerHistoryCount Count of randomizers in history
               */
              function numHistoricalRandomizers() external view returns (uint256) {
                  return _historicalRandomizerAddresses.length;
              }
              /**
               * @notice Gets address of randomizer at index `_index` in history of all
               * randomizers used by this core contract. Index is zero-based.
               * @param _index Historical index of randomizer to be queried.
               * @return randomizerAddress Address of randomizer at index `_index`.
               * @dev If a randomizer is switched away from and then switched back to, it
               * will show up in the history twice.
               */
              function getHistoricalRandomizerAt(uint256 _index)
                  external
                  view
                  returns (address)
              {
                  require(
                      _index < _historicalRandomizerAddresses.length,
                      "Index out of bounds"
                  );
                  return _historicalRandomizerAddresses[_index];
              }
              /**
               * @notice Backwards-compatible (pre-V3) function returning Art Blocks
               * primary sales payment address (now called artblocksPrimarySalesAddress).
               * @return address payable Art Blocks primary sales payment address
               */
              function artblocksAddress() external view returns (address payable) {
                  return artblocksPrimarySalesAddress;
              }
              /**
               * @notice Backwards-compatible (pre-V3) function returning Art Blocks
               * primary sales percentage (now called artblocksPrimarySalesPercentage).
               * @return uint256 Art Blocks primary sales percentage
               */
              function artblocksPercentage() external view returns (uint256) {
                  return _artblocksPrimarySalesPercentage;
              }
              /**
               * @notice Backwards-compatible (pre-V3) function.
               * Gets artist + artist's additional payee royalty data for token ID
               `_tokenId`.
               * WARNING: Does not include Art Blocks portion of royalties.
               * @param _tokenId Token ID to be queried.
               * @return artistAddress Artist's payment address
               * @return additionalPayee Additional payee's payment address
               * @return additionalPayeePercentage Percentage of artist revenue
               * to be sent to the additional payee's address
               * @return royaltyFeeByID Total royalty percentage to be sent to
               * combination of artist and additional payee
               * @dev Does not include Art Blocks portion of royalties.
               */
              function getRoyaltyData(uint256 _tokenId)
                  external
                  view
                  returns (
                      address artistAddress,
                      address additionalPayee,
                      uint256 additionalPayeePercentage,
                      uint256 royaltyFeeByID
                  )
              {
                  uint256 projectId = tokenIdToProjectId(_tokenId);
                  ProjectFinance storage projectFinance = projectIdToFinancials[
                      projectId
                  ];
                  artistAddress = projectFinance.artistAddress;
                  additionalPayee = projectFinance.additionalPayeeSecondarySales;
                  additionalPayeePercentage = projectFinance
                      .additionalPayeeSecondarySalesPercentage;
                  royaltyFeeByID = projectFinance.secondaryMarketRoyaltyPercentage;
              }
              /**
               * @notice Gets royalty Basis Points (BPS) for token ID `_tokenId`.
               * This conforms to the IManifold interface designated in the Royalty
               * Registry's RoyaltyEngineV1.sol contract.
               * ref: https://github.com/manifoldxyz/royalty-registry-solidity
               * @param _tokenId Token ID to be queried.
               * @return recipients Array of royalty payment recipients
               * @return bps Array of Basis Points (BPS) allocated to each recipient,
               * aligned by index.
               * @dev reverts if invalid _tokenId
               * @dev only returns recipients that have a non-zero BPS allocation
               */
              function getRoyalties(uint256 _tokenId)
                  external
                  view
                  onlyValidTokenId(_tokenId)
                  returns (address payable[] memory recipients, uint256[] memory bps)
              {
                  // initialize arrays with maximum potential length
                  recipients = new address payable[](3);
                  bps = new uint256[](3);
                  uint256 projectId = tokenIdToProjectId(_tokenId);
                  ProjectFinance storage projectFinance = projectIdToFinancials[
                      projectId
                  ];
                  // load values into memory
                  uint256 royaltyPercentageForArtistAndAdditional = projectFinance
                      .secondaryMarketRoyaltyPercentage;
                  uint256 additionalPayeePercentage = projectFinance
                      .additionalPayeeSecondarySalesPercentage;
                  // calculate BPS = percentage * 100
                  uint256 artistBPS = (ONE_HUNDRED - additionalPayeePercentage) *
                      royaltyPercentageForArtistAndAdditional;
                  uint256 additionalBPS = additionalPayeePercentage *
                      royaltyPercentageForArtistAndAdditional;
                  uint256 artblocksBPS = artblocksSecondarySalesBPS;
                  // populate arrays
                  uint256 payeeCount;
                  if (artistBPS > 0) {
                      recipients[payeeCount] = projectFinance.artistAddress;
                      bps[payeeCount++] = artistBPS;
                  }
                  if (additionalBPS > 0) {
                      recipients[payeeCount] = projectFinance
                          .additionalPayeeSecondarySales;
                      bps[payeeCount++] = additionalBPS;
                  }
                  if (artblocksBPS > 0) {
                      recipients[payeeCount] = artblocksSecondarySalesAddress;
                      bps[payeeCount++] = artblocksBPS;
                  }
                  // trim arrays if necessary
                  if (3 > payeeCount) {
                      assembly {
                          let decrease := sub(3, payeeCount)
                          mstore(recipients, sub(mload(recipients), decrease))
                          mstore(bps, sub(mload(bps), decrease))
                      }
                  }
                  return (recipients, bps);
              }
              /**
               * @notice View function that returns appropriate revenue splits between
               * different Art Blocks, Artist, and Artist's additional primary sales
               * payee given a sale price of `_price` on project `_projectId`.
               * This always returns three revenue amounts and three addresses, but if a
               * revenue is zero for either Artist or additional payee, the corresponding
               * address returned will also be null (for gas optimization).
               * Does not account for refund if user overpays for a token (minter should
               * handle a refund of the difference, if appropriate).
               * Some minters may have alternative methods of splitting payments, in
               * which case they should implement their own payment splitting logic.
               * @param _projectId Project ID to be queried.
               * @param _price Sale price of token.
               * @return artblocksRevenue_ amount of revenue to be sent to Art Blocks
               * @return artblocksAddress_ address to send Art Blocks revenue to
               * @return artistRevenue_ amount of revenue to be sent to Artist
               * @return artistAddress_ address to send Artist revenue to. Will be null
               * if no revenue is due to artist (gas optimization).
               * @return additionalPayeePrimaryRevenue_ amount of revenue to be sent to
               * additional payee for primary sales
               * @return additionalPayeePrimaryAddress_ address to send Artist's
               * additional payee for primary sales revenue to. Will be null if no
               * revenue is due to additional payee for primary sales (gas optimization).
               * @dev this always returns three addresses and three revenues, but if the
               * revenue is zero, the corresponding address will be address(0). It is up
               * to the contract performing the revenue split to handle this
               * appropriately.
               */
              function getPrimaryRevenueSplits(uint256 _projectId, uint256 _price)
                  external
                  view
                  returns (
                      uint256 artblocksRevenue_,
                      address payable artblocksAddress_,
                      uint256 artistRevenue_,
                      address payable artistAddress_,
                      uint256 additionalPayeePrimaryRevenue_,
                      address payable additionalPayeePrimaryAddress_
                  )
              {
                  ProjectFinance storage projectFinance = projectIdToFinancials[
                      _projectId
                  ];
                  // calculate revenues
                  artblocksRevenue_ =
                      (_price * uint256(_artblocksPrimarySalesPercentage)) /
                      ONE_HUNDRED;
                  uint256 projectFunds;
                  unchecked {
                      // artblocksRevenue_ is always <=25, so guaranteed to never underflow
                      projectFunds = _price - artblocksRevenue_;
                  }
                  additionalPayeePrimaryRevenue_ =
                      (projectFunds *
                          projectFinance.additionalPayeePrimarySalesPercentage) /
                      ONE_HUNDRED;
                  unchecked {
                      // projectIdToAdditionalPayeePrimarySalesPercentage is always
                      // <=100, so guaranteed to never underflow
                      artistRevenue_ = projectFunds - additionalPayeePrimaryRevenue_;
                  }
                  // set addresses from storage
                  artblocksAddress_ = artblocksPrimarySalesAddress;
                  if (artistRevenue_ > 0) {
                      artistAddress_ = projectFinance.artistAddress;
                  }
                  if (additionalPayeePrimaryRevenue_ > 0) {
                      additionalPayeePrimaryAddress_ = projectFinance
                          .additionalPayeePrimarySales;
                  }
              }
              /**
               * @notice Backwards-compatible (pre-V3) getter returning contract admin
               * @return address Address of contract admin (same as owner)
               */
              function admin() external view returns (address) {
                  return owner();
              }
              /**
               * @notice Gets the project ID for a given `_tokenId`.
               * @param _tokenId Token ID to be queried.
               * @return _projectId Project ID for given `_tokenId`.
               */
              function tokenIdToProjectId(uint256 _tokenId)
                  public
                  pure
                  returns (uint256 _projectId)
              {
                  return _tokenId / ONE_MILLION;
              }
              /**
               * @notice Convenience function that returns whether `_sender` is allowed
               * to call function with selector `_selector` on contract `_contract`, as
               * determined by this contract's current Admin ACL contract. Expected use
               * cases include minter contracts checking if caller is allowed to call
               * admin-gated functions on minter contracts.
               * @param _sender Address of the sender calling function with selector
               * `_selector` on contract `_contract`.
               * @param _contract Address of the contract being called by `_sender`.
               * @param _selector Function selector of the function being called by
               * `_sender`.
               * @return bool Whether `_sender` is allowed to call function with selector
               * `_selector` on contract `_contract`.
               * @dev assumes the Admin ACL contract is the owner of this contract, which
               * is expected to always be true.
               * @dev adminACLContract is expected to either be null address (if owner
               * has renounced ownership), or conform to IAdminACLV0 interface. Check for
               * null address first to avoid revert when admin has renounced ownership.
               */
              function adminACLAllowed(
                  address _sender,
                  address _contract,
                  bytes4 _selector
              ) public returns (bool) {
                  return
                      owner() != address(0) &&
                      adminACLContract.allowed(_sender, _contract, _selector);
              }
              /**
               * @notice Returns contract owner. Set to deployer's address by default on
               * contract deployment.
               * @return address Address of contract owner.
               * @dev ref: https://docs.openzeppelin.com/contracts/4.x/api/access#Ownable
               * @dev owner role was called `admin` prior to V3 core contract
               */
              function owner()
                  public
                  view
                  override(Ownable, IGenArt721CoreContractV3)
                  returns (address)
              {
                  return Ownable.owner();
              }
              /**
               * @notice Gets token URI for token ID `_tokenId`.
               * @param _tokenId Token ID to be queried.
               * @return string URI of token ID `_tokenId`.
               * @dev token URIs are the concatenation of the project base URI and the
               * token ID.
               */
              function tokenURI(uint256 _tokenId)
                  public
                  view
                  override
                  onlyValidTokenId(_tokenId)
                  returns (string memory)
              {
                  string memory _projectBaseURI = projects[tokenIdToProjectId(_tokenId)]
                      .projectBaseURI;
                  return string.concat(_projectBaseURI, _tokenId.toString());
              }
              /**
               * @dev See {IERC165-supportsInterface}.
               */
              function supportsInterface(bytes4 interfaceId)
                  public
                  view
                  virtual
                  override
                  returns (bool)
              {
                  return
                      interfaceId == type(IManifold).interfaceId ||
                      super.supportsInterface(interfaceId);
              }
              /**
               * @notice Forbids new projects from being created
               * @dev only performs operation and emits event if contract is not already
               * forbidding new projects.
               */
              function _forbidNewProjects() internal {
                  if (!newProjectsForbidden) {
                      newProjectsForbidden = true;
                      emit PlatformUpdated(FIELD_NEW_PROJECTS_FORBIDDEN);
                  }
              }
              /**
               * @notice Transfers ownership of the contract to a new account (`newOwner`).
               * Internal function without access restriction.
               * @param newOwner New owner.
               * @dev owner role was called `admin` prior to V3 core contract.
               * @dev Overrides and wraps OpenZeppelin's _transferOwnership function to
               * also update adminACLContract for improved introspection.
               */
              function _transferOwnership(address newOwner) internal override {
                  Ownable._transferOwnership(newOwner);
                  adminACLContract = IAdminACLV0(newOwner);
              }
              /**
               * @notice Updates Art Blocks payment address to `_artblocksPrimarySalesAddress`.
               * @param _artblocksPrimarySalesAddress New Art Blocks payment address.
               * @dev Note that this method does not check that the input address is
               * not `address(0)`, as it is expected that callers of this method should
               * perform input validation where applicable.
               */
              function _updateArtblocksPrimarySalesAddress(
                  address _artblocksPrimarySalesAddress
              ) internal {
                  artblocksPrimarySalesAddress = payable(_artblocksPrimarySalesAddress);
                  emit PlatformUpdated(FIELD_ARTBLOCKS_PRIMARY_SALES_ADDRESS);
              }
              /**
               * @notice Updates Art Blocks secondary sales royalty payment address to
               * `_artblocksSecondarySalesAddress`.
               * @param _artblocksSecondarySalesAddress New Art Blocks secondary sales
               * payment address.
               * @dev Note that this method does not check that the input address is
               * not `address(0)`, as it is expected that callers of this method should
               * perform input validation where applicable.
               */
              function _updateArtblocksSecondarySalesAddress(
                  address _artblocksSecondarySalesAddress
              ) internal {
                  artblocksSecondarySalesAddress = payable(
                      _artblocksSecondarySalesAddress
                  );
                  emit PlatformUpdated(FIELD_ARTBLOCKS_SECONDARY_SALES_ADDRESS);
              }
              /**
               * @notice Updates randomizer address to `_randomizerAddress`.
               * @param _randomizerAddress New randomizer address.
               * @dev Note that this method does not check that the input address is
               * not `address(0)`, as it is expected that callers of this method should
               * perform input validation where applicable.
               */
              function _updateRandomizerAddress(address _randomizerAddress) internal {
                  randomizerContract = IRandomizerV2(_randomizerAddress);
                  // populate historical randomizer array
                  _historicalRandomizerAddresses.push(_randomizerAddress);
                  emit PlatformUpdated(FIELD_RANDOMIZER_ADDRESS);
              }
              /**
               * @notice Updates default base URI to `_defaultBaseURI`.
               * When new projects are added, their `projectBaseURI` is automatically
               * initialized to `_defaultBaseURI`.
               * @param _defaultBaseURI New default base URI.
               * @dev Note that this method does not check that the input string is not
               * the empty string, as it is expected that callers of this method should
               * perform input validation where applicable.
               */
              function _updateDefaultBaseURI(string memory _defaultBaseURI) internal {
                  defaultBaseURI = _defaultBaseURI;
                  emit PlatformUpdated(FIELD_DEFAULT_BASE_URI);
              }
              /**
               * @notice Internal function to complete a project.
               * @param _projectId Project ID to be completed.
               */
              function _completeProject(uint256 _projectId) internal {
                  projects[_projectId].completedTimestamp = uint64(block.timestamp);
                  emit ProjectUpdated(_projectId, FIELD_PROJECT_COMPLETED);
              }
              /**
               * @notice Internal function that returns whether a project is unlocked.
               * Projects automatically lock four weeks after they are completed.
               * Projects are considered completed when they have been invoked the
               * maximum number of times.
               * @param _projectId Project ID to be queried.
               * @return bool true if project is unlocked, false otherwise.
               * @dev This also enforces that the `_projectId` passed in is valid.
               */
              function _projectUnlocked(uint256 _projectId)
                  internal
                  view
                  onlyValidProjectId(_projectId)
                  returns (bool)
              {
                  uint256 projectCompletedTimestamp = projects[_projectId]
                      .completedTimestamp;
                  bool projectOpen = projectCompletedTimestamp == 0;
                  return
                      projectOpen ||
                      (block.timestamp - projectCompletedTimestamp <
                          FOUR_WEEKS_IN_SECONDS);
              }
          }
          // SPDX-License-Identifier: LGPL-3.0-only
          // Creatd By: Art Blocks Inc.
          pragma solidity ^0.8.0;
          import "./IGenArt721CoreContractV3.sol";
          interface IRandomizerV2 {
              // The core contract that may interact with this randomizer contract.
              function genArt721Core() external view returns (IGenArt721CoreContractV3);
              // When a core contract calls this, it can be assured that the randomizer
              // will set a bytes32 hash for tokenId `_tokenId` on the core contract.
              function assignTokenHash(uint256 _tokenId) external;
          }
          // SPDX-License-Identifier: LGPL-3.0-only
          // Created By: Art Blocks Inc.
          pragma solidity ^0.8.0;
          interface IAdminACLV0 {
              /**
               * @notice Token ID `_tokenId` minted to `_to`.
               * @param previousSuperAdmin The previous superAdmin address.
               * @param newSuperAdmin The new superAdmin address.
               * @param genArt721CoreAddressesToUpdate Array of genArt721Core
               * addresses to update to the new superAdmin, for indexing purposes only.
               */
              event SuperAdminTransferred(
                  address indexed previousSuperAdmin,
                  address indexed newSuperAdmin,
                  address[] genArt721CoreAddressesToUpdate
              );
              /// Type of the Admin ACL contract, e.g. "AdminACLV0"
              function AdminACLType() external view returns (string memory);
              /// super admin address
              function superAdmin() external view returns (address);
              /**
               * @notice Calls transferOwnership on other contract from this contract.
               * This is useful for updating to a new AdminACL contract.
               * @dev this function should be gated to only superAdmin-like addresses.
               */
              function transferOwnershipOn(address _contract, address _newAdminACL)
                  external;
              /**
               * @notice Calls renounceOwnership on other contract from this contract.
               * @dev this function should be gated to only superAdmin-like addresses.
               */
              function renounceOwnershipOn(address _contract) external;
              /**
               * @notice Checks if sender `_sender` is allowed to call function with selector
               * `_selector` on contract `_contract`.
               */
              function allowed(
                  address _sender,
                  address _contract,
                  bytes4 _selector
              ) external returns (bool);
          }
          // SPDX-License-Identifier: LGPL-3.0-only
          // Created By: Art Blocks Inc.
          pragma solidity ^0.8.0;
          import "./IAdminACLV0.sol";
          /// use the Royalty Registry's IManifold interface for token royalties
          import "./IManifold.sol";
          interface IGenArt721CoreContractV3 is IManifold {
              /**
               * @notice Token ID `_tokenId` minted to `_to`.
               */
              event Mint(address indexed _to, uint256 indexed _tokenId);
              /**
               * @notice currentMinter updated to `_currentMinter`.
               * @dev Implemented starting with V3 core
               */
              event MinterUpdated(address indexed _currentMinter);
              /**
               * @notice Platform updated on bytes32-encoded field `_field`.
               */
              event PlatformUpdated(bytes32 indexed _field);
              /**
               * @notice Project ID `_projectId` updated on bytes32-encoded field
               * `_update`.
               */
              event ProjectUpdated(uint256 indexed _projectId, bytes32 indexed _update);
              event ProposedArtistAddressesAndSplits(
                  uint256 indexed _projectId,
                  address _artistAddress,
                  address _additionalPayeePrimarySales,
                  uint256 _additionalPayeePrimarySalesPercentage,
                  address _additionalPayeeSecondarySales,
                  uint256 _additionalPayeeSecondarySalesPercentage
              );
              event AcceptedArtistAddressesAndSplits(uint256 indexed _projectId);
              // version and type of the core contract
              // coreVersion is a string of the form "0.x.y"
              function coreVersion() external view returns (string memory);
              // coreType is a string of the form "GenArt721CoreV3"
              function coreType() external view returns (string memory);
              // owner (pre-V3 was named admin) of contract
              // this is expected to be an Admin ACL contract for V3
              function owner() external view returns (address);
              // Admin ACL contract for V3, will be at the address owner()
              function adminACLContract() external returns (IAdminACLV0);
              // backwards-compatible (pre-V3) admin - equal to owner()
              function admin() external view returns (address);
              /**
               * Function determining if _sender is allowed to call function with
               * selector _selector on contract `_contract`. Intended to be used with
               * peripheral contracts such as minters, as well as internally by the
               * core contract itself.
               */
              function adminACLAllowed(
                  address _sender,
                  address _contract,
                  bytes4 _selector
              ) external returns (bool);
              // getter function of public variable
              function nextProjectId() external view returns (uint256);
              // getter function of public mapping
              function tokenIdToProjectId(uint256 tokenId)
                  external
                  view
                  returns (uint256 projectId);
              // @dev this is not available in V0
              function isMintWhitelisted(address minter) external view returns (bool);
              function projectIdToArtistAddress(uint256 _projectId)
                  external
                  view
                  returns (address payable);
              function projectIdToAdditionalPayeePrimarySales(uint256 _projectId)
                  external
                  view
                  returns (address payable);
              function projectIdToAdditionalPayeePrimarySalesPercentage(
                  uint256 _projectId
              ) external view returns (uint256);
              // @dev new function in V3
              function getPrimaryRevenueSplits(uint256 _projectId, uint256 _price)
                  external
                  view
                  returns (
                      uint256 artblocksRevenue_,
                      address payable artblocksAddress_,
                      uint256 artistRevenue_,
                      address payable artistAddress_,
                      uint256 additionalPayeePrimaryRevenue_,
                      address payable additionalPayeePrimaryAddress_
                  );
              // @dev new function in V3
              function projectStateData(uint256 _projectId)
                  external
                  view
                  returns (
                      uint256 invocations,
                      uint256 maxInvocations,
                      bool active,
                      bool paused,
                      uint256 completedTimestamp,
                      bool locked
                  );
              // @dev Art Blocks primary sales payment address
              function artblocksPrimarySalesAddress()
                  external
                  view
                  returns (address payable);
              /**
               * @notice Backwards-compatible (pre-V3) function returning Art Blocks
               * primary sales payment address (now called artblocksPrimarySalesAddress).
               */
              function artblocksAddress() external view returns (address payable);
              // @dev Percentage of primary sales allocated to Art Blocks
              function artblocksPrimarySalesPercentage() external view returns (uint256);
              /**
               * @notice Backwards-compatible (pre-V3) function returning Art Blocks
               * primary sales percentage (now called artblocksPrimarySalesPercentage).
               */
              function artblocksPercentage() external view returns (uint256);
              // @dev Art Blocks secondary sales royalties payment address
              function artblocksSecondarySalesAddress()
                  external
                  view
                  returns (address payable);
              // @dev Basis points of secondary sales allocated to Art Blocks
              function artblocksSecondarySalesBPS() external view returns (uint256);
              // function to set a token's hash (must be guarded)
              function setTokenHash_8PT(uint256 _tokenId, bytes32 _hash) external;
              // @dev gas-optimized signature in V3 for `mint`
              function mint_Ecf(
                  address _to,
                  uint256 _projectId,
                  address _by
              ) external returns (uint256 tokenId);
              /**
               * @notice Backwards-compatible (pre-V3) function  that gets artist +
               * artist's additional payee royalty data for token ID `_tokenId`.
               * WARNING: Does not include Art Blocks portion of royalties.
               */
              function getRoyaltyData(uint256 _tokenId)
                  external
                  view
                  returns (
                      address artistAddress,
                      address additionalPayee,
                      uint256 additionalPayeePercentage,
                      uint256 royaltyFeeByID
                  );
          }
          // SPDX-License-Identifier: MIT
          pragma solidity ^0.8.0;
          /// @dev Royalty Registry interface, used to support the Royalty Registry.
          /// @dev Source: https://github.com/manifoldxyz/royalty-registry-solidity/blob/main/contracts/specs/IManifold.sol
          /// @author: manifold.xyz
          /**
           * @dev Royalty interface for creator core classes
           */
          interface IManifold {
              /**
               * @dev Get royalites of a token.  Returns list of receivers and basisPoints
               *
               *  bytes4(keccak256('getRoyalties(uint256)')) == 0xbb3bafd6
               *
               *  => 0xbb3bafd6 = 0xbb3bafd6
               */
              function getRoyalties(uint256 tokenId)
                  external
                  view
                  returns (address payable[] memory, uint256[] memory);
          }
          // SPDX-License-Identifier: MIT
          // OpenZeppelin Contracts (last updated v4.7.0) (utils/Strings.sol)
          pragma solidity ^0.8.0;
          /**
           * @dev String operations.
           */
          library Strings {
              bytes16 private constant _HEX_SYMBOLS = "0123456789abcdef";
              uint8 private constant _ADDRESS_LENGTH = 20;
              /**
               * @dev Converts a `uint256` to its ASCII `string` decimal representation.
               */
              function toString(uint256 value) internal pure returns (string memory) {
                  // Inspired by OraclizeAPI's implementation - MIT licence
                  // https://github.com/oraclize/ethereum-api/blob/b42146b063c7d6ee1358846c198246239e9360e8/oraclizeAPI_0.4.25.sol
                  if (value == 0) {
                      return "0";
                  }
                  uint256 temp = value;
                  uint256 digits;
                  while (temp != 0) {
                      digits++;
                      temp /= 10;
                  }
                  bytes memory buffer = new bytes(digits);
                  while (value != 0) {
                      digits -= 1;
                      buffer[digits] = bytes1(uint8(48 + uint256(value % 10)));
                      value /= 10;
                  }
                  return string(buffer);
              }
              /**
               * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation.
               */
              function toHexString(uint256 value) internal pure returns (string memory) {
                  if (value == 0) {
                      return "0x00";
                  }
                  uint256 temp = value;
                  uint256 length = 0;
                  while (temp != 0) {
                      length++;
                      temp >>= 8;
                  }
                  return toHexString(value, length);
              }
              /**
               * @dev Converts a `uint256` to its ASCII `string` hexadecimal representation with fixed length.
               */
              function toHexString(uint256 value, uint256 length) internal pure returns (string memory) {
                  bytes memory buffer = new bytes(2 * length + 2);
                  buffer[0] = "0";
                  buffer[1] = "x";
                  for (uint256 i = 2 * length + 1; i > 1; --i) {
                      buffer[i] = _HEX_SYMBOLS[value & 0xf];
                      value >>= 4;
                  }
                  require(value == 0, "Strings: hex length insufficient");
                  return string(buffer);
              }
              /**
               * @dev Converts an `address` with fixed length of 20 bytes to its not checksummed ASCII `string` hexadecimal representation.
               */
              function toHexString(address addr) internal pure returns (string memory) {
                  return toHexString(uint256(uint160(addr)), _ADDRESS_LENGTH);
              }
          }
          // SPDX-License-Identifier: MIT
          // OpenZeppelin Contracts (last updated v4.7.0) (access/Ownable.sol)
          pragma solidity ^0.8.0;
          import "../utils/Context.sol";
          /**
           * @dev Contract module which provides a basic access control mechanism, where
           * there is an account (an owner) that can be granted exclusive access to
           * specific functions.
           *
           * By default, the owner account will be the one that deploys the contract. This
           * can later be changed with {transferOwnership}.
           *
           * This module is used through inheritance. It will make available the modifier
           * `onlyOwner`, which can be applied to your functions to restrict their use to
           * the owner.
           */
          abstract contract Ownable is Context {
              address private _owner;
              event OwnershipTransferred(address indexed previousOwner, address indexed newOwner);
              /**
               * @dev Initializes the contract setting the deployer as the initial owner.
               */
              constructor() {
                  _transferOwnership(_msgSender());
              }
              /**
               * @dev Throws if called by any account other than the owner.
               */
              modifier onlyOwner() {
                  _checkOwner();
                  _;
              }
              /**
               * @dev Returns the address of the current owner.
               */
              function owner() public view virtual returns (address) {
                  return _owner;
              }
              /**
               * @dev Throws if the sender is not the owner.
               */
              function _checkOwner() internal view virtual {
                  require(owner() == _msgSender(), "Ownable: caller is not the owner");
              }
              /**
               * @dev Leaves the contract without owner. It will not be possible to call
               * `onlyOwner` functions anymore. Can only be called by the current owner.
               *
               * NOTE: Renouncing ownership will leave the contract without an owner,
               * thereby removing any functionality that is only available to the owner.
               */
              function renounceOwnership() public virtual onlyOwner {
                  _transferOwnership(address(0));
              }
              /**
               * @dev Transfers ownership of the contract to a new account (`newOwner`).
               * Can only be called by the current owner.
               */
              function transferOwnership(address newOwner) public virtual onlyOwner {
                  require(newOwner != address(0), "Ownable: new owner is the zero address");
                  _transferOwnership(newOwner);
              }
              /**
               * @dev Transfers ownership of the contract to a new account (`newOwner`).
               * Internal function without access restriction.
               */
              function _transferOwnership(address newOwner) internal virtual {
                  address oldOwner = _owner;
                  _owner = newOwner;
                  emit OwnershipTransferred(oldOwner, newOwner);
              }
          }
          // SPDX-License-Identifier: MIT
          // OpenZeppelin Contracts (last updated v4.7.0) (token/ERC721/ERC721.sol)
          pragma solidity ^0.8.0;
          import "@openzeppelin-4.7/contracts/token/ERC721/IERC721.sol";
          import "@openzeppelin-4.7/contracts/token/ERC721/IERC721Receiver.sol";
          import "@openzeppelin-4.7/contracts/token/ERC721/extensions/IERC721Metadata.sol";
          import "@openzeppelin-4.7/contracts/utils/Address.sol";
          import "@openzeppelin-4.7/contracts/utils/Context.sol";
          import "@openzeppelin-4.7/contracts/utils/Strings.sol";
          import "@openzeppelin-4.7/contracts/utils/introspection/ERC165.sol";
          /**
           * @dev Forked version of the OpenZeppelin v4.7.1 ERC721 contract. Utilizes a
           * struct to pack owner and hash seed into a single storage slot.
           * ---------------------
           * @dev Implementation of https://eips.ethereum.org/EIPS/eip-721[ERC721] Non-Fungible Token Standard, including
           * the Metadata extension, but not including the Enumerable extension, which is available separately as
           * {ERC721Enumerable}.
           */
          contract ERC721_PackedHashSeed is Context, ERC165, IERC721, IERC721Metadata {
              using Address for address;
              using Strings for uint256;
              // Token name
              string private _name;
              // Token symbol
              string private _symbol;
              /// struct to pack a token owner and hash seed into same storage slot
              struct OwnerAndHashSeed {
                  // 20 bytes for address of token's owner
                  address owner;
                  // remaining 12 bytes allocated to token hash seed
                  bytes12 hashSeed;
              }
              /// mapping of token ID to OwnerAndHashSeed
              /// @dev visibility internal so inheriting contracts can access
              mapping(uint256 => OwnerAndHashSeed) internal _ownersAndHashSeeds;
              // Mapping owner address to token count
              mapping(address => uint256) private _balances;
              // Mapping from token ID to approved address
              mapping(uint256 => address) private _tokenApprovals;
              // Mapping from owner to operator approvals
              mapping(address => mapping(address => bool)) private _operatorApprovals;
              /**
               * @dev Initializes the contract by setting a `name` and a `symbol` to the token collection.
               */
              constructor(string memory name_, string memory symbol_) {
                  _name = name_;
                  _symbol = symbol_;
              }
              /**
               * @dev See {IERC165-supportsInterface}.
               */
              function supportsInterface(bytes4 interfaceId)
                  public
                  view
                  virtual
                  override(ERC165, IERC165)
                  returns (bool)
              {
                  return
                      interfaceId == type(IERC721).interfaceId ||
                      interfaceId == type(IERC721Metadata).interfaceId ||
                      super.supportsInterface(interfaceId);
              }
              /**
               * @dev See {IERC721-balanceOf}.
               */
              function balanceOf(address owner)
                  public
                  view
                  virtual
                  override
                  returns (uint256)
              {
                  require(
                      owner != address(0),
                      "ERC721: address zero is not a valid owner"
                  );
                  return _balances[owner];
              }
              /**
               * @dev See {IERC721-ownerOf}.
               */
              function ownerOf(uint256 tokenId)
                  public
                  view
                  virtual
                  override
                  returns (address)
              {
                  address owner = _ownersAndHashSeeds[tokenId].owner;
                  require(owner != address(0), "ERC721: invalid token ID");
                  return owner;
              }
              /**
               * @dev See {IERC721Metadata-name}.
               */
              function name() public view virtual override returns (string memory) {
                  return _name;
              }
              /**
               * @dev See {IERC721Metadata-symbol}.
               */
              function symbol() public view virtual override returns (string memory) {
                  return _symbol;
              }
              /**
               * @dev See {IERC721Metadata-tokenURI}.
               */
              function tokenURI(uint256 tokenId)
                  public
                  view
                  virtual
                  override
                  returns (string memory)
              {
                  _requireMinted(tokenId);
                  string memory baseURI = _baseURI();
                  return
                      bytes(baseURI).length > 0
                          ? string(abi.encodePacked(baseURI, tokenId.toString()))
                          : "";
              }
              /**
               * @dev Base URI for computing {tokenURI}. If set, the resulting URI for each
               * token will be the concatenation of the `baseURI` and the `tokenId`. Empty
               * by default, can be overridden in child contracts.
               */
              function _baseURI() internal view virtual returns (string memory) {
                  return "";
              }
              /**
               * @dev See {IERC721-approve}.
               */
              function approve(address to, uint256 tokenId) public virtual override {
                  address owner = ERC721_PackedHashSeed.ownerOf(tokenId);
                  require(to != owner, "ERC721: approval to current owner");
                  require(
                      _msgSender() == owner || isApprovedForAll(owner, _msgSender()),
                      "ERC721: approve caller is not token owner nor approved for all"
                  );
                  _approve(to, tokenId);
              }
              /**
               * @dev See {IERC721-getApproved}.
               */
              function getApproved(uint256 tokenId)
                  public
                  view
                  virtual
                  override
                  returns (address)
              {
                  _requireMinted(tokenId);
                  return _tokenApprovals[tokenId];
              }
              /**
               * @dev See {IERC721-setApprovalForAll}.
               */
              function setApprovalForAll(address operator, bool approved)
                  public
                  virtual
                  override
              {
                  _setApprovalForAll(_msgSender(), operator, approved);
              }
              /**
               * @dev See {IERC721-isApprovedForAll}.
               */
              function isApprovedForAll(address owner, address operator)
                  public
                  view
                  virtual
                  override
                  returns (bool)
              {
                  return _operatorApprovals[owner][operator];
              }
              /**
               * @dev See {IERC721-transferFrom}.
               */
              function transferFrom(
                  address from,
                  address to,
                  uint256 tokenId
              ) public virtual override {
                  //solhint-disable-next-line max-line-length
                  require(
                      _isApprovedOrOwner(_msgSender(), tokenId),
                      "ERC721: caller is not token owner nor approved"
                  );
                  _transfer(from, to, tokenId);
              }
              /**
               * @dev See {IERC721-safeTransferFrom}.
               */
              function safeTransferFrom(
                  address from,
                  address to,
                  uint256 tokenId
              ) public virtual override {
                  safeTransferFrom(from, to, tokenId, "");
              }
              /**
               * @dev See {IERC721-safeTransferFrom}.
               */
              function safeTransferFrom(
                  address from,
                  address to,
                  uint256 tokenId,
                  bytes memory data
              ) public virtual override {
                  require(
                      _isApprovedOrOwner(_msgSender(), tokenId),
                      "ERC721: caller is not token owner nor approved"
                  );
                  _safeTransfer(from, to, tokenId, data);
              }
              /**
               * @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients
               * are aware of the ERC721 protocol to prevent tokens from being forever locked.
               *
               * `data` is additional data, it has no specified format and it is sent in call to `to`.
               *
               * This internal function is equivalent to {safeTransferFrom}, and can be used to e.g.
               * implement alternative mechanisms to perform token transfer, such as signature-based.
               *
               * Requirements:
               *
               * - `from` cannot be the zero address.
               * - `to` cannot be the zero address.
               * - `tokenId` token must exist and be owned by `from`.
               * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
               *
               * Emits a {Transfer} event.
               */
              function _safeTransfer(
                  address from,
                  address to,
                  uint256 tokenId,
                  bytes memory data
              ) internal virtual {
                  _transfer(from, to, tokenId);
                  require(
                      _checkOnERC721Received(from, to, tokenId, data),
                      "ERC721: transfer to non ERC721Receiver implementer"
                  );
              }
              /**
               * @dev Returns whether `tokenId` exists.
               *
               * Tokens can be managed by their owner or approved accounts via {approve} or {setApprovalForAll}.
               *
               * Tokens start existing when they are minted (`_mint`),
               * and stop existing when they are burned (`_burn`).
               */
              function _exists(uint256 tokenId) internal view virtual returns (bool) {
                  return _ownersAndHashSeeds[tokenId].owner != address(0);
              }
              /**
               * @dev Returns whether `spender` is allowed to manage `tokenId`.
               *
               * Requirements:
               *
               * - `tokenId` must exist.
               */
              function _isApprovedOrOwner(address spender, uint256 tokenId)
                  internal
                  view
                  virtual
                  returns (bool)
              {
                  address owner = ERC721_PackedHashSeed.ownerOf(tokenId);
                  return (spender == owner ||
                      isApprovedForAll(owner, spender) ||
                      getApproved(tokenId) == spender);
              }
              /**
               * @dev Safely mints `tokenId` and transfers it to `to`.
               *
               * Requirements:
               *
               * - `tokenId` must not exist.
               * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
               *
               * Emits a {Transfer} event.
               */
              function _safeMint(address to, uint256 tokenId) internal virtual {
                  _safeMint(to, tokenId, "");
              }
              /**
               * @dev Same as {xref-ERC721-_safeMint-address-uint256-}[`_safeMint`], with an additional `data` parameter which is
               * forwarded in {IERC721Receiver-onERC721Received} to contract recipients.
               */
              function _safeMint(
                  address to,
                  uint256 tokenId,
                  bytes memory data
              ) internal virtual {
                  _mint(to, tokenId);
                  require(
                      _checkOnERC721Received(address(0), to, tokenId, data),
                      "ERC721: transfer to non ERC721Receiver implementer"
                  );
              }
              /**
               * @dev Mints `tokenId` and transfers it to `to`.
               *
               * WARNING: Usage of this method is discouraged, use {_safeMint} whenever possible
               *
               * Requirements:
               *
               * - `tokenId` must not exist.
               * - `to` cannot be the zero address.
               *
               * Emits a {Transfer} event.
               */
              function _mint(address to, uint256 tokenId) internal virtual {
                  require(to != address(0), "ERC721: mint to the zero address");
                  require(!_exists(tokenId), "ERC721: token already minted");
                  _beforeTokenTransfer(address(0), to, tokenId);
                  _balances[to] += 1;
                  _ownersAndHashSeeds[tokenId].owner = to;
                  emit Transfer(address(0), to, tokenId);
                  _afterTokenTransfer(address(0), to, tokenId);
              }
              /**
               * @dev Destroys `tokenId`.
               * The approval is cleared when the token is burned.
               *
               * Requirements:
               *
               * - `tokenId` must exist.
               *
               * Emits a {Transfer} event.
               */
              function _burn(uint256 tokenId) internal virtual {
                  address owner = ERC721_PackedHashSeed.ownerOf(tokenId);
                  _beforeTokenTransfer(owner, address(0), tokenId);
                  // Clear approvals
                  _approve(address(0), tokenId);
                  _balances[owner] -= 1;
                  delete _ownersAndHashSeeds[tokenId].owner;
                  emit Transfer(owner, address(0), tokenId);
                  _afterTokenTransfer(owner, address(0), tokenId);
              }
              /**
               * @dev Transfers `tokenId` from `from` to `to`.
               *  As opposed to {transferFrom}, this imposes no restrictions on msg.sender.
               *
               * Requirements:
               *
               * - `to` cannot be the zero address.
               * - `tokenId` token must be owned by `from`.
               *
               * Emits a {Transfer} event.
               */
              function _transfer(
                  address from,
                  address to,
                  uint256 tokenId
              ) internal virtual {
                  require(
                      ERC721_PackedHashSeed.ownerOf(tokenId) == from,
                      "ERC721: transfer from incorrect owner"
                  );
                  require(to != address(0), "ERC721: transfer to the zero address");
                  _beforeTokenTransfer(from, to, tokenId);
                  // Clear approvals from the previous owner
                  _approve(address(0), tokenId);
                  _balances[from] -= 1;
                  _balances[to] += 1;
                  _ownersAndHashSeeds[tokenId].owner = to;
                  emit Transfer(from, to, tokenId);
                  _afterTokenTransfer(from, to, tokenId);
              }
              /**
               * @dev Approve `to` to operate on `tokenId`
               *
               * Emits an {Approval} event.
               */
              function _approve(address to, uint256 tokenId) internal virtual {
                  _tokenApprovals[tokenId] = to;
                  emit Approval(ERC721_PackedHashSeed.ownerOf(tokenId), to, tokenId);
              }
              /**
               * @dev Approve `operator` to operate on all of `owner` tokens
               *
               * Emits an {ApprovalForAll} event.
               */
              function _setApprovalForAll(
                  address owner,
                  address operator,
                  bool approved
              ) internal virtual {
                  require(owner != operator, "ERC721: approve to caller");
                  _operatorApprovals[owner][operator] = approved;
                  emit ApprovalForAll(owner, operator, approved);
              }
              /**
               * @dev Reverts if the `tokenId` has not been minted yet.
               */
              function _requireMinted(uint256 tokenId) internal view virtual {
                  require(_exists(tokenId), "ERC721: invalid token ID");
              }
              /**
               * @dev Internal function to invoke {IERC721Receiver-onERC721Received} on a target address.
               * The call is not executed if the target address is not a contract.
               *
               * @param from address representing the previous owner of the given token ID
               * @param to target address that will receive the tokens
               * @param tokenId uint256 ID of the token to be transferred
               * @param data bytes optional data to send along with the call
               * @return bool whether the call correctly returned the expected magic value
               */
              function _checkOnERC721Received(
                  address from,
                  address to,
                  uint256 tokenId,
                  bytes memory data
              ) private returns (bool) {
                  if (to.isContract()) {
                      try
                          IERC721Receiver(to).onERC721Received(
                              _msgSender(),
                              from,
                              tokenId,
                              data
                          )
                      returns (bytes4 retval) {
                          return retval == IERC721Receiver.onERC721Received.selector;
                      } catch (bytes memory reason) {
                          if (reason.length == 0) {
                              revert(
                                  "ERC721: transfer to non ERC721Receiver implementer"
                              );
                          } else {
                              /// @solidity memory-safe-assembly
                              assembly {
                                  revert(add(32, reason), mload(reason))
                              }
                          }
                      }
                  } else {
                      return true;
                  }
              }
              /**
               * @dev Hook that is called before any token transfer. This includes minting
               * and burning.
               *
               * Calling conditions:
               *
               * - When `from` and `to` are both non-zero, ``from``'s `tokenId` will be
               * transferred to `to`.
               * - When `from` is zero, `tokenId` will be minted for `to`.
               * - When `to` is zero, ``from``'s `tokenId` will be burned.
               * - `from` and `to` are never both zero.
               *
               * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
               */
              function _beforeTokenTransfer(
                  address from,
                  address to,
                  uint256 tokenId
              ) internal virtual {}
              /**
               * @dev Hook that is called after any transfer of tokens. This includes
               * minting and burning.
               *
               * Calling conditions:
               *
               * - when `from` and `to` are both non-zero.
               * - `from` and `to` are never both zero.
               *
               * To learn more about hooks, head to xref:ROOT:extending-contracts.adoc#using-hooks[Using Hooks].
               */
              function _afterTokenTransfer(
                  address from,
                  address to,
                  uint256 tokenId
              ) internal virtual {}
          }
          // SPDX-License-Identifier: LGPL-3.0-only
          // Created By: Art Blocks Inc.
          pragma solidity ^0.8.0;
          /**
           * @title Art Blocks Script Storage Library
           * @notice Utilize contract bytecode as persistant storage for large chunks of script string data.
           *
           * @author Art Blocks Inc.
           * @author Modified from 0xSequence (https://github.com/0xsequence/sstore2/blob/master/contracts/SSTORE2.sol)
           * @author Modified from Solmate (https://github.com/transmissions11/solmate/blob/main/src/utils/SSTORE2.sol)
           *
           * @dev Compared to the above two rerferenced libraries, this contracts-as-storage implementation makes a few
           *      notably different design decisions:
           *      - uses the `string` data type for input/output on reads, rather than speaking in bytes directly
           *      - exposes "delete" functionality, allowing no-longer-used storage to be purged from chain state
           *      - stores the "writer" address (library user) in the deployed contract bytes, which is useful for both:
           *         a) providing necessary information for safe deletion; and
           *         b) allowing this to be introspected on-chain
           *      Also, given that much of this library is written in assembly, this library makes use of a slightly
           *      different convention (when compared to the rest of the Art Blocks smart contract repo) around
           *      pre-defining return values in some cases in order to simplify need to directly memory manage these
           *      return values.
           */
          library BytecodeStorage {
              //---------------------------------------------------------------------------------------------------------------//
              // Starting Index | Size | Ending Index | Description                                                            //
              //---------------------------------------------------------------------------------------------------------------//
              // 0              | N/A  | 0            |                                                                        //
              // 0              | 72   | 72           | the bytes of the gated-cleanup-logic allowing for `selfdestruct`ion    //
              // 72             | 32   | 104          | the 32 bytes for storing the deploying contract's (0-padded) address   //
              //---------------------------------------------------------------------------------------------------------------//
              // Define the offset for where the "logic bytes" end, and the "data bytes" begin. Note that this is a manually
              // calculated value, and must be updated if the above table is changed. It is expected that tests will fail
              // loudly if these values are not updated in-step with eachother.
              uint256 internal constant DATA_OFFSET = 104;
              uint256 internal constant ADDRESS_OFFSET = 72;
              /*//////////////////////////////////////////////////////////////
                                     WRITE LOGIC
              //////////////////////////////////////////////////////////////*/
              /**
               * @notice Write a string to contract bytecode
               * @param _data string to be written to contract
               * @return address_ address of deployed contract with bytecode containing concat(gated-cleanup-logic, data)
               */
              function writeToBytecode(string memory _data)
                  internal
                  returns (address address_)
              {
                  // prefix bytecode with
                  bytes memory creationCode = abi.encodePacked(
                      //---------------------------------------------------------------------------------------------------------------//
                      // Opcode  | Opcode + Arguments  | Description  | Stack View                                                     //
                      //---------------------------------------------------------------------------------------------------------------//
                      // (0) creation code returns all code in the contract except for the first 11 (0B in hex) bytes, as these 11
                      //     bytes are the creation code itself which we do not want to store in the deployed storage contract result
                      //---------------------------------------------------------------------------------------------------------------//
                      // 0x60    |  0x60_0B            | PUSH1 11     | codeOffset                                                     //
                      // 0x59    |  0x59               | MSIZE        | 0 codeOffset                                                   //
                      // 0x81    |  0x81               | DUP2         | codeOffset 0 codeOffset                                        //
                      // 0x38    |  0x38               | CODESIZE     | codeSize codeOffset 0 codeOffset                               //
                      // 0x03    |  0x03               | SUB          | (codeSize - codeOffset) 0 codeOffset                           //
                      // 0x80    |  0x80               | DUP          | (codeSize - codeOffset) (codeSize - codeOffset) 0 codeOffset   //
                      // 0x92    |  0x92               | SWAP3        | codeOffset (codeSize - codeOffset) 0 (codeSize - codeOffset)   //
                      // 0x59    |  0x59               | MSIZE        | 0 codeOffset (codeSize - codeOffset) 0 (codeSize - codeOffset) //
                      // 0x39    |  0x39               | CODECOPY     | 0 (codeSize - codeOffset)                                      //
                      // 0xf3    |  0xf3               | RETURN       |                                                                //
                      //---------------------------------------------------------------------------------------------------------------//
                      // (11 bytes)
                      hex"60_0B_59_81_38_03_80_92_59_39_F3",
                      //---------------------------------------------------------------------------------------------------------------//
                      // Opcode  | Opcode + Arguments  | Description  | Stack View                                                     //
                      //---------------------------------------------------------------------------------------------------------------//
                      // (1a) conditional logic for determing purge-gate (only the bytecode contract deployer can `selfdestruct`)
                      //---------------------------------------------------------------------------------------------------------------//
                      // 0x60    |  0x60_20            | PUSH1 32           | 32                                                       //
                      // 0x60    |  0x60_48            | PUSH1 72 (*)       | contractOffset 32                                        //
                      // 0x60    |  0x60_00            | PUSH1 0            | 0 contractOffset 32                                      //
                      // 0x39    |  0x39               | CODECOPY           |                                                          //
                      // 0x60    |  0x60_00            | PUSH1 0            | 0                                                        //
                      // 0x51    |  0x51               | MLOAD              | byteDeployerAddress                                      //
                      // 0x33    |  0x33               | CALLER             | msg.sender byteDeployerAddress                           //
                      // 0x14    |  0x14               | EQ                 | (msg.sender == byteDeployerAddress)                      //
                      //---------------------------------------------------------------------------------------------------------------//
                      // (12 bytes: 0-11 in deployed contract)
                      hex"60_20_60_48_60_00_39_60_00_51_33_14",
                      //---------------------------------------------------------------------------------------------------------------//
                      // (1b) load up the destination jump address for `(2a) calldata length check` logic, jump or raise `invalid` op-code
                      //---------------------------------------------------------------------------------------------------------------//
                      // 0x60    |  0x60_10            | PUSH1 16 (^)       | jumpDestination (msg.sender == byteDeployerAddress)      //
                      // 0x57    |  0x57               | JUMPI              |                                                          //
                      // 0xFE    |  0xFE               | INVALID            |                                                          //
                      //---------------------------------------------------------------------------------------------------------------//
                      // (4 bytes: 12-15 in deployed contract)
                      hex"60_10_57_FE",
                      //---------------------------------------------------------------------------------------------------------------//
                      // (2a) conditional logic for determing purge-gate (only if calldata length is 1 byte)
                      //---------------------------------------------------------------------------------------------------------------//
                      // 0x5B    |  0x5B               | JUMPDEST (16)      |                                                          //
                      // 0x60    |  0x60_01            | PUSH1 1            | 1                                                        //
                      // 0x36    |  0x36               | CALLDATASIZE       | calldataSize 1                                           //
                      // 0x14    |  0x14               | EQ                 | (calldataSize == 1)                                      //
                      //---------------------------------------------------------------------------------------------------------------//
                      // (5 bytes: 16-20 in deployed contract)
                      hex"5B_60_01_36_14",
                      //---------------------------------------------------------------------------------------------------------------//
                      // (2b) load up the destination jump address for `(3a) calldata value check` logic, jump or raise `invalid` op-code
                      //---------------------------------------------------------------------------------------------------------------//
                      // 0x60    |  0x60_19            | PUSH1 25 (^)       | jumpDestination (calldataSize == 1)                      //
                      // 0x57    |  0x57               | JUMPI              |                                                          //
                      // 0xFE    |  0xFE               | INVALID            |                                                          //
                      //---------------------------------------------------------------------------------------------------------------//
                      // (4 bytes: 21-24 in deployed contract)
                      hex"60_19_57_FE",
                      //---------------------------------------------------------------------------------------------------------------//
                      // (3a) conditional logic for determing purge-gate (only if calldata is `0xFF`)
                      //---------------------------------------------------------------------------------------------------------------//
                      // 0x5B    |  0x5B               | JUMPDEST (25)      |                                                          //
                      // 0x60    |  0x60_00            | PUSH1 0            | 0                                                        //
                      // 0x35    |  0x35               | CALLDATALOAD       | calldata                                                 //
                      // 0x7F    |  0x7F_FF_00_..._00  | PUSH32 0xFF00...00 | 0xFF0...00 calldata                                      //
                      // 0x14    |  0x14               | EQ                 | (0xFF00...00 == calldata)                                //
                      //---------------------------------------------------------------------------------------------------------------//
                      // (4 bytes: 25-28 in deployed contract)
                      hex"5B_60_00_35",
                      // (33 bytes: 29-61 in deployed contract)
                      hex"7F_FF_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00_00",
                      // (1 byte: 62 in deployed contract)
                      hex"14",
                      //---------------------------------------------------------------------------------------------------------------//
                      // (3b) load up the destination jump address for actual purging (4), jump or raise `invalid` op-code
                      //---------------------------------------------------------------------------------------------------------------//
                      // 0x60    |  0x60_43            | PUSH1 67 (^)       | jumpDestination (0xFF00...00 == calldata)                //
                      // 0x57    |  0x57               | JUMPI              |                                                          //
                      // 0xFE    |  0xFE               | INVALID            |                                                          //
                      //---------------------------------------------------------------------------------------------------------------//
                      // (4 bytes: 63-66 in deployed contract)
                      hex"60_43_57_FE",
                      //---------------------------------------------------------------------------------------------------------------//
                      // (4) perform actual purging
                      //---------------------------------------------------------------------------------------------------------------//
                      // 0x5B    |  0x5B               | JUMPDEST (67)      |                                                          //
                      // 0x60    |  0x60_00            | PUSH1 0            | 0                                                        //
                      // 0x51    |  0x51               | MLOAD              | byteDeployerAddress                                      //
                      // 0xFF    |  0xFF               | SELFDESTRUCT       |                                                          //
                      //---------------------------------------------------------------------------------------------------------------//
                      // (5 bytes: 67-71 in deployed contract)
                      hex"5B_60_00_51_FF",
                      //---------------------------------------------------------------------------------------------------------------//
                      // (*) Note: this value must be adjusted if selfdestruct purge logic is adjusted, to refer to the correct start  //
                      //           offset for where the `msg.sender` address was stored in deployed bytecode.                          //
                      //                                                                                                               //
                      // (^) Note: this value must be adjusted if portions of the selfdestruct purge logic are adjusted.               //
                      //---------------------------------------------------------------------------------------------------------------//
                      //
                      // store the deploying-contract's address (to be used to gate and call `selfdestruct`),
                      // with expected 0-padding to fit a 20-byte address into a 30-byte slot.
                      //
                      // note: it is important that this address is the executing contract's address
                      //      (the address that represents the client-application smart contract of this library)
                      //      which means that it is the responsibility of the client-application smart contract
                      //      to determine how deletes are gated (or if they are exposed at all) as it is only
                      //      this contract that will be able to call `purgeBytecode` as the `CALLER` that is
                      //      checked above (op-code 0x33).
                      hex"00_00_00_00_00_00_00_00_00_00_00_00", // left-pad 20-byte address with 12 0x00 bytes
                      address(this),
                      // uploaded data (stored as bytecode) comes last
                      _data
                  );
                  assembly {
                      // deploy a new contract with the generated creation code.
                      // start 32 bytes into creationCode to avoid copying the byte length.
                      address_ := create(0, add(creationCode, 0x20), mload(creationCode))
                  }
                  // address must be non-zero if contract was deployed successfully
                  require(address_ != address(0), "ContractAsStorage: Write Error");
              }
              /*//////////////////////////////////////////////////////////////
                                         READ LOGIC
              //////////////////////////////////////////////////////////////*/
              /**
               * @notice Read a string from contract bytecode
               * @param _address address of deployed contract with bytecode containing concat(gated-cleanup-logic, data)
               * @return data string read from contract bytecode
               */
              function readFromBytecode(address _address)
                  internal
                  view
                  returns (string memory data)
              {
                  // get the size of the bytecode
                  uint256 bytecodeSize = _bytecodeSizeAt(_address);
                  // handle case where address contains code < DATA_OFFSET
                  // note: the first check here also captures the case where
                  //       (bytecodeSize == 0) implicitly, but we add the second check of
                  //       (bytecodeSize == 0) as a fall-through that will never execute
                  //       unless `DATA_OFFSET` is set to 0 at some point.
                  if ((bytecodeSize < DATA_OFFSET) || (bytecodeSize == 0)) {
                      revert("ContractAsStorage: Read Error");
                  }
                  // handle case where address contains code >= DATA_OFFSET
                  // decrement by DATA_OFFSET to account for purge logic
                  uint256 size;
                  unchecked {
                      size = bytecodeSize - DATA_OFFSET;
                  }
                  assembly {
                      // allocate free memory
                      data := mload(0x40)
                      // update free memory pointer
                      // use and(x, not(0x1f) as cheaper equivalent to sub(x, mod(x, 0x20)).
                      // adding 0x1f to size + logic above ensures the free memory pointer
                      // remains word-aligned, following the Solidity convention.
                      mstore(0x40, add(data, and(add(add(size, 0x20), 0x1f), not(0x1f))))
                      // store length of data in first 32 bytes
                      mstore(data, size)
                      // copy code to memory, excluding the gated-cleanup-logic
                      extcodecopy(_address, add(data, 0x20), DATA_OFFSET, size)
                  }
              }
              /**
               * @notice Get address for deployer for given contract bytecode
               * @param _address address of deployed contract with bytecode containing concat(gated-cleanup-logic, data)
               * @return writerAddress address read from contract bytecode
               */
              function getWriterAddressForBytecode(address _address)
                  internal
                  view
                  returns (address)
              {
                  // get the size of the data
                  uint256 bytecodeSize = _bytecodeSizeAt(_address);
                  // handle case where address contains code < DATA_OFFSET
                  // note: the first check here also captures the case where
                  //       (bytecodeSize == 0) implicitly, but we add the second check of
                  //       (bytecodeSize == 0) as a fall-through that will never execute
                  //       unless `DATA_OFFSET` is set to 0 at some point.
                  if ((bytecodeSize < DATA_OFFSET) || (bytecodeSize == 0)) {
                      revert("ContractAsStorage: Read Error");
                  }
                  assembly {
                      // allocate free memory
                      let writerAddress := mload(0x40)
                      // shift free memory pointer by one slot
                      mstore(0x40, add(mload(0x40), 0x20))
                      // copy the 32-byte address of the data contract writer to memory
                      // note: this relies on the assumption noted at the top-level of
                      //       this file that the storage layout for the deployed
                      //       contracts-as-storage contract looks like:
                      //       | gated-cleanup-logic | deployer-address (padded) | data |
                      extcodecopy(
                          _address,
                          writerAddress,
                          ADDRESS_OFFSET,
                          0x20 // full 32-bytes, as address is expected to be zero-padded
                      )
                      return(
                          writerAddress,
                          0x20 // return size is entire slot, as it is zero-padded
                      )
                  }
              }
              /*//////////////////////////////////////////////////////////////
                                        DELETE LOGIC
              //////////////////////////////////////////////////////////////*/
              /**
               * @notice Purge contract bytecode for cleanup purposes
               * @param _address address of deployed contract with bytecode containing concat(gated-cleanup-logic, data)
               * @dev This contract is only callable by the address of the contract that originally deployed the bytecode
               *      being purged. If this method is called by any other address, it will revert with the `INVALID` op-code.
               *      Additionally, for security purposes, the contract must be called with calldata `0xFF` to ensure that
               *      the `selfdestruct` op-code is intentionally being invoked, otherwise the `INVALID` op-code will be raised.
               */
              function purgeBytecode(address _address) internal {
                  // deployed bytecode (above) handles all logic for purging state, so no
                  // call data is expected to be passed along to perform data purge
                  (
                      bool success, /* `data` not needed */
                  ) = _address.call(hex"FF");
                  if (!success) {
                      revert("ContractAsStorage: Delete Error");
                  }
              }
              /*//////////////////////////////////////////////////////////////
                                    INTERNAL HELPER LOGIC
              //////////////////////////////////////////////////////////////*/
              /**
                  @notice Returns the size of the bytecode at address `_address`
                  @param _address address that may or may not contain bytecode
                  @return size size of the bytecode code at `_address`
              */
              function _bytecodeSizeAt(address _address)
                  private
                  view
                  returns (uint256 size)
              {
                  assembly {
                      size := extcodesize(_address)
                  }
              }
          }
          // SPDX-License-Identifier: LGPL-3.0-only
          // Created By: Art Blocks Inc.
          // Inspired by: https://ethereum.stackexchange.com/a/123950/103422
          pragma solidity ^0.8.0;
          /**
           * @dev Operations on bytes32 data type, dealing with conversion to string.
           */
          library Bytes32Strings {
              /**
               * @dev Intended to convert a `bytes32`-encoded string literal to `string`.
               * Trims zero padding to arrive at original string literal.
               */
              function toString(bytes32 source)
                  internal
                  pure
                  returns (string memory result)
              {
                  uint8 length = 0;
                  while (source[length] != 0 && length < 32) {
                      length++;
                  }
                  assembly {
                      // free memory pointer
                      result := mload(0x40)
                      // update free memory pointer to new "memory end"
                      // (offset is 64-bytes: 32 for length, 32 for data)
                      mstore(0x40, add(result, 0x40))
                      // store length in first 32-byte memory slot
                      mstore(result, length)
                      // write actual data in second 32-byte memory slot
                      mstore(add(result, 0x20), source)
                  }
              }
              /**
               * @dev Intended to check if a `bytes32`-encoded string contains a given
               * character with UTF-8 character code `utf8CharCode exactly `targetQty`
               * times. Does not support searching for multi-byte characters, only
               * characters with UTF-8 character codes < 0x80.
               */
              function containsExactCharacterQty(
                  bytes32 source,
                  uint8 utf8CharCode,
                  uint8 targetQty
              ) internal pure returns (bool) {
                  uint8 _occurrences = 0;
                  uint8 i;
                  for (i = 0; i < 32; ) {
                      uint8 _charCode = uint8(source[i]);
                      // if not a null byte, or a multi-byte UTF-8 character, check match
                      if (_charCode != 0 && _charCode < 0x80) {
                          if (_charCode == utf8CharCode) {
                              unchecked {
                                  // no risk of overflow since max 32 iterations < max uin8=255
                                  ++_occurrences;
                              }
                          }
                      }
                      unchecked {
                          // no risk of overflow since max 32 iterations < max uin8=255
                          ++i;
                      }
                  }
                  return _occurrences == targetQty;
              }
          }
          // SPDX-License-Identifier: MIT
          // OpenZeppelin Contracts v4.4.1 (utils/Context.sol)
          pragma solidity ^0.8.0;
          /**
           * @dev Provides information about the current execution context, including the
           * sender of the transaction and its data. While these are generally available
           * via msg.sender and msg.data, they should not be accessed in such a direct
           * manner, since when dealing with meta-transactions the account sending and
           * paying for execution may not be the actual sender (as far as an application
           * is concerned).
           *
           * This contract is only required for intermediate, library-like contracts.
           */
          abstract contract Context {
              function _msgSender() internal view virtual returns (address) {
                  return msg.sender;
              }
              function _msgData() internal view virtual returns (bytes calldata) {
                  return msg.data;
              }
          }
          // SPDX-License-Identifier: MIT
          // OpenZeppelin Contracts (last updated v4.7.0) (token/ERC721/IERC721.sol)
          pragma solidity ^0.8.0;
          import "../../utils/introspection/IERC165.sol";
          /**
           * @dev Required interface of an ERC721 compliant contract.
           */
          interface IERC721 is IERC165 {
              /**
               * @dev Emitted when `tokenId` token is transferred from `from` to `to`.
               */
              event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
              /**
               * @dev Emitted when `owner` enables `approved` to manage the `tokenId` token.
               */
              event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
              /**
               * @dev Emitted when `owner` enables or disables (`approved`) `operator` to manage all of its assets.
               */
              event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
              /**
               * @dev Returns the number of tokens in ``owner``'s account.
               */
              function balanceOf(address owner) external view returns (uint256 balance);
              /**
               * @dev Returns the owner of the `tokenId` token.
               *
               * Requirements:
               *
               * - `tokenId` must exist.
               */
              function ownerOf(uint256 tokenId) external view returns (address owner);
              /**
               * @dev Safely transfers `tokenId` token from `from` to `to`.
               *
               * Requirements:
               *
               * - `from` cannot be the zero address.
               * - `to` cannot be the zero address.
               * - `tokenId` token must exist and be owned by `from`.
               * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.
               * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
               *
               * Emits a {Transfer} event.
               */
              function safeTransferFrom(
                  address from,
                  address to,
                  uint256 tokenId,
                  bytes calldata data
              ) external;
              /**
               * @dev Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients
               * are aware of the ERC721 protocol to prevent tokens from being forever locked.
               *
               * Requirements:
               *
               * - `from` cannot be the zero address.
               * - `to` cannot be the zero address.
               * - `tokenId` token must exist and be owned by `from`.
               * - If the caller is not `from`, it must have been allowed to move this token by either {approve} or {setApprovalForAll}.
               * - If `to` refers to a smart contract, it must implement {IERC721Receiver-onERC721Received}, which is called upon a safe transfer.
               *
               * Emits a {Transfer} event.
               */
              function safeTransferFrom(
                  address from,
                  address to,
                  uint256 tokenId
              ) external;
              /**
               * @dev Transfers `tokenId` token from `from` to `to`.
               *
               * WARNING: Usage of this method is discouraged, use {safeTransferFrom} whenever possible.
               *
               * Requirements:
               *
               * - `from` cannot be the zero address.
               * - `to` cannot be the zero address.
               * - `tokenId` token must be owned by `from`.
               * - If the caller is not `from`, it must be approved to move this token by either {approve} or {setApprovalForAll}.
               *
               * Emits a {Transfer} event.
               */
              function transferFrom(
                  address from,
                  address to,
                  uint256 tokenId
              ) external;
              /**
               * @dev Gives permission to `to` to transfer `tokenId` token to another account.
               * The approval is cleared when the token is transferred.
               *
               * Only a single account can be approved at a time, so approving the zero address clears previous approvals.
               *
               * Requirements:
               *
               * - The caller must own the token or be an approved operator.
               * - `tokenId` must exist.
               *
               * Emits an {Approval} event.
               */
              function approve(address to, uint256 tokenId) external;
              /**
               * @dev Approve or remove `operator` as an operator for the caller.
               * Operators can call {transferFrom} or {safeTransferFrom} for any token owned by the caller.
               *
               * Requirements:
               *
               * - The `operator` cannot be the caller.
               *
               * Emits an {ApprovalForAll} event.
               */
              function setApprovalForAll(address operator, bool _approved) external;
              /**
               * @dev Returns the account approved for `tokenId` token.
               *
               * Requirements:
               *
               * - `tokenId` must exist.
               */
              function getApproved(uint256 tokenId) external view returns (address operator);
              /**
               * @dev Returns if the `operator` is allowed to manage all of the assets of `owner`.
               *
               * See {setApprovalForAll}
               */
              function isApprovedForAll(address owner, address operator) external view returns (bool);
          }
          // SPDX-License-Identifier: MIT
          // OpenZeppelin Contracts (last updated v4.6.0) (token/ERC721/IERC721Receiver.sol)
          pragma solidity ^0.8.0;
          /**
           * @title ERC721 token receiver interface
           * @dev Interface for any contract that wants to support safeTransfers
           * from ERC721 asset contracts.
           */
          interface IERC721Receiver {
              /**
               * @dev Whenever an {IERC721} `tokenId` token is transferred to this contract via {IERC721-safeTransferFrom}
               * by `operator` from `from`, this function is called.
               *
               * It must return its Solidity selector to confirm the token transfer.
               * If any other value is returned or the interface is not implemented by the recipient, the transfer will be reverted.
               *
               * The selector can be obtained in Solidity with `IERC721Receiver.onERC721Received.selector`.
               */
              function onERC721Received(
                  address operator,
                  address from,
                  uint256 tokenId,
                  bytes calldata data
              ) external returns (bytes4);
          }
          // SPDX-License-Identifier: MIT
          // OpenZeppelin Contracts v4.4.1 (token/ERC721/extensions/IERC721Metadata.sol)
          pragma solidity ^0.8.0;
          import "../IERC721.sol";
          /**
           * @title ERC-721 Non-Fungible Token Standard, optional metadata extension
           * @dev See https://eips.ethereum.org/EIPS/eip-721
           */
          interface IERC721Metadata is IERC721 {
              /**
               * @dev Returns the token collection name.
               */
              function name() external view returns (string memory);
              /**
               * @dev Returns the token collection symbol.
               */
              function symbol() external view returns (string memory);
              /**
               * @dev Returns the Uniform Resource Identifier (URI) for `tokenId` token.
               */
              function tokenURI(uint256 tokenId) external view returns (string memory);
          }
          // SPDX-License-Identifier: MIT
          // OpenZeppelin Contracts (last updated v4.7.0) (utils/Address.sol)
          pragma solidity ^0.8.1;
          /**
           * @dev Collection of functions related to the address type
           */
          library Address {
              /**
               * @dev Returns true if `account` is a contract.
               *
               * [IMPORTANT]
               * ====
               * It is unsafe to assume that an address for which this function returns
               * false is an externally-owned account (EOA) and not a contract.
               *
               * Among others, `isContract` will return false for the following
               * types of addresses:
               *
               *  - an externally-owned account
               *  - a contract in construction
               *  - an address where a contract will be created
               *  - an address where a contract lived, but was destroyed
               * ====
               *
               * [IMPORTANT]
               * ====
               * You shouldn't rely on `isContract` to protect against flash loan attacks!
               *
               * Preventing calls from contracts is highly discouraged. It breaks composability, breaks support for smart wallets
               * like Gnosis Safe, and does not provide security since it can be circumvented by calling from a contract
               * constructor.
               * ====
               */
              function isContract(address account) internal view returns (bool) {
                  // This method relies on extcodesize/address.code.length, which returns 0
                  // for contracts in construction, since the code is only stored at the end
                  // of the constructor execution.
                  return account.code.length > 0;
              }
              /**
               * @dev Replacement for Solidity's `transfer`: sends `amount` wei to
               * `recipient`, forwarding all available gas and reverting on errors.
               *
               * https://eips.ethereum.org/EIPS/eip-1884[EIP1884] increases the gas cost
               * of certain opcodes, possibly making contracts go over the 2300 gas limit
               * imposed by `transfer`, making them unable to receive funds via
               * `transfer`. {sendValue} removes this limitation.
               *
               * https://diligence.consensys.net/posts/2019/09/stop-using-soliditys-transfer-now/[Learn more].
               *
               * IMPORTANT: because control is transferred to `recipient`, care must be
               * taken to not create reentrancy vulnerabilities. Consider using
               * {ReentrancyGuard} or the
               * https://solidity.readthedocs.io/en/v0.5.11/security-considerations.html#use-the-checks-effects-interactions-pattern[checks-effects-interactions pattern].
               */
              function sendValue(address payable recipient, uint256 amount) internal {
                  require(address(this).balance >= amount, "Address: insufficient balance");
                  (bool success, ) = recipient.call{value: amount}("");
                  require(success, "Address: unable to send value, recipient may have reverted");
              }
              /**
               * @dev Performs a Solidity function call using a low level `call`. A
               * plain `call` is an unsafe replacement for a function call: use this
               * function instead.
               *
               * If `target` reverts with a revert reason, it is bubbled up by this
               * function (like regular Solidity function calls).
               *
               * Returns the raw returned data. To convert to the expected return value,
               * use https://solidity.readthedocs.io/en/latest/units-and-global-variables.html?highlight=abi.decode#abi-encoding-and-decoding-functions[`abi.decode`].
               *
               * Requirements:
               *
               * - `target` must be a contract.
               * - calling `target` with `data` must not revert.
               *
               * _Available since v3.1._
               */
              function functionCall(address target, bytes memory data) internal returns (bytes memory) {
                  return functionCall(target, data, "Address: low-level call failed");
              }
              /**
               * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`], but with
               * `errorMessage` as a fallback revert reason when `target` reverts.
               *
               * _Available since v3.1._
               */
              function functionCall(
                  address target,
                  bytes memory data,
                  string memory errorMessage
              ) internal returns (bytes memory) {
                  return functionCallWithValue(target, data, 0, errorMessage);
              }
              /**
               * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
               * but also transferring `value` wei to `target`.
               *
               * Requirements:
               *
               * - the calling contract must have an ETH balance of at least `value`.
               * - the called Solidity function must be `payable`.
               *
               * _Available since v3.1._
               */
              function functionCallWithValue(
                  address target,
                  bytes memory data,
                  uint256 value
              ) internal returns (bytes memory) {
                  return functionCallWithValue(target, data, value, "Address: low-level call with value failed");
              }
              /**
               * @dev Same as {xref-Address-functionCallWithValue-address-bytes-uint256-}[`functionCallWithValue`], but
               * with `errorMessage` as a fallback revert reason when `target` reverts.
               *
               * _Available since v3.1._
               */
              function functionCallWithValue(
                  address target,
                  bytes memory data,
                  uint256 value,
                  string memory errorMessage
              ) internal returns (bytes memory) {
                  require(address(this).balance >= value, "Address: insufficient balance for call");
                  require(isContract(target), "Address: call to non-contract");
                  (bool success, bytes memory returndata) = target.call{value: value}(data);
                  return verifyCallResult(success, returndata, errorMessage);
              }
              /**
               * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
               * but performing a static call.
               *
               * _Available since v3.3._
               */
              function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) {
                  return functionStaticCall(target, data, "Address: low-level static call failed");
              }
              /**
               * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`],
               * but performing a static call.
               *
               * _Available since v3.3._
               */
              function functionStaticCall(
                  address target,
                  bytes memory data,
                  string memory errorMessage
              ) internal view returns (bytes memory) {
                  require(isContract(target), "Address: static call to non-contract");
                  (bool success, bytes memory returndata) = target.staticcall(data);
                  return verifyCallResult(success, returndata, errorMessage);
              }
              /**
               * @dev Same as {xref-Address-functionCall-address-bytes-}[`functionCall`],
               * but performing a delegate call.
               *
               * _Available since v3.4._
               */
              function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) {
                  return functionDelegateCall(target, data, "Address: low-level delegate call failed");
              }
              /**
               * @dev Same as {xref-Address-functionCall-address-bytes-string-}[`functionCall`],
               * but performing a delegate call.
               *
               * _Available since v3.4._
               */
              function functionDelegateCall(
                  address target,
                  bytes memory data,
                  string memory errorMessage
              ) internal returns (bytes memory) {
                  require(isContract(target), "Address: delegate call to non-contract");
                  (bool success, bytes memory returndata) = target.delegatecall(data);
                  return verifyCallResult(success, returndata, errorMessage);
              }
              /**
               * @dev Tool to verifies that a low level call was successful, and revert if it wasn't, either by bubbling the
               * revert reason using the provided one.
               *
               * _Available since v4.3._
               */
              function verifyCallResult(
                  bool success,
                  bytes memory returndata,
                  string memory errorMessage
              ) internal pure returns (bytes memory) {
                  if (success) {
                      return returndata;
                  } else {
                      // Look for revert reason and bubble it up if present
                      if (returndata.length > 0) {
                          // The easiest way to bubble the revert reason is using memory via assembly
                          /// @solidity memory-safe-assembly
                          assembly {
                              let returndata_size := mload(returndata)
                              revert(add(32, returndata), returndata_size)
                          }
                      } else {
                          revert(errorMessage);
                      }
                  }
              }
          }
          // SPDX-License-Identifier: MIT
          // OpenZeppelin Contracts v4.4.1 (utils/introspection/ERC165.sol)
          pragma solidity ^0.8.0;
          import "./IERC165.sol";
          /**
           * @dev Implementation of the {IERC165} interface.
           *
           * Contracts that want to implement ERC165 should inherit from this contract and override {supportsInterface} to check
           * for the additional interface id that will be supported. For example:
           *
           * ```solidity
           * function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
           *     return interfaceId == type(MyInterface).interfaceId || super.supportsInterface(interfaceId);
           * }
           * ```
           *
           * Alternatively, {ERC165Storage} provides an easier to use but more expensive implementation.
           */
          abstract contract ERC165 is IERC165 {
              /**
               * @dev See {IERC165-supportsInterface}.
               */
              function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) {
                  return interfaceId == type(IERC165).interfaceId;
              }
          }
          // SPDX-License-Identifier: MIT
          // OpenZeppelin Contracts v4.4.1 (utils/introspection/IERC165.sol)
          pragma solidity ^0.8.0;
          /**
           * @dev Interface of the ERC165 standard, as defined in the
           * https://eips.ethereum.org/EIPS/eip-165[EIP].
           *
           * Implementers can declare support of contract interfaces, which can then be
           * queried by others ({ERC165Checker}).
           *
           * For an implementation, see {ERC165}.
           */
          interface IERC165 {
              /**
               * @dev Returns true if this contract implements the interface defined by
               * `interfaceId`. See the corresponding
               * https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section]
               * to learn more about how these ids are created.
               *
               * This function call must use less than 30 000 gas.
               */
              function supportsInterface(bytes4 interfaceId) external view returns (bool);
          }
          

          File 3 of 3: MinterFilterV1
          // SPDX-License-Identifier: LGPL-3.0-only
          // Created By: Art Blocks Inc.
          import "../../interfaces/0.8.x/IMinterFilterV0.sol";
          import "../../interfaces/0.8.x/IFilteredMinterV0.sol";
          import "../../interfaces/0.8.x/IAdminACLV0.sol";
          import "../../interfaces/0.8.x/IGenArt721CoreContractV3.sol";
          import "@openzeppelin-4.5/contracts/utils/structs/EnumerableMap.sol";
          pragma solidity 0.8.17;
          /**
           * @title Minter filter contract that allows filtered minters to be set
           * on a per-project basis.
           * This is designed to be used with IGenArt721CoreContractV3 contracts.
           * @author Art Blocks Inc.
           * @notice Privileged Roles and Ownership:
           * This contract is designed to be managed, with limited powers.
           * Privileged roles and abilities are controlled by the core contract's Admin
           * ACL contract and a project's artist. Both of these roles hold extensive
           * power and can modify a project's current minter.
           * Care must be taken to ensure that the admin ACL contract and artist
           * addresses are secure behind a multi-sig or other access control mechanism.
           * ----------------------------------------------------------------------------
           * The following functions are restricted to the core contract's Admin ACL
           * contract:
           * - addApprovedMinters
           * - removeApprovedMinters
           * - removeMintersForProjects
           * ----------------------------------------------------------------------------
           * The following functions are restricted to the core contract's Admin ACL
           * contract or a project's artist:
           * - setMinterForProject
           * - removeMinterForProject
           * ----------------------------------------------------------------------------
           * Additional admin and artist privileged roles may be described on minters
           */
          contract MinterFilterV1 is IMinterFilterV0 {
              // add Enumerable Map methods
              using EnumerableMap for EnumerableMap.UintToAddressMap;
              /// Core contract address this minter interacts with
              address public immutable genArt721CoreAddress;
              /// This contract integrates with the IV3 core contract
              IGenArt721CoreContractV3 private immutable genArtCoreContract;
              /// projectId => minter address
              EnumerableMap.UintToAddressMap private minterForProject;
              /// minter address => qty projects currently using minter
              mapping(address => uint256) public numProjectsUsingMinter;
              /// minter address => is an approved minter?
              mapping(address => bool) public isApprovedMinter;
              modifier onlyNonZeroAddress(address _address) {
                  require(_address != address(0), "Must input non-zero address");
                  _;
              }
              // modifier to restrict access to only AdminACL allowed calls
              // @dev defers which ACL contract is used to the core contract
              modifier onlyCoreAdminACL(bytes4 _selector) {
                  require(_coreAdminACLAllowed(_selector), "Only Core AdminACL allowed");
                  _;
              }
              // modifier to restrict access to only the artist of `_projectId`, or
              // AdminACL allowed calls
              // @dev defers which ACL contract is used to the core contract
              modifier onlyCoreAdminACLOrArtist(uint256 _projectId, bytes4 _selector) {
                  require(
                      msg.sender ==
                          genArtCoreContract.projectIdToArtistAddress(_projectId) ||
                          _coreAdminACLAllowed(_selector),
                      "Only Core AdminACL or Artist"
                  );
                  _;
              }
              modifier onlyValidProjectId(uint256 _projectId) {
                  require(
                      _projectId < genArtCoreContract.nextProjectId(),
                      "Only existing projects"
                  );
                  _;
              }
              modifier usingApprovedMinter(address _minterAddress) {
                  require(
                      isApprovedMinter[_minterAddress],
                      "Only approved minters are allowed"
                  );
                  _;
              }
              /**
               * @notice Initializes contract to be a Minter for `_genArt721Address`.
               * @param _genArt721Address Art Blocks core contract address
               * this contract will be a minter for. Can never be updated.
               */
              constructor(address _genArt721Address)
                  onlyNonZeroAddress(_genArt721Address)
              {
                  genArt721CoreAddress = _genArt721Address;
                  genArtCoreContract = IGenArt721CoreContractV3(_genArt721Address);
              }
              /**
               * @notice Internal function that determines if msg.sender is allowed to
               * call a function on this contract. Defers to core contract's
               * adminACLAllowed function.
               */
              function _coreAdminACLAllowed(bytes4 _selector) internal returns (bool) {
                  return
                      genArtCoreContract.adminACLAllowed(
                          msg.sender,
                          address(this),
                          _selector
                      );
              }
              /**
               * @notice Approves minter `_minterAddress`.
               * @param _minterAddress Minter to be added as an approved minter.
               */
              function addApprovedMinter(address _minterAddress)
                  external
                  onlyCoreAdminACL(this.addApprovedMinter.selector)
                  onlyNonZeroAddress(_minterAddress)
              {
                  isApprovedMinter[_minterAddress] = true;
                  emit MinterApproved(
                      _minterAddress,
                      IFilteredMinterV0(_minterAddress).minterType()
                  );
              }
              /**
               * @notice Removes previously approved minter `_minterAddress`.
               * @param _minterAddress Minter to remove.
               */
              function removeApprovedMinter(address _minterAddress)
                  external
                  onlyCoreAdminACL(this.removeApprovedMinter.selector)
              {
                  require(isApprovedMinter[_minterAddress], "Only approved minters");
                  require(
                      numProjectsUsingMinter[_minterAddress] == 0,
                      "Only unused minters"
                  );
                  isApprovedMinter[_minterAddress] = false;
                  emit MinterRevoked(_minterAddress);
              }
              /**
               * @notice Sets minter for project `_projectId` to minter
               * `_minterAddress`.
               * @param _projectId Project ID to set minter for.
               * @param _minterAddress Minter to be the project's minter.
               */
              function setMinterForProject(uint256 _projectId, address _minterAddress)
                  external
                  onlyCoreAdminACLOrArtist(_projectId, this.setMinterForProject.selector)
                  usingApprovedMinter(_minterAddress)
                  onlyValidProjectId(_projectId)
              {
                  // decrement number of projects using a previous minter
                  (bool hasPreviousMinter, address previousMinter) = minterForProject
                      .tryGet(_projectId);
                  if (hasPreviousMinter) {
                      numProjectsUsingMinter[previousMinter]--;
                  }
                  // add new minter
                  numProjectsUsingMinter[_minterAddress]++;
                  minterForProject.set(_projectId, _minterAddress);
                  emit ProjectMinterRegistered(
                      _projectId,
                      _minterAddress,
                      IFilteredMinterV0(_minterAddress).minterType()
                  );
              }
              /**
               * @notice Updates project `_projectId` to have no configured minter.
               * @param _projectId Project ID to remove minter.
               * @dev requires project to have an assigned minter
               */
              function removeMinterForProject(uint256 _projectId)
                  external
                  onlyCoreAdminACLOrArtist(
                      _projectId,
                      this.removeMinterForProject.selector
                  )
              {
                  _removeMinterForProject(_projectId);
              }
              /**
               * @notice Updates an array of project IDs to have no configured minter.
               * @param _projectIds Array of project IDs to remove minters for.
               * @dev requires all project IDs to have an assigned minter
               * @dev caution with respect to single tx gas limits
               */
              function removeMintersForProjects(uint256[] calldata _projectIds)
                  external
                  onlyCoreAdminACL(this.removeMintersForProjects.selector)
              {
                  uint256 numProjects = _projectIds.length;
                  for (uint256 i; i < numProjects; i++) {
                      _removeMinterForProject(_projectIds[i]);
                  }
              }
              /**
               * @notice Updates project `_projectId` to have no configured minter
               * (reverts tx if project does not have an assigned minter).
               * @param _projectId Project ID to remove minter.
               */
              function _removeMinterForProject(uint256 _projectId) private {
                  // remove minter for project and emit
                  // `minterForProject.get()` reverts tx if no minter set for project
                  numProjectsUsingMinter[minterForProject.get(_projectId)]--;
                  minterForProject.remove(_projectId);
                  emit ProjectMinterRemoved(_projectId);
              }
              /**
               * @notice Mint a token from project `_projectId` to `_to`, originally
               * purchased by `sender`.
               * @param _to The new token's owner.
               * @param _projectId Project ID to mint a new token on.
               * @param sender Address purchasing a new token.
               * @return _tokenId Token ID of minted token
               * @dev reverts w/nonexistent key error when project has no assigned minter
               */
              function mint(
                  address _to,
                  uint256 _projectId,
                  address sender
              ) external returns (uint256 _tokenId) {
                  // CHECKS
                  // minter is the project's minter
                  require(
                      msg.sender == minterForProject.get(_projectId),
                      "Only assigned minter"
                  );
                  // EFFECTS
                  uint256 tokenId = genArtCoreContract.mint_Ecf(_to, _projectId, sender);
                  return tokenId;
              }
              /**
               * @notice Gets the assigned minter for project `_projectId`.
               * @param _projectId Project ID to query.
               * @return address Minter address assigned to project `_projectId`
               * @dev requires project to have an assigned minter
               */
              function getMinterForProject(uint256 _projectId)
                  external
                  view
                  onlyValidProjectId(_projectId)
                  returns (address)
              {
                  (bool _hasMinter, address _currentMinter) = minterForProject.tryGet(
                      _projectId
                  );
                  require(_hasMinter, "No minter assigned");
                  return _currentMinter;
              }
              /**
               * @notice Queries if project `_projectId` has an assigned minter.
               * @param _projectId Project ID to query.
               * @return bool true if project has an assigned minter, else false
               */
              function projectHasMinter(uint256 _projectId)
                  external
                  view
                  onlyValidProjectId(_projectId)
                  returns (bool)
              {
                  (bool _hasMinter, ) = minterForProject.tryGet(_projectId);
                  return _hasMinter;
              }
              /**
               * @notice Gets quantity of projects that have assigned minters.
               * @return uint256 quantity of projects that have assigned minters
               */
              function getNumProjectsWithMinters() external view returns (uint256) {
                  return minterForProject.length();
              }
              /**
               * @notice Get project ID and minter address at index `_index` of
               * enumerable map.
               * @param _index enumerable map index to query.
               * @return projectId project ID at index `_index`
               * @return minterAddress minter address for project at index `_index`
               * @return minterType minter type of minter at minterAddress
               * @dev index must be < quantity of projects that have assigned minters
               */
              function getProjectAndMinterInfoAt(uint256 _index)
                  external
                  view
                  returns (
                      uint256 projectId,
                      address minterAddress,
                      string memory minterType
                  )
              {
                  (projectId, minterAddress) = minterForProject.at(_index);
                  minterType = IFilteredMinterV0(minterAddress).minterType();
                  return (projectId, minterAddress, minterType);
              }
          }
          // SPDX-License-Identifier: LGPL-3.0-only
          // Created By: Art Blocks Inc.
          pragma solidity ^0.8.0;
          interface IMinterFilterV0 {
              /**
               * @notice Approved minter `_minterAddress`.
               */
              event MinterApproved(address indexed _minterAddress, string _minterType);
              /**
               * @notice Revoked approval for minter `_minterAddress`
               */
              event MinterRevoked(address indexed _minterAddress);
              /**
               * @notice Minter `_minterAddress` of type `_minterType`
               * registered for project `_projectId`.
               */
              event ProjectMinterRegistered(
                  uint256 indexed _projectId,
                  address indexed _minterAddress,
                  string _minterType
              );
              /**
               * @notice Any active minter removed for project `_projectId`.
               */
              event ProjectMinterRemoved(uint256 indexed _projectId);
              function genArt721CoreAddress() external returns (address);
              function setMinterForProject(uint256, address) external;
              function removeMinterForProject(uint256) external;
              function mint(
                  address _to,
                  uint256 _projectId,
                  address sender
              ) external returns (uint256);
              function getMinterForProject(uint256) external view returns (address);
              function projectHasMinter(uint256) external view returns (bool);
          }
          // SPDX-License-Identifier: LGPL-3.0-only
          // Created By: Art Blocks Inc.
          pragma solidity ^0.8.0;
          interface IFilteredMinterV0 {
              /**
               * @notice Price per token in wei updated for project `_projectId` to
               * `_pricePerTokenInWei`.
               */
              event PricePerTokenInWeiUpdated(
                  uint256 indexed _projectId,
                  uint256 indexed _pricePerTokenInWei
              );
              /**
               * @notice Currency updated for project `_projectId` to symbol
               * `_currencySymbol` and address `_currencyAddress`.
               */
              event ProjectCurrencyInfoUpdated(
                  uint256 indexed _projectId,
                  address indexed _currencyAddress,
                  string _currencySymbol
              );
              /// togglePurchaseToDisabled updated
              event PurchaseToDisabledUpdated(
                  uint256 indexed _projectId,
                  bool _purchaseToDisabled
              );
              // getter function of public variable
              function minterType() external view returns (string memory);
              function genArt721CoreAddress() external returns (address);
              function minterFilterAddress() external returns (address);
              // Triggers a purchase of a token from the desired project, to the
              // TX-sending address.
              function purchase(uint256 _projectId)
                  external
                  payable
                  returns (uint256 tokenId);
              // Triggers a purchase of a token from the desired project, to the specified
              // receiving address.
              function purchaseTo(address _to, uint256 _projectId)
                  external
                  payable
                  returns (uint256 tokenId);
              // Toggles the ability for `purchaseTo` to be called directly with a
              // specified receiving address that differs from the TX-sending address.
              function togglePurchaseToDisabled(uint256 _projectId) external;
              // Called to make the minter contract aware of the max invocations for a
              // given project.
              function setProjectMaxInvocations(uint256 _projectId) external;
              // Gets if token price is configured, token price in wei, currency symbol,
              // and currency address, assuming this is project's minter.
              // Supersedes any defined core price.
              function getPriceInfo(uint256 _projectId)
                  external
                  view
                  returns (
                      bool isConfigured,
                      uint256 tokenPriceInWei,
                      string memory currencySymbol,
                      address currencyAddress
                  );
          }
          // SPDX-License-Identifier: LGPL-3.0-only
          // Created By: Art Blocks Inc.
          pragma solidity ^0.8.0;
          interface IAdminACLV0 {
              /**
               * @notice Token ID `_tokenId` minted to `_to`.
               * @param previousSuperAdmin The previous superAdmin address.
               * @param newSuperAdmin The new superAdmin address.
               * @param genArt721CoreAddressesToUpdate Array of genArt721Core
               * addresses to update to the new superAdmin, for indexing purposes only.
               */
              event SuperAdminTransferred(
                  address indexed previousSuperAdmin,
                  address indexed newSuperAdmin,
                  address[] genArt721CoreAddressesToUpdate
              );
              /// Type of the Admin ACL contract, e.g. "AdminACLV0"
              function AdminACLType() external view returns (string memory);
              /// super admin address
              function superAdmin() external view returns (address);
              /**
               * @notice Calls transferOwnership on other contract from this contract.
               * This is useful for updating to a new AdminACL contract.
               * @dev this function should be gated to only superAdmin-like addresses.
               */
              function transferOwnershipOn(address _contract, address _newAdminACL)
                  external;
              /**
               * @notice Calls renounceOwnership on other contract from this contract.
               * @dev this function should be gated to only superAdmin-like addresses.
               */
              function renounceOwnershipOn(address _contract) external;
              /**
               * @notice Checks if sender `_sender` is allowed to call function with selector
               * `_selector` on contract `_contract`.
               */
              function allowed(
                  address _sender,
                  address _contract,
                  bytes4 _selector
              ) external returns (bool);
          }
          // SPDX-License-Identifier: LGPL-3.0-only
          // Created By: Art Blocks Inc.
          pragma solidity ^0.8.0;
          import "./IAdminACLV0.sol";
          /// use the Royalty Registry's IManifold interface for token royalties
          import "./IManifold.sol";
          interface IGenArt721CoreContractV3 is IManifold {
              /**
               * @notice Token ID `_tokenId` minted to `_to`.
               */
              event Mint(address indexed _to, uint256 indexed _tokenId);
              /**
               * @notice currentMinter updated to `_currentMinter`.
               * @dev Implemented starting with V3 core
               */
              event MinterUpdated(address indexed _currentMinter);
              /**
               * @notice Platform updated on bytes32-encoded field `_field`.
               */
              event PlatformUpdated(bytes32 indexed _field);
              /**
               * @notice Project ID `_projectId` updated on bytes32-encoded field
               * `_update`.
               */
              event ProjectUpdated(uint256 indexed _projectId, bytes32 indexed _update);
              event ProposedArtistAddressesAndSplits(
                  uint256 indexed _projectId,
                  address _artistAddress,
                  address _additionalPayeePrimarySales,
                  uint256 _additionalPayeePrimarySalesPercentage,
                  address _additionalPayeeSecondarySales,
                  uint256 _additionalPayeeSecondarySalesPercentage
              );
              event AcceptedArtistAddressesAndSplits(uint256 indexed _projectId);
              // version and type of the core contract
              // coreVersion is a string of the form "0.x.y"
              function coreVersion() external view returns (string memory);
              // coreType is a string of the form "GenArt721CoreV3"
              function coreType() external view returns (string memory);
              // owner (pre-V3 was named admin) of contract
              // this is expected to be an Admin ACL contract for V3
              function owner() external view returns (address);
              // Admin ACL contract for V3, will be at the address owner()
              function adminACLContract() external returns (IAdminACLV0);
              // backwards-compatible (pre-V3) admin - equal to owner()
              function admin() external view returns (address);
              /**
               * Function determining if _sender is allowed to call function with
               * selector _selector on contract `_contract`. Intended to be used with
               * peripheral contracts such as minters, as well as internally by the
               * core contract itself.
               */
              function adminACLAllowed(
                  address _sender,
                  address _contract,
                  bytes4 _selector
              ) external returns (bool);
              // getter function of public variable
              function nextProjectId() external view returns (uint256);
              // getter function of public mapping
              function tokenIdToProjectId(uint256 tokenId)
                  external
                  view
                  returns (uint256 projectId);
              // @dev this is not available in V0
              function isMintWhitelisted(address minter) external view returns (bool);
              function projectIdToArtistAddress(uint256 _projectId)
                  external
                  view
                  returns (address payable);
              function projectIdToAdditionalPayeePrimarySales(uint256 _projectId)
                  external
                  view
                  returns (address payable);
              function projectIdToAdditionalPayeePrimarySalesPercentage(
                  uint256 _projectId
              ) external view returns (uint256);
              // @dev new function in V3
              function getPrimaryRevenueSplits(uint256 _projectId, uint256 _price)
                  external
                  view
                  returns (
                      uint256 artblocksRevenue_,
                      address payable artblocksAddress_,
                      uint256 artistRevenue_,
                      address payable artistAddress_,
                      uint256 additionalPayeePrimaryRevenue_,
                      address payable additionalPayeePrimaryAddress_
                  );
              // @dev new function in V3
              function projectStateData(uint256 _projectId)
                  external
                  view
                  returns (
                      uint256 invocations,
                      uint256 maxInvocations,
                      bool active,
                      bool paused,
                      uint256 completedTimestamp,
                      bool locked
                  );
              // @dev Art Blocks primary sales payment address
              function artblocksPrimarySalesAddress()
                  external
                  view
                  returns (address payable);
              /**
               * @notice Backwards-compatible (pre-V3) function returning Art Blocks
               * primary sales payment address (now called artblocksPrimarySalesAddress).
               */
              function artblocksAddress() external view returns (address payable);
              // @dev Percentage of primary sales allocated to Art Blocks
              function artblocksPrimarySalesPercentage() external view returns (uint256);
              /**
               * @notice Backwards-compatible (pre-V3) function returning Art Blocks
               * primary sales percentage (now called artblocksPrimarySalesPercentage).
               */
              function artblocksPercentage() external view returns (uint256);
              // @dev Art Blocks secondary sales royalties payment address
              function artblocksSecondarySalesAddress()
                  external
                  view
                  returns (address payable);
              // @dev Basis points of secondary sales allocated to Art Blocks
              function artblocksSecondarySalesBPS() external view returns (uint256);
              // function to set a token's hash (must be guarded)
              function setTokenHash_8PT(uint256 _tokenId, bytes32 _hash) external;
              // @dev gas-optimized signature in V3 for `mint`
              function mint_Ecf(
                  address _to,
                  uint256 _projectId,
                  address _by
              ) external returns (uint256 tokenId);
              /**
               * @notice Backwards-compatible (pre-V3) function  that gets artist +
               * artist's additional payee royalty data for token ID `_tokenId`.
               * WARNING: Does not include Art Blocks portion of royalties.
               */
              function getRoyaltyData(uint256 _tokenId)
                  external
                  view
                  returns (
                      address artistAddress,
                      address additionalPayee,
                      uint256 additionalPayeePercentage,
                      uint256 royaltyFeeByID
                  );
          }
          // SPDX-License-Identifier: MIT
          // OpenZeppelin Contracts v4.4.1 (utils/structs/EnumerableMap.sol)
          pragma solidity ^0.8.0;
          import "./EnumerableSet.sol";
          /**
           * @dev Library for managing an enumerable variant of Solidity's
           * https://solidity.readthedocs.io/en/latest/types.html#mapping-types[`mapping`]
           * type.
           *
           * Maps have the following properties:
           *
           * - Entries are added, removed, and checked for existence in constant time
           * (O(1)).
           * - Entries are enumerated in O(n). No guarantees are made on the ordering.
           *
           * ```
           * contract Example {
           *     // Add the library methods
           *     using EnumerableMap for EnumerableMap.UintToAddressMap;
           *
           *     // Declare a set state variable
           *     EnumerableMap.UintToAddressMap private myMap;
           * }
           * ```
           *
           * As of v3.0.0, only maps of type `uint256 -> address` (`UintToAddressMap`) are
           * supported.
           */
          library EnumerableMap {
              using EnumerableSet for EnumerableSet.Bytes32Set;
              // To implement this library for multiple types with as little code
              // repetition as possible, we write it in terms of a generic Map type with
              // bytes32 keys and values.
              // The Map implementation uses private functions, and user-facing
              // implementations (such as Uint256ToAddressMap) are just wrappers around
              // the underlying Map.
              // This means that we can only create new EnumerableMaps for types that fit
              // in bytes32.
              struct Map {
                  // Storage of keys
                  EnumerableSet.Bytes32Set _keys;
                  mapping(bytes32 => bytes32) _values;
              }
              /**
               * @dev Adds a key-value pair to a map, or updates the value for an existing
               * key. O(1).
               *
               * Returns true if the key was added to the map, that is if it was not
               * already present.
               */
              function _set(
                  Map storage map,
                  bytes32 key,
                  bytes32 value
              ) private returns (bool) {
                  map._values[key] = value;
                  return map._keys.add(key);
              }
              /**
               * @dev Removes a key-value pair from a map. O(1).
               *
               * Returns true if the key was removed from the map, that is if it was present.
               */
              function _remove(Map storage map, bytes32 key) private returns (bool) {
                  delete map._values[key];
                  return map._keys.remove(key);
              }
              /**
               * @dev Returns true if the key is in the map. O(1).
               */
              function _contains(Map storage map, bytes32 key) private view returns (bool) {
                  return map._keys.contains(key);
              }
              /**
               * @dev Returns the number of key-value pairs in the map. O(1).
               */
              function _length(Map storage map) private view returns (uint256) {
                  return map._keys.length();
              }
              /**
               * @dev Returns the key-value pair stored at position `index` in the map. O(1).
               *
               * Note that there are no guarantees on the ordering of entries inside the
               * array, and it may change when more entries are added or removed.
               *
               * Requirements:
               *
               * - `index` must be strictly less than {length}.
               */
              function _at(Map storage map, uint256 index) private view returns (bytes32, bytes32) {
                  bytes32 key = map._keys.at(index);
                  return (key, map._values[key]);
              }
              /**
               * @dev Tries to returns the value associated with `key`.  O(1).
               * Does not revert if `key` is not in the map.
               */
              function _tryGet(Map storage map, bytes32 key) private view returns (bool, bytes32) {
                  bytes32 value = map._values[key];
                  if (value == bytes32(0)) {
                      return (_contains(map, key), bytes32(0));
                  } else {
                      return (true, value);
                  }
              }
              /**
               * @dev Returns the value associated with `key`.  O(1).
               *
               * Requirements:
               *
               * - `key` must be in the map.
               */
              function _get(Map storage map, bytes32 key) private view returns (bytes32) {
                  bytes32 value = map._values[key];
                  require(value != 0 || _contains(map, key), "EnumerableMap: nonexistent key");
                  return value;
              }
              /**
               * @dev Same as {_get}, with a custom error message when `key` is not in the map.
               *
               * CAUTION: This function is deprecated because it requires allocating memory for the error
               * message unnecessarily. For custom revert reasons use {_tryGet}.
               */
              function _get(
                  Map storage map,
                  bytes32 key,
                  string memory errorMessage
              ) private view returns (bytes32) {
                  bytes32 value = map._values[key];
                  require(value != 0 || _contains(map, key), errorMessage);
                  return value;
              }
              // UintToAddressMap
              struct UintToAddressMap {
                  Map _inner;
              }
              /**
               * @dev Adds a key-value pair to a map, or updates the value for an existing
               * key. O(1).
               *
               * Returns true if the key was added to the map, that is if it was not
               * already present.
               */
              function set(
                  UintToAddressMap storage map,
                  uint256 key,
                  address value
              ) internal returns (bool) {
                  return _set(map._inner, bytes32(key), bytes32(uint256(uint160(value))));
              }
              /**
               * @dev Removes a value from a set. O(1).
               *
               * Returns true if the key was removed from the map, that is if it was present.
               */
              function remove(UintToAddressMap storage map, uint256 key) internal returns (bool) {
                  return _remove(map._inner, bytes32(key));
              }
              /**
               * @dev Returns true if the key is in the map. O(1).
               */
              function contains(UintToAddressMap storage map, uint256 key) internal view returns (bool) {
                  return _contains(map._inner, bytes32(key));
              }
              /**
               * @dev Returns the number of elements in the map. O(1).
               */
              function length(UintToAddressMap storage map) internal view returns (uint256) {
                  return _length(map._inner);
              }
              /**
               * @dev Returns the element stored at position `index` in the set. O(1).
               * Note that there are no guarantees on the ordering of values inside the
               * array, and it may change when more values are added or removed.
               *
               * Requirements:
               *
               * - `index` must be strictly less than {length}.
               */
              function at(UintToAddressMap storage map, uint256 index) internal view returns (uint256, address) {
                  (bytes32 key, bytes32 value) = _at(map._inner, index);
                  return (uint256(key), address(uint160(uint256(value))));
              }
              /**
               * @dev Tries to returns the value associated with `key`.  O(1).
               * Does not revert if `key` is not in the map.
               *
               * _Available since v3.4._
               */
              function tryGet(UintToAddressMap storage map, uint256 key) internal view returns (bool, address) {
                  (bool success, bytes32 value) = _tryGet(map._inner, bytes32(key));
                  return (success, address(uint160(uint256(value))));
              }
              /**
               * @dev Returns the value associated with `key`.  O(1).
               *
               * Requirements:
               *
               * - `key` must be in the map.
               */
              function get(UintToAddressMap storage map, uint256 key) internal view returns (address) {
                  return address(uint160(uint256(_get(map._inner, bytes32(key)))));
              }
              /**
               * @dev Same as {get}, with a custom error message when `key` is not in the map.
               *
               * CAUTION: This function is deprecated because it requires allocating memory for the error
               * message unnecessarily. For custom revert reasons use {tryGet}.
               */
              function get(
                  UintToAddressMap storage map,
                  uint256 key,
                  string memory errorMessage
              ) internal view returns (address) {
                  return address(uint160(uint256(_get(map._inner, bytes32(key), errorMessage))));
              }
          }
          // SPDX-License-Identifier: MIT
          pragma solidity ^0.8.0;
          /// @dev Royalty Registry interface, used to support the Royalty Registry.
          /// @dev Source: https://github.com/manifoldxyz/royalty-registry-solidity/blob/main/contracts/specs/IManifold.sol
          /// @author: manifold.xyz
          /**
           * @dev Royalty interface for creator core classes
           */
          interface IManifold {
              /**
               * @dev Get royalites of a token.  Returns list of receivers and basisPoints
               *
               *  bytes4(keccak256('getRoyalties(uint256)')) == 0xbb3bafd6
               *
               *  => 0xbb3bafd6 = 0xbb3bafd6
               */
              function getRoyalties(uint256 tokenId)
                  external
                  view
                  returns (address payable[] memory, uint256[] memory);
          }
          // SPDX-License-Identifier: MIT
          // OpenZeppelin Contracts v4.4.1 (utils/structs/EnumerableSet.sol)
          pragma solidity ^0.8.0;
          /**
           * @dev Library for managing
           * https://en.wikipedia.org/wiki/Set_(abstract_data_type)[sets] of primitive
           * types.
           *
           * Sets have the following properties:
           *
           * - Elements are added, removed, and checked for existence in constant time
           * (O(1)).
           * - Elements are enumerated in O(n). No guarantees are made on the ordering.
           *
           * ```
           * contract Example {
           *     // Add the library methods
           *     using EnumerableSet for EnumerableSet.AddressSet;
           *
           *     // Declare a set state variable
           *     EnumerableSet.AddressSet private mySet;
           * }
           * ```
           *
           * As of v3.3.0, sets of type `bytes32` (`Bytes32Set`), `address` (`AddressSet`)
           * and `uint256` (`UintSet`) are supported.
           */
          library EnumerableSet {
              // To implement this library for multiple types with as little code
              // repetition as possible, we write it in terms of a generic Set type with
              // bytes32 values.
              // The Set implementation uses private functions, and user-facing
              // implementations (such as AddressSet) are just wrappers around the
              // underlying Set.
              // This means that we can only create new EnumerableSets for types that fit
              // in bytes32.
              struct Set {
                  // Storage of set values
                  bytes32[] _values;
                  // Position of the value in the `values` array, plus 1 because index 0
                  // means a value is not in the set.
                  mapping(bytes32 => uint256) _indexes;
              }
              /**
               * @dev Add a value to a set. O(1).
               *
               * Returns true if the value was added to the set, that is if it was not
               * already present.
               */
              function _add(Set storage set, bytes32 value) private returns (bool) {
                  if (!_contains(set, value)) {
                      set._values.push(value);
                      // The value is stored at length-1, but we add 1 to all indexes
                      // and use 0 as a sentinel value
                      set._indexes[value] = set._values.length;
                      return true;
                  } else {
                      return false;
                  }
              }
              /**
               * @dev Removes a value from a set. O(1).
               *
               * Returns true if the value was removed from the set, that is if it was
               * present.
               */
              function _remove(Set storage set, bytes32 value) private returns (bool) {
                  // We read and store the value's index to prevent multiple reads from the same storage slot
                  uint256 valueIndex = set._indexes[value];
                  if (valueIndex != 0) {
                      // Equivalent to contains(set, value)
                      // To delete an element from the _values array in O(1), we swap the element to delete with the last one in
                      // the array, and then remove the last element (sometimes called as 'swap and pop').
                      // This modifies the order of the array, as noted in {at}.
                      uint256 toDeleteIndex = valueIndex - 1;
                      uint256 lastIndex = set._values.length - 1;
                      if (lastIndex != toDeleteIndex) {
                          bytes32 lastvalue = set._values[lastIndex];
                          // Move the last value to the index where the value to delete is
                          set._values[toDeleteIndex] = lastvalue;
                          // Update the index for the moved value
                          set._indexes[lastvalue] = valueIndex; // Replace lastvalue's index to valueIndex
                      }
                      // Delete the slot where the moved value was stored
                      set._values.pop();
                      // Delete the index for the deleted slot
                      delete set._indexes[value];
                      return true;
                  } else {
                      return false;
                  }
              }
              /**
               * @dev Returns true if the value is in the set. O(1).
               */
              function _contains(Set storage set, bytes32 value) private view returns (bool) {
                  return set._indexes[value] != 0;
              }
              /**
               * @dev Returns the number of values on the set. O(1).
               */
              function _length(Set storage set) private view returns (uint256) {
                  return set._values.length;
              }
              /**
               * @dev Returns the value stored at position `index` in the set. O(1).
               *
               * Note that there are no guarantees on the ordering of values inside the
               * array, and it may change when more values are added or removed.
               *
               * Requirements:
               *
               * - `index` must be strictly less than {length}.
               */
              function _at(Set storage set, uint256 index) private view returns (bytes32) {
                  return set._values[index];
              }
              /**
               * @dev Return the entire set in an array
               *
               * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
               * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
               * this function has an unbounded cost, and using it as part of a state-changing function may render the function
               * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
               */
              function _values(Set storage set) private view returns (bytes32[] memory) {
                  return set._values;
              }
              // Bytes32Set
              struct Bytes32Set {
                  Set _inner;
              }
              /**
               * @dev Add a value to a set. O(1).
               *
               * Returns true if the value was added to the set, that is if it was not
               * already present.
               */
              function add(Bytes32Set storage set, bytes32 value) internal returns (bool) {
                  return _add(set._inner, value);
              }
              /**
               * @dev Removes a value from a set. O(1).
               *
               * Returns true if the value was removed from the set, that is if it was
               * present.
               */
              function remove(Bytes32Set storage set, bytes32 value) internal returns (bool) {
                  return _remove(set._inner, value);
              }
              /**
               * @dev Returns true if the value is in the set. O(1).
               */
              function contains(Bytes32Set storage set, bytes32 value) internal view returns (bool) {
                  return _contains(set._inner, value);
              }
              /**
               * @dev Returns the number of values in the set. O(1).
               */
              function length(Bytes32Set storage set) internal view returns (uint256) {
                  return _length(set._inner);
              }
              /**
               * @dev Returns the value stored at position `index` in the set. O(1).
               *
               * Note that there are no guarantees on the ordering of values inside the
               * array, and it may change when more values are added or removed.
               *
               * Requirements:
               *
               * - `index` must be strictly less than {length}.
               */
              function at(Bytes32Set storage set, uint256 index) internal view returns (bytes32) {
                  return _at(set._inner, index);
              }
              /**
               * @dev Return the entire set in an array
               *
               * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
               * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
               * this function has an unbounded cost, and using it as part of a state-changing function may render the function
               * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
               */
              function values(Bytes32Set storage set) internal view returns (bytes32[] memory) {
                  return _values(set._inner);
              }
              // AddressSet
              struct AddressSet {
                  Set _inner;
              }
              /**
               * @dev Add a value to a set. O(1).
               *
               * Returns true if the value was added to the set, that is if it was not
               * already present.
               */
              function add(AddressSet storage set, address value) internal returns (bool) {
                  return _add(set._inner, bytes32(uint256(uint160(value))));
              }
              /**
               * @dev Removes a value from a set. O(1).
               *
               * Returns true if the value was removed from the set, that is if it was
               * present.
               */
              function remove(AddressSet storage set, address value) internal returns (bool) {
                  return _remove(set._inner, bytes32(uint256(uint160(value))));
              }
              /**
               * @dev Returns true if the value is in the set. O(1).
               */
              function contains(AddressSet storage set, address value) internal view returns (bool) {
                  return _contains(set._inner, bytes32(uint256(uint160(value))));
              }
              /**
               * @dev Returns the number of values in the set. O(1).
               */
              function length(AddressSet storage set) internal view returns (uint256) {
                  return _length(set._inner);
              }
              /**
               * @dev Returns the value stored at position `index` in the set. O(1).
               *
               * Note that there are no guarantees on the ordering of values inside the
               * array, and it may change when more values are added or removed.
               *
               * Requirements:
               *
               * - `index` must be strictly less than {length}.
               */
              function at(AddressSet storage set, uint256 index) internal view returns (address) {
                  return address(uint160(uint256(_at(set._inner, index))));
              }
              /**
               * @dev Return the entire set in an array
               *
               * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
               * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
               * this function has an unbounded cost, and using it as part of a state-changing function may render the function
               * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
               */
              function values(AddressSet storage set) internal view returns (address[] memory) {
                  bytes32[] memory store = _values(set._inner);
                  address[] memory result;
                  assembly {
                      result := store
                  }
                  return result;
              }
              // UintSet
              struct UintSet {
                  Set _inner;
              }
              /**
               * @dev Add a value to a set. O(1).
               *
               * Returns true if the value was added to the set, that is if it was not
               * already present.
               */
              function add(UintSet storage set, uint256 value) internal returns (bool) {
                  return _add(set._inner, bytes32(value));
              }
              /**
               * @dev Removes a value from a set. O(1).
               *
               * Returns true if the value was removed from the set, that is if it was
               * present.
               */
              function remove(UintSet storage set, uint256 value) internal returns (bool) {
                  return _remove(set._inner, bytes32(value));
              }
              /**
               * @dev Returns true if the value is in the set. O(1).
               */
              function contains(UintSet storage set, uint256 value) internal view returns (bool) {
                  return _contains(set._inner, bytes32(value));
              }
              /**
               * @dev Returns the number of values on the set. O(1).
               */
              function length(UintSet storage set) internal view returns (uint256) {
                  return _length(set._inner);
              }
              /**
               * @dev Returns the value stored at position `index` in the set. O(1).
               *
               * Note that there are no guarantees on the ordering of values inside the
               * array, and it may change when more values are added or removed.
               *
               * Requirements:
               *
               * - `index` must be strictly less than {length}.
               */
              function at(UintSet storage set, uint256 index) internal view returns (uint256) {
                  return uint256(_at(set._inner, index));
              }
              /**
               * @dev Return the entire set in an array
               *
               * WARNING: This operation will copy the entire storage to memory, which can be quite expensive. This is designed
               * to mostly be used by view accessors that are queried without any gas fees. Developers should keep in mind that
               * this function has an unbounded cost, and using it as part of a state-changing function may render the function
               * uncallable if the set grows to a point where copying to memory consumes too much gas to fit in a block.
               */
              function values(UintSet storage set) internal view returns (uint256[] memory) {
                  bytes32[] memory store = _values(set._inner);
                  uint256[] memory result;
                  assembly {
                      result := store
                  }
                  return result;
              }
          }