05 semi fungible tokens

What is Semi-Fungible?

In carbon credits, a "unit" must be:

  • Interchangeable within the same project/vintage (easy to buy/sell/retire in bulk)

  • Not interchangeable across different projects/vintages (auditors need origin traceability)

The Rule:

1 unit of Project A 2024 = interchangeable with any other unit of Project A 2024
1 unit of Project A 2024 ≠ 1 unit of Project B 2023

This "fungible inside, non-fungible across" behavior is semi-fungible.

Why Semi-Fungible?

Traditional Approaches Have Limitations

Fully Fungible Tokens:

  • ❌ Credits become interchangeable across projects

  • ❌ Breaks audit traceability

  • ❌ High risk of credit mixing

  • ❌ Not regulator-friendly

NFT-Only Models:

  • ❌ Each credit is a separate NFT

  • ❌ Poor scalability (millions of NFTs)

  • ❌ No fractional ownership

  • ❌ Enterprise bulk operations are complex

Semi-Fungible Solution:

  • ✅ Fungible within project (easy operations)

  • ✅ Non-fungible across projects (traceability preserved)

  • ✅ Supports fractional ownership

  • ✅ Scales efficiently

  • ✅ Enterprise-friendly

Contract Architecture

A single contract that supports many token IDs, where:

  • tokenId represents the project (or project + vintage)

  • Balances stored as balanceOf(user, tokenId)

Advantages:

  • Scales to millions of credits

  • Supports enterprise bulk operations

  • Preserves project separation

  • Supports decimals via units design

Standard: ERC-1155 (Multi-Token Standard)

Option B: Separate ERC-20 Per Project

Each project becomes its own ERC-20 token contract.

Disadvantages:

  • Many contracts to manage

  • Complex UX (many token addresses)

  • Harder to scale

  • More gas costs

Recommendation: Use Option A for platform scalability.

Token ID Strategy

1

Each project record gets an on-chain ID (projectId), and credits use the same projectId as tokenId.

Advantages:

  • Easiest to trace and reason about

  • Direct 1:1 mapping

  • Simple implementation

Example:

2

tokenId = hash(projectId + vintage + verifier + methodology)

Useful if you need multiple "batches" per project.

Use Case: Same project issues 2024 credits and 2025 credits separately.

Example:

Recommendation: Use Option 1 for v1. Upgrade to this option if multiple batches per project are needed.

Units & Decimals

The Challenge

ERC-1155 uses integer balances. Carbon credits need decimal precision (e.g., 0.25 tons, 12,437.62 tons).

The Solution: Fixed-Point Units

Define UNIT = 1e6 (or 1e18 for maximum precision).

Conversion:

UI Convention:

  • Store on-chain as integers

  • Display in UI as decimals (divide by UNIT)

Example:

Contract Design: CarbonCredit1155

Storage (Per tokenId)

Transfer Policy Enum

Roles

Core Functions

setProjectConfig

Checks:

  • Project must have PoAI approved

  • Cap must be > 0

mintCredits

Enforces:

  • Project must be active

  • issued[tokenId] + amountUnits <= cap[tokenId]

  • Increments issued[tokenId]

retire

Enforces:

  • balanceOf(msg.sender, tokenId) >= amountUnits

  • Burns units from user

  • Increments retired[tokenId]

  • Emits CreditsRetired event

balanceOf

Standard ERC-1155 balance query.

Events

How PoAI Plugs In (Gating)

PoAI is NOT inside the ERC-1155 contract; it's the approval precondition.

Enforced Gating Rule: A tokenId can be configured (cap/active) only if:

  • Project has PoAI approved on-chain in ProjectRegistry, OR

  • Governance provides PoAI hashes as part of config transaction

Implementation:

Result: No PoAI → no activation → no mint.

Complete Platform Flow

1

Project Onboarding → PoAI → Governance Approval

  • Issuer submits project (off-chain)

  • PoAI module verifies:

    • Asset integrity

    • Data integrity

    • Process integrity

  • PoAI produces:

    • bundleURI (encrypted JSON)

    • bundleHash, assetHash, dataHash, processHash

  • Governance proposal: Approve project + issuance cap + policy

  • On-chain:

    • Mint ProjectRecord (projectId)

    • Attach PoAI hashes to ProjectRegistry

    • Call CarbonCredit1155.setProjectConfig(projectId, capUnits, policy, active=true)

Result: Project tokenId is now "live" and can issue credits.

2

Credit Issuance

  • Issuer decides to issue N credits

  • Calls mintCredits(projectId, treasury/multisig, amountUnits)

  • Contract checks:

    • Project active

    • issued + amount <= cap

  • Emits CreditsMinted

Result: Supply exists on-chain and can be sold/allocated.

3

Retail Purchase

Pattern A (Non-custodial on-chain):

  • User pays in stablecoin/native token

  • Marketplace contract transfers credit units from treasury to user

Pattern B (Off-chain payment):

  • User pays via fiat/off-chain

  • Backend instructs issuer wallet to transfer/mint credits to user

On-chain result: balanceOf(user, projectId) increases

4

Enterprise Purchase

  • Enterprise selects project mix and quantity (e.g., 12,437.62)

  • Platform computes amountUnits = 12437.62 * UNIT

  • Credits transferred/minted to enterprise wallet

  • Dashboard shows holdings per tokenId/projectId

5

Retirement (Burn) + Certificate

  • User/Enterprise selects:

    • projectId

    • Amount to retire

  • Call retire(projectId, amountUnits)

  • Contract:

    • Burns balance

    • Increments retired counter

    • Emits event

  • Indexer listens to CreditsRetired

  • Reporting service generates:

    • Certificate PDF + JSON

    • Includes tx hash, project metadata, PoAI bundleHash reference

Result: Retirement is permanent and auditable (prevents double counting).

Marketplace / Transfer Policy

Per-Project Policy

  • OPEN: Normal ERC-1155 transfers allowed

  • RESTRICTED: Transfers only to/from approved operators (marketplace, custody, escrow)

  • DISABLED: No transfers at all; only mint to buyer and retire

Enforcement

Override _beforeTokenTransfer and check:

This is a big "enterprise-friendly" lever for compliance.

Metadata Structure

uri(tokenId) should return JSON containing:

This makes your ERC-1155 tokenId self-describing and audit-ready.

Operational Advantages

Retail Side

  • Small purchases possible (0.01, 0.25, etc.)

  • Simple holdings view by project

  • Retirement gives clean proof

Enterprise Side

  • Bulk purchase and retirement without handling thousands of NFTs

  • Exact accounting down to decimals

  • Reports remain project-specific for audits

Compliance

  • Each unit always points back to a specific ProjectRecord (origin preserved)

  • Prevents "credit mixing" that breaks ESG audits

Implementation Decision Points

1

Are Transfers Allowed?

  • If marketplace trading desired: Allow transfers

  • If strict compliance: Allow only "buy" and "retire" (no P2P transfers)

2

Custodial vs Non-Custodial Enterprise Wallets

  • Many enterprises prefer multisig/custodial

  • Affects UX but not contract design

3

Project tokenId Mapping

  • Best: ProjectRecord tokenId is same ID used in credits contract

One-Liner Explanation

"We make each project a unique record, and the credits under that project behave like units you can buy in fractions and retire easily like tokens, but still traceable per project like NFTs."

Last updated