--- eip: 8393 title: Tokenized Carbon Credits with Retirement description: An ERC-1155 extension for carbon credits with project/credit identifiers, registry-aligned metadata, and irreversible retirement. author: Sehyun Kim (@sehyun-wincl) discussions-to: https://ethereum-magicians.org/t/erc-8393-tokenized-carbon-credits-with-retirement/29488 status: Draft type: Standards Track category: ERC created: 2026-08-24 requires: 165, 1155 --- ## Abstract This proposal defines an interface for representing carbon credits as semi-fungible tokens on top of [ERC-1155](./eip-1155.md). 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](./eip-20.md)) or non-fungible ([ERC-721](./eip-721.md)) 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](./eip-1155.md) 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](./eip-1155.md), [ERC-165](./eip-165.md), and the `IERC8393` interface below. ### Interface ```solidity 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](./eip-1155.md) (`0xd9b67a26`) and [ERC-165](./eip-165.md) (`0x01ffc9a7`) interface identifiers. ### Token identifier encoding A token id is the concatenation of a 128-bit project id and a 128-bit credit id: ```solidity 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](./eip-1155.md) `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](./eip-1155.md) `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](./eip-1155.md) 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](./eip-1155.md) 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 Copyright and related rights waived via [CC0](../LICENSE.md).