EIP.tools

EIP.tools

⚠️ DraftStandards Track: ERC

ERC-8393: Tokenized Carbon Credits with Retirement

An ERC-1155 extension for carbon credits with project/credit identifiers, registry-aligned metadata, and irreversible retirement.

Authors
Created2026-08-24
Discussion Linkhttps://ethereum-magicians.org/t/erc-8393-tokenized-carbon-credits-with-retirement/29488
Requires

Markdown

https://raw.githubusercontent.com/ethereum/ERCs/re...
Pull Request#1965PR open

EIP-GPT summary

Contents
AbstractMotivationSpecificationInterfaceToken identifier encodingMetadataIssuanceRetirement (offset)Expiry and existenceMetadata URIRationaleBackwards CompatibilitySecurity ConsiderationsCopyright

Abstract

This proposal defines an interface for representing carbon credits as semi-fungible tokens on top of ERC-1155. Each token encodes a (projectId, creditId) pair into a single uint256 identifier and carries a fixed metadata schema aligned with the attributes published by carbon registries: project name, methodology, vintage, country of issuance, total issued volume, and expiry. The interface also defines retirement, the act of using a credit to offset emissions. Retiring subtracts from the holder's transferable balance and records an equal, permanent, non-transferable retired balance, so issuance, transfer, and retirement can all be audited directly from the ledger.

Motivation

Voluntary carbon markets have no standard on-chain representation for credits. The same reduction can be sold or counted more than once, and large minimum lot sizes keep individuals and small enterprises out of the market. Existing tokens use plain fungible (ERC-20) or non-fungible (ERC-721) representations, neither of which fits an asset where one project issues many credit batches, each divisible down to fractions of a ton and each subject to retirement.

ERC-1155 provides the semi-fungible substrate, but it does not say how a project/credit hierarchy maps onto a token id, which metadata attributes a credit must expose to be matched against registry records, or what retirement means on-chain. Without a shared interface, wallets, marketplaces, and auditors cannot interpret a carbon credit token from an unknown contract; this ERC defines that interface.

Specification

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in RFC 2119 and RFC 8174.

A compliant contract MUST implement ERC-1155, ERC-165, and the IERC8393 interface below.

Interface

interface IERC8393 /* is IERC1155, IERC165 */ {
    struct CreditMetadata {
        uint256 projectId;
        uint256 creditId;
        string  projectName;
        string  methodology;   // registry methodology identifier
        string  vintage;       // vintage year(s)
        string  country;       // country of issuance (ISO 3166-1 recommended)
        uint256 totalVolume;   // total issued amount, in base units
        uint256 expiry;        // unix seconds; 0 means no expiry
        bool    isRetired;     // true once the entire issued volume is retired
    }

    event CreditMinted(address indexed to, uint256 indexed tokenId, uint256 projectId, uint256 creditId, uint256 amount, uint256 expiry);
    event CreditRetired(address indexed account, uint256 indexed tokenId, uint256 amount);

    function encodeTokenId(uint256 projectId, uint256 creditId) external pure returns (uint256 tokenId);
    function decodeTokenId(uint256 tokenId) external pure returns (uint256 projectId, uint256 creditId);
    function getMetadata(uint256 tokenId) external view returns (CreditMetadata memory);
    function mintCredit(address to, uint256 projectId, uint256 creditId, uint256 amount, uint256 expiry, CreditMetadata calldata metadata) external;
    function retire(uint256 tokenId, address account, uint256 amount) external;
    function retiredBalanceOf(address account, uint256 tokenId) external view returns (uint256);
    function tokenExists(uint256 tokenId) external view returns (bool);
    function isExpired(uint256 tokenId) external view returns (bool);
}

supportsInterface(type(IERC8393).interfaceId) MUST return true, in addition to the ERC-1155 (0xd9b67a26) and ERC-165 (0x01ffc9a7) interface identifiers.

Token identifier encoding

A token id is the concatenation of a 128-bit project id and a 128-bit credit id:

tokenId = (projectId << 128) | creditId;

encodeTokenId and decodeTokenId MUST implement exactly this mapping. encodeTokenId MUST revert if projectId >= 2**128 or creditId >= 2**128.

Metadata

getMetadata MUST return the metadata recorded at issuance and MUST revert if tokenExists(tokenId) is false.

Token amounts SHOULD use 8 decimals (1 tCO2e = 10**8 base units) so that sub-ton amounts can be held and retired.

Issuance

mintCredit creates a credit and mints its entire volume to to:

  • It MUST revert if tokenExists(tokenId) is already true for tokenId = encodeTokenId(projectId, creditId); a credit is issued exactly once.
  • It MUST revert if metadata.projectId != projectId, metadata.creditId != creditId, metadata.expiry != expiry, or metadata.totalVolume != amount.
  • It MUST store the metadata with isRetired = false, regardless of the value supplied by the caller.
  • It MUST emit CreditMinted.

Retirement (offset)

Retirement converts transferable balance into retired balance:

  • retire MUST revert unless msg.sender is account, is approved for account via ERC-1155 setApprovalForAll, or holds an implementation-defined operator authorization.
  • It MUST revert if amount exceeds account's transferable balance of tokenId.
  • It MUST decrease balanceOf(account, tokenId) by amount and MUST emit the ERC-1155 TransferSingle event with to set to the zero address, so that ERC-1155 tooling observes the supply reduction.
  • It MUST increase retiredBalanceOf(account, tokenId) by amount and MUST emit CreditRetired.
  • A retired balance MUST NOT be transferable and MUST NOT decrease.
  • Once the sum of retired balances for a token id equals its totalVolume, the contract MUST set isRetired = true in the token's metadata. isRetired MUST NOT be true while any account holds a nonzero transferable balance of the token id.

A retired balance is the permanent on-chain record that account offset amount of the credit. Because it can never move or shrink, the same credit cannot be sold or counted twice.

Expiry and existence

tokenExists(tokenId) MUST return true if and only if mintCredit has been executed successfully for tokenId.

isExpired(tokenId) MUST return true if and only if the token's expiry is nonzero and block.timestamp >= expiry.

This standard places no restriction on transferring or retiring expired credits; consuming systems SHOULD check isExpired before accepting a retirement as an offset.

Metadata URI

uri(tokenId) MAY return an on-chain data:application/json;base64,... document embedding the CreditMetadata fields, so that a credit's attributes can be read without any off-chain dependency.

Rationale

Packing the project and credit ids 128/128 into one uint256 keeps the token a plain ERC-1155 id. One contract can host any number of projects and credit batches, and existing wallets and explorers keep working without changes.

Retirement decreases the transferable balance but records who retired how much, rather than only burning. Auditors and registries need a permanent answer to which account offset how much of which credit; a plain burn would erase that attribution.

The metadata fields (methodology, vintage, country, issued volume, expiry) are the attributes existing registries such as Verra and Gold Standard already publish for every credit batch, so tokens map directly onto registry records.

8 decimals allows sub-ton amounts. Retail offsets are usually fractions of a ton, and whole-ton lot sizes are one of the barriers this design tries to remove.

Issuance is deliberately one-shot per token id. Fixing totalVolume at mint is what makes the retired-balance accounting and the isRetired flag verifiable against a known total.

The design is extracted from a production deployment on a public EVM-compatible network, where it operates as the settlement layer of a carbon credit marketplace.

Backwards Compatibility

Every compliant token is a valid ERC-1155 token and works with existing ERC-1155 tooling; the functions and events defined here are additive. Retirement is observable by generic tooling as a burn (TransferSingle to the zero address).

Security Considerations

  • Retirement is irreversible. Implementations MUST enforce the authorization rules above, and callers SHOULD confirm amounts before retiring, because an erroneous retirement cannot be undone on-chain.
  • If an implementation exposes an administrative function that updates a credit's metadata, that function MUST NOT clear isRetired and MUST NOT change projectId, creditId, or totalVolume.
  • An implementation MAY grant a privileged operator the ability to transfer or retire on behalf of holders. Such a role concentrates custody and SHOULD be minimized and documented by the deployment.
  • Expiry is advisory at the token layer; systems consuming retirements as offsets SHOULD reject expired credits.

Copyright and related rights waived via CC0.

EIP.tools

EIP.tools

Search, read, and map Ethereum improvement proposals, ERCs, RIPs, and CAIPs from one focused interface.

Farcaster
by @apoorveth