๐Ÿ”ฌ
Molecule Docs
  • Introduction
    • ๐Ÿ”ฌWhat is Molecule?
    • ๐Ÿ’กWhy Molecule?
  • Proof of Invention (PoI)
    • ๐Ÿ’กIntro to Proof of Invention (PoI)
    • ๐Ÿ’ฎHow to register inventions?
    • โš™๏ธAPI Access (Beta)
  • MOLECULE LABS
    • ๐ŸงชIntro to Molecule Labs
    • ๐Ÿง‘โ€๐Ÿ”ฌSubmit a Project
    • ๐Ÿ“How is data stored?
  • IP-NFTs
    • โšกIntro to IP-NFT
    • ๐Ÿ›๏ธIP-NFT legal structure
    • โ“Why mint an IP-NFT?
    • โœจHow to mint an IP-NFT?
    • ๐Ÿ› ๏ธTechnical Components of IP-NFTs
      • โš™๏ธTechnical Details for Developers
      • ๐Ÿ“ญSmart Contract Addresses
  • ๐Ÿง‘โ€๐Ÿ”ฌIP Tokens
    • ๐Ÿ’ŠWhat are IP Tokens?
    • โœจHow to tokenize IPTs from an IP-NFT?
    • โš–๏ธWhat are risks of IP-NFTs and IPTs?
    • ๐Ÿ‘จโ€๐Ÿ‘ฉโ€๐Ÿ‘งโ€๐Ÿ‘ฆWhat is a crowdsale of IPTs?
    • โœจHow to participate in a crowdsale of IPTs?
  • IP Tokenization Guide
    • ๐Ÿ—บ๏ธIP Tokenization Guide
    • ๐ŸฆWallets
Powered by GitBook
On this page
  • Non Fungible Tokens in a Nutshell
  • Reserve an IP-NFT Token ID
  • Assemble and Upload Metadata
  • Validate Metadata Correctness
  • Link to External Resources
  • Decentralized Encryption and IP-NFT Token Gating
  • Using Multisig Wallet Signers
  • Granting Read Access to Third Parties
  • Proving Content Integrity
  • Terms and Validation signatures
  • Sign off legal terms and agreements' content
  • Verify and sign off final metadata
  • Call the mint function
  • Putting it all together: The IP-NFT Minting Flow
  1. IP-NFTs
  2. Technical Components of IP-NFTs

Technical Details for Developers

Background information and code examples for implementers who want to interact with the IP-NFT protocol in code.

PreviousTechnical Components of IP-NFTsNextSmart Contract Addresses

Last updated 1 year ago

You can find all relevant IP-NFT contract sources on our official . Runnable versions of the code samples depicted in this article can be found in the accompanying .

Non Fungible Tokens in a Nutshell

Non fungible tokens () are smart contract based assets that associate a unique token identifier with the blockchain address of its respective owner. The underlying smart contract defines rules on how they are minted (brought into existence), transferred, or burned (destroyed). It also can restrict their ability to be transferred or offer features that are unlocked for individual token holders.

IP-NFTs use the common NFT standard with minting, burning and metadata extensions. The IP-NFT collection contract is deployed on Mainnet and on Gรถrli Testnet, you can find the addresses . Each token's metadata is stored as a file descriptor URI (e.g. ar://HxXKCIE0skR4siRNYeLKI61Vwg_TJ5PJTbxQmtO0EPo) that must be resolved client side, e.g. by using decentralized storage network gateways ( or ). The contract is non-enumerable, i.e. users can't simply query their owned assets on-chain but instead must rely on reading the respective event logs to build their own off-chain state. We've deployed TheGraph subgraphs and that can be queried for asset ownership and other IP-NFT related information.

Read more about and from your application.

Here's a GraphQL example of how to query IP-NFT data from the subgraph:

query IPNFTsOf($owner: ID!)
{
  ipnfts(where: {owner: $owner}) {
    id
    owner
    createdAt
    tokenURI
  }
}

Variables:

{
  "owner": "0xf7990cd398dafb4fe5fd6b9228b8e6f72b296555"
}

Result:

{
  "data": {
    "ipnfts": [
      {
        "id": "2",
        "owner": "0xf7990cd398dafb4fe5fd6b9228b8e6f72b296555",
        "createdAt": "1686582803",
        "tokenURI": "ipfs://bafkreig274nfj7srmtnb5wd5wlwm3ig2s63wovlz7i3noodjlfz2tm3n5q"
      }
    ]
  }
}

Reserve an IP-NFT Token ID

IP-NFTs can generally be minted by any account. The first step on the minting journey is to reserve an IP-NFT token id by calling the IP-NFT contract's reserve() method. This capturing step is necessary because the legal documents attached to the final IP-NFT are referring to the NFT's token id that only becomes available after the mint has occurred. Minters will use the token id to craft the legal documents that outline the rights and obligations of owning that NFT in the real world.

Assemble and Upload Metadata

{
  "schema_version": "0.0.1",
  "name": "Our awesome test IP-NFT",
  "description": "Lorem ipsum dolor sit amet, ...",
  "image": "ar://7De6dRLDaMhMeC6Utm9bB9PRbcvKdi-rw_sDM8pJSMU",
  "external_url": "https://testnet.mint.molecule.to/ipnft/76",
  "terms_signature": "0x...",
  "properties": {
    "type": "IP-NFT",
    "agreements": [
      {
        "type": "License Agreement",
        "url": "ar://4FG3GR8qCdLo923tVBC85NTYHaaAc3TvCsF3aZwum_o",
        "mime_type": "application/pdf",
        "content_hash": "bagaaiera7ftqs3jmnoph3zgq67bgjrszlqtxkk5ygadgjvnihukrqioipndq",
      }
    ],
    "project_details": {
      "industry": "Space Exploration",
      "organization": "NASA",
      "topic": "Wormholes",
      "funding_amount": {
        "value": 123456789,
        "decimals": 2,
        "currency": "USD",
        "currency_type": "ISO4217"
      },
      "research_lead": {
        "name": "Carl Sagan",
        "email": "carl@example.com"
      }
    }
  }
}

Validate Metadata Correctness

import Ajv from "ajv";
import addFormats from "ajv-formats";

(async () => {
  const ipnftSchema = JSON.parse(ipnftSchemaJson);
  const document = JSON.parse(ipnftMetadataJson);

  const ajv = new Ajv();
  addFormats(ajv);
  const validateIpnft = ajv.compile(ipnftSchema);
  const validationResult = validateIpnft(document);  
})();

Link to External Resources

import dotenv from "dotenv";
import { Blob } from "node:buffer";
import { Web3Storage } from "web3.storage";
dotenv.config();

const w3sClient = new Web3Storage({ token: process.env.W3S_TOKEN });

(async () => {
  const content = {
    uploaded_at: new Date().toISOString(),
  };

  const binaryContent = new Blob([JSON.stringify(content)], {
    type: "application/json",
  });

  const file = {
    name: "filename.json",
    stream: () => binaryContent.stream(),
  };

  //@ts-ignore
  const cid = await w3sClient.put([file], {
    name: "some.json",
    wrapWithDirectory: false,
  });
  console.log(cid);
})();

results in an IPFS CIDv1:

bafkreicrhuxfzrydht6tmd4kyy6pkbhspqthswg6xbiqlaztmf774ojxhq
{"uploaded_at":"2023-01-02T22:45:17.788Z"}

Decentralized Encryption and IP-NFT Token Gating

Lit runs a network of nodes that derive signing and encryption keys by multiparty computation / threshold cryptography on trusted computing enclaves. The nodes themselves only know parts of the private key that effectively is fully assembled on the client side after all conditions for key retrieval have been met. Lit protocol allows gating any content behind access control conditions that are backed by blockchain state, therefore it's disclosing decryption keys only to holders of an NFT or users that meet a certain condition on chain.

  import LitJsSdk from '@lit-protocol/sdk-browser'
  
  const litClient = new LitJsSdk.LitNodeClient()
  await client.connect() //connects to Lit network nodes

  const file: Blob | File = //some file
  const { encryptedFile, symmetricKey } = await LitJsSdk.encryptFile({ file })

  //you can reuse a Siwe / EIP-4361 compatible signed message here, see https://login.xyz/
  const authSig = await LitJsSdk.checkAndSignAuthMessage({ chain: "ethereum" });

  accessControlConditions = {
      conditionType: 'evmBasic',
      contractAddress: tokenContractAddress,
      standardContractType: 'ERC721',
      chain: "ethereum",
      method: 'ownerOf',
      parameters: [tokenId],
      returnValueTest: {
        comparator: '=',
        value: ':userAddress'
      }
    }

  const u8encryptedSymmetricKey = await litClient.saveEncryptionKey({
    unifiedAccessControlConditions: accessControlConditions,
    symmetricKey,
    authSig,
    chain,
    permanent: false
  })
  
  

A user who wants to decrypt the content must again initialize the Lit SDK using a signed message that proves control over their address. Next, they ask several network nodes to present their key shares of the encrypted key by presenting the access control conditions and the encrypted symmetric key to the network. If the network nodes find that the account matches the given conditions, each one yields its key share for the encrypted decryption key. With that, the SDK decrypts the key the content has initially been encrypted with.

const authSig = await LitJsSdk.checkAndSignAuthMessage({ chain: "ethereum" });
const symmetricKey = await litClient.getEncryptionKey({
    unifiedAccessControlConditions: accessControlConditions,
    toDecrypt: encryptedSymmetricKey,
    chain: "ethereum",
    authSig
})
"agreements": [
  {
    "type": "License Agreement",
    "url": "ar://4FG3GR8qCdLo923tVBC85NTYHaaAc3TvCsF3aZwum_o",
    "mime_type": "application/pdf",
    "content_hash": "bagaaiera7ftqs3jmnoph3zgq67bgjrszlqtxkk5ygadgjvnihukrqioipndq",
    "encryption": {
      "protocol": "lit",
      "encrypted_sym_key": "fcbeaf3af31c7104d1d1f7099a6c6f6fda5803a4f7a0bef93256f3377450291872ad07bed3e9402cb47cc932c8f48219e56b84c06becd5ec0ee83ef2c0c93b932fb675c7932fa8df0ad164f17642b32415e382081577a403c19da2eff22c9083fa134ad1f370c2bec449adcdea790498637c7238b7d67cf2d69507a962656d3200000000000000205e4ad4e6323e06862babc934f740bc2e566d175a5da23bb4f1b35635e5cc3768cd040e8776307ea038484ff42033c18f",
      "access_control_conditions": [
        {
          "conditionType": "evmBasic",
          "contractAddress": "0xaf7358576C9F7cD84696D28702fC5ADe33cce0e9",
          "standardContractType": "ERC721",
          "chain": "goerli",
          "method": "ownerOf",
          "parameters": [
            "6"
          ],
          "returnValueTest": {
            "comparator": "=",
            "value": ":userAddress"
          }
        }
      ]
    }
  }
]

Using Multisig Wallet Signers

The recommended workaround is to denote a dedicated trusted member of the multisig that's supposed to intially own the minted IP-NFT. This could be the researcher, a core contributor or maintainer of the project. The IP-NFT contract's mintReservation function takes a recipient parameter (to) that defines the NFT's initial owner. Note, that the account that invokes the mint function is required to hold a mint pass, not the receiver.

Granting Read Access to Third Parties

"encryption": {
  "protocol": "lit",
  "encrypted_sym_key": "...",
  "access_control_conditions": [
    {
      "conditionType": "evmContract",
      //the IP-NFT UUPS proxy contract address
      "contractAddress": "0xaf7358576C9F7cD84696D28702fC5ADe33cce0e",
      "chain": "goerli",
      "functionName": "canRead",
      "functionParams": [
        ":userAddress",
        "10"
      ],
      "functionAbi": {
        "name": "canRead",
        "inputs": [
          {
            "internalType": "address",
            "name": "reader",
            "type": "address"
          },
          {
            "internalType": "uint256",
            "name": "tokenId",
            "type": "uint256"
          }
        ],
        "outputs": [
          {
            "internalType": "bool",
            "name": "",
            "type": "bool"
          }
        ],
        "stateMutability": "view",
        "type": "function"
      },
      "returnValueTest": {
        "key": "",
        "comparator": "=",
        "value": "true"
      }
    }
  ]
}

Proving Content Integrity

import { CID } from "multiformats/cid";
import * as json from "multiformats/codecs/json";
import { sha256 } from "multiformats/hashes/sha2";

const checksum = async (u8: Uint8Array) => {
  //https://multiformats.io/multihash/
  const digest = await sha256.digest(u8);
  return CID.create(1, json.code, digest);
};

const verifyChecksum = async (
  u8: Uint8Array,
  _cid: string
): Promise<boolean> => {
  const cid = CID.parse(_cid);
  //https://github.com/multiformats/multicodec/blob/master/table.csv#L9
  console.log("hash algo used: 0x%s", cid.multihash.code.toString(16));

  const digest = await sha256.digest(u8);
  return cid.multihash.bytes.every((el, i) => el === digest.bytes[i]);
};

(async () => {
  const binaryContent = new TextEncoder().encode("This is the content");
  const cid = await checksum(binaryContent);
  const valid = await verifyChecksum(binaryContent, cid.toString());
  console.log(cid, valid);
})();

Terms and Validation signatures

Sign off legal terms and agreements' content

A TermsSig V1 contains the following information

  • a "banner"

  • a list of terms the minter has agreed to sign

  • a list of document hashes, keyed by their document type

  • a version indicator

  • the chain id on which this signature should be considered valid

Example:

I accept the IP-NFT minting terms

I have read and agreed to the terms of the IP-NFT Assignment Agreement
I understand that the IP-NFT represents legal rights to IP and data of the project in my Research Agreement
I understand that this in an irreversible and publicly traceable transaction on the Ethereum Blockchain
I understand this is beta software and agree to the Terms of Service and assume all risks of minting this IP-NFT

Sponsored Research Agreement Hash: bagaaiera7ftqs3jmnoph3zgq67bgjrszlqtxkk5ygadgjvnihukrqioipndq
Assignment Agreement Hash: bagaaiera7ftqs3jmnoph3zgq67bgjrszlqtxkk5ygadgjvnihukrqioipndq

Version: 1
Chain ID: 5

Verify and sign off final metadata

There's no way for the IP-NFT contract to verify that the metadata one provides is valid. To ensure its formal validity and completeness, we're therefore running a service that's signing off valid metadata with a signature that must be presented to the mintReservation function. These are the accounts and service endpoints of signers trusted by the IP-NFT contracts:

Here's a sample request

POST https://mint.molecule.to/api/signoffMetadata
Content-Type: application/json

{
  "network": "homestead",
  "minter": "0xcB0b20A55e188eD52F1af694e2a3bCb9e81e3911",
  "to": "0xD920E60b798A2F5a8332799d8a23075c9E77d5F8",
  "reservationId": 2,
  "tokenURI": "ipfs://bafkreibkvbe5n4iub7jac4cuhlokmdpt6fkv64x4huhy3gvgpbjf6ggzpa"
}
  • network the network alias the IP-NFT is being minted on (homestead for mainnet)

  • minter the minter's account that has signed the terms included in the metadata

  • tokenURI the final, resolvable metadata URI

  • to the minting recipient address

  • reservationId the reservation id that's going to minted (becomes the IP-NFT id after mint)

The signoff service will

  • download the metadata from the provided tokenURI (which proves its availability)

  • check whether the metadata conforms to the IP-NFT metadata schema

  • checks whether the metadata's terms_signature recovers to the minter address

If that's the case, it will sign keccak256(minter, to, reservationId, tokenURI) with an account the IP-NFT contract accepts as verifier and yield it as response.

{
  "status": "ok",
  "tokenURI": "ipfs://bafkreibkvbe5n4iub7jac4cuhlokmdpt6fkv64x4huhy3gvgpbjf6ggzpa",
  "authorization": "0x171e5282e55f178eb384723d6b9e52181cab154ecf399ff1940211a1b80f1c7f0590103a49ea47dca7c4d7667a7ab640f3e836141b32c06aeb8819daec2d673c1c",
  "signer": "0x3d30452c48f2448764d5819a9a2b684ae2cc5acf"
}

The response's authorization signature bytes are then provided as the IP-NFTs mintReservation function's authorization parameter

Call the mint function

The remaining final step to mint an IP-NFT is calling the smart contract's mintReservation function:

function mintReservation(
  address to, 
  uint256 reservationId, 
  string calldata _tokenURI, 
  string calldata _symbol, 
  bytes calldata authorization
) external payable override whenNotPaused returns (uint256)

with the parameters:

  • to the recipient of the new IP-NFT (e.g. a multisig wallet)

  • reservationId the reserved IP-NFT id as received by the initial call to reserve()

  • _tokenURI the URI that resolves to the metadata

  • _symbol a short symbol that identifies the IP-NFT and its derivatives

  • authorization a bytes encoded signature by the validation service

and a value of 0.001 etherthat deals as symbolic minting fee.

Putting it all together: The IP-NFT Minting Flow

To sum up, minting an IP-NFT technically requires the following steps:

  • invoke the IP-NFT contract's reserve() function to reserve a token id

    • get the reserved token id by parsing the method's event log or use the subgraph

  • upload an image to a (de)centralised network of your choice

  • use the token id and the IP-NFT contract's address to create legal documents

  • compute a checksum over the original documents

  • optionally encrypt the documents with a Lit access control condition

  • assemble a metadata structure containing the file pointers, access control conditions, encrypted symmetric key and checksum

  • craft a TermsSig message, sign it off by the minting account and add it to the metadata

  • verify that the metadata validates against the publicly known IPNFT schema

  • upload the metadata to a (de)centralised network of your choice

  • call the molecule validation service with your metadata URI to sign it off

  • invoke mintReservation on the IP-NFT contract

We've deployed the contracts as owned by the , thus the contract you're interacting with and the contract that contains the current logic are different. Make sure to always invoke functions on the UUPS proxy - its official addresses can be found . The implementation contracts have been verified on Etherscan, so you can .

When minting an IP-NFT using the molecule frontend, the reserved token id will be inserted in the autogenerated Assignment Agreement. If you're minting IP-NFTs on your own, make sure to correctly mention them in your legal attachments. A selection of premade contract templates for IP-NFTs can be found .

The JSON metadata documents behind IP-NFTs are required to strictly validate against a flexible enough to cover many relevant use cases. to investigate a valid IP-NFT's metadata interactively. Note that the generic fields name, image and description are located on the document's root level, whereas the agreements and project_details structures are modelled as rich property definitions.

To validate IP-NFT metatadata documents against that schema, you can use arbitrary JSON schema tools. is one of the most powerful ones in Javascript. We're omitting to retrieve schema or documents for brevity's sake but in a nutshell validation boils down to:

IP-NFT metadata documents require you to refer to external resources, e.g. the image or agreements[].url fields. While you can choose to go with the well known https:// protocol, it's advisable to use web3-native decentralized storage pointers like ar:// or ipfs:// instead. Clients are supposed to resolve them to their respective http gateway counterparts and most NFT related services and frontends can handle them. You don't need to run an Arweave or IPFS node yourself - or web3.storage are excellent helper services that get the job done and our official IP-NFT minting UI uses web3.storage to publish IPFS documents to a reliable storage backend and automatically create storage deals on Filecoin.

Here's a nodejs example on how to upload a JSON document using web3.storage (it's simpler to do this , though):

To resolve the published content, you can request it from any public IPFS http gateway. You'll experience the lowest latency when querying a gateway close to the node that you used for uploading; web3.storage offers https://w3s.link for that purpose. Requesting yields

An IP-NFT's most important metadata property are its agreements, another term for legal documents attached to it. These documents refer to the IP-NFT smart contract's ("collection") address and the IP-NFT's token id. The agreements' content might contain confidential information about the involved parties and hence should be encrypted before being uploaded. Decentralized and permissionless encryption is a non trivial requirement that we solve by relying on .

lays out the encryption process in detail. To instantiate a Lit SDK instance that's capable of encrypting or decrypting content, an compatible signature that proves control over the current user's account. Once authenticated we can request a new symmetric key to encrypt our content and ultimately ask the Lit network nodes to store its key shares along with an access control condition. That request yields an encrypted decryption key (๐Ÿ˜ตโ€๐Ÿ’ซ) that has been created by the network nodes.

An IP-NFT metadata's agreements item can store the encrypted symmetric key and its access control conditions inside its encryption field. Note that the IP-NFT JSON schema of access_control_conditions is externally .

Due to the high value nature of IP-NFTs you might feel tempted to use a multisig wallet for the minting process, maybe because you'd like to prove that the IP-NFT has been created by a dedicated group of individuals. This works fine for all contract function invocations but is not supported by Lit protocol. Multisig wallets (or contract wallets to be precise) cannot sign messages in the way it's required to authenticate against Lit nodes because they're not based on a private key. This might once be possible by utilizing compatible wallet signatures but earlier. We're going to add soon.

Another shortcoming related to Lit's requirement of private key based authentication signatures is that multisig token holders cannot prove their address to the protocol. To allow multisig members to decrypt the accompanying agreement documents, the IP-NFT contract contains a function that can only be invoked by the current token holder (e.g. a multisig) to grant certain accounts (e.g. some of their members or potential buyers) read access to the underlying content for a limited amount of time. Its counterpart yields a boolean whether the reader is currently allowed access. For the current owner of an IP-NFT this method always returns true.

To make read grants work inside Lit protocol, one can craft a that not only takes the current IP-NFT ownership into account but also lets users pass that currently are granted read access:

Since agreement documents are encrypted before being stored, each agreement item may contain a content_hash that downloaders can use to prove the legal documents' content integrity after they've decrypted it. When using IPFS as storage layer this hash is not adding much value since the network's content ids already provide an untamperable way of guaranteeing content integrity, however and hard to prove without an IPFS node at hand.

The content_hash field shall contain the sha-256 digest of the attachment's binary content, encoded as a multihash compatible to CIDv1. This allows users to decode the content hash and verify document's content without being aware of the hashing algorithm used. This is how it looks like in Typescript using the :

Some of the attached legal PDF documents might contain a reference on being only valid when being digitally signed by an individual party. IP-NFTs that are minted from within the Molecule ecosystem must carry an compatible message with a personal signature. We refer to this structure as TermsSig. It proves that a party has agreed to terms shown on a website or mentioned in legal contracts.

You can use any EIP-191 compatible function to prove who has signed these terms. Etherscan to publicly prove messages and signatures to simplify this process.

mainnet 0x3D30452c48F2448764d5819a9A2b684Ae2CC5AcF

testnet (gรถrli) 0xbCeb6b875513629eFEDeF2A2D0b2f2a8fd2D4Ea4

๐Ÿ› ๏ธ
โš™๏ธ
Github repo
samples repo
NFTs
ERC-721
here
Arweave
IPFS
on mainnet
on Gรถrli
TheGraph, subgraphs
how to query them
UUPS proxies
Molecule developer team's multisig
here
easily retrieve their ABIs
on Github
well defined JSON schema that's
Here's a visual tool
Ajv
the code
Ardrive
within a browser context
https://w3s.link/ipfs/bafkreicrhuxfzrydht6tmd4kyy6pkbhspqthswg6xbiqlaztmf774ojxhq
Lit Protocol
Lit's documentation
it needs
EIP-4361
defined by Lit protocol
EIP-1271
was not supported
support for it
grantReadAccess
canRead
custom contract access control condition
they're not derived from the original content
multiformats NPM package
EIP-191
ecrecover
comes with a dedicated feature
https://mint.molecule.to/api/signoffMetadata
https://testnet.mint.molecule.to/api/signoffMetadata