EIP.tools

EIP.tools

⚠️ ReviewStandards Track: ERC

ERC-8097: In-Ground Asset Token

A framework for natural resource assets on Ethereum with geological metadata, CP/QP attestation, depletion-aware state, and ESG primitives.

Authors
Created2025-11-24
Discussion Linkhttps://ethereum-magicians.org/t/erc-8097-in-ground-asset-token-standard-for-geological-metadata-compliance-extraction-aware-token-models/26869
Requires

Markdown

https://raw.githubusercontent.com/solomonashok/ERC...

EIP-GPT summary

Contents
AbstractMotivationSpecificationTerminology and External StandardsOverviewData StructuresInterfaceEIP-712 Domain and SigningDeposit IdentifierTransfer GatingNormative RulesRationaleBackwards CompatibilityReference ImplementationSecurity ConsiderationsCopyright

Abstract

The In-Ground Asset Token standard defines a machine-verifiable framework for representing mining and natural resource assets on Ethereum-compatible chains.

This standard defines five canonical metadata objects — IRO, IGO, ICO, IEXO, and IEO — plus a Score Mutation Log (SML). Together they represent the verifiable disclosure record of an in-ground asset: resource, geology, compliance, extraction, environmental/social/governance, and score-history metadata. The contract anchors object hashes, enforces lifecycle and depletion invariants, records CP/QP EIP-712 attestations, and exposes an informational disclosure-quality score. It does not compute financial risk, investment merit, geological quality, or asset value.

Motivation

In-ground assets represent trillions of dollars of value in mineral reserves and natural resource projects globally. These assets are commonly represented by static PDFs, regulatory filings, and ESG disclosures that are difficult to compare, verify, or keep current across jurisdictions.

This creates several compounding problems:

  1. No canonical digital identity: Mining assets lack a shared on-chain identity and metadata schema comparable to the structured reporting used in equities or land registries.

  2. Disclosure is difficult to verify: Geological, compliance, ESG, and extraction claims are spread across reports and filings, usually without a stable machine-verifiable record.

  3. RWA tokenisation is blocked at the data layer: Real-world asset tokenisation of mining assets requires trusted, structured, verifiable data before ownership, financing, or collateral systems can rely on it.

  4. Operational state is not monotonic: Lifecycle, depletion, and production status changes need explicit invariants so historical records cannot be rewritten into misleading current-state claims.

Existing token standards (ERC-20, ERC-721, ERC-1155, ERC-3643) do not define geological metadata, CP/QP attestations, depletion-aware state, or mining-specific compliance metadata. This ERC defines that core metadata and anchoring layer.

Specification

Terminology and External Standards

This specification references the following international mineral reporting frameworks. These standards define geological classification, reporting, and professional attestation requirements that this ERC encodes into machine-verifiable form.

AbbreviationFull nameReference
JORCAustralasian Joint Ore Reserves Committee Codejorc.org/code
NI 43-101Canadian National Instrument 43-101securities-administrators.ca
SAMRECSouth African Mineral Reporting Codessamcode.co.za
PERCPan-European Reserves and Resources Reporting Committeepercreserves.eu
S-K 1300US Securities and Exchange Commission mining disclosure standardsec.gov
CP/QPCompetent Person or Qualified Person — a named professional with specified credentials who signs or attests the technical reportPer the relevant reporting standard above

In this document, "CP" and "QP" are used interchangeably and refer to the qualified professional whose identity and signature are required under the applicable reporting standard.

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

Disclosure Quality means the completeness and currency of documented asset disclosures. It is not geological quality, financial viability, investment merit, or financial risk.

Intelligence Grade means an off-chain label for disclosure completeness. NO_DATA MUST display as "No Data". The terms HIGH_RISK, RISK_BAND, and "Risk Band" MUST NOT appear in ABI-visible names, return values, or runtime strings.

Overview

An ERC-8097 implementation maintains a lifecycle-governed asset record and anchors six off-chain JSON object hashes:

ObjectCodePrimary purpose
In-Ground Resource ObjectIROWhat is in the ground
In-Ground Geology ObjectIGOHow the resource is known
In-Ground Compliance ObjectICOLegal, tenure, ownership, and permitting record
In-Ground Extraction ObjectIEXOProduction status and depletion record
In-Ground Environmental ObjectIEOEnvironmental, social, and governance disclosures
Score Mutation LogSMLOff-chain score-history event array hash and informational current score

Every asset starts at INDEXED, progresses through CLAIMED and VERIFIED, and becomes ANCHORED only through anchor().

INDEXED   -> CLAIMED -> VERIFIED -> ANCHORED

advanceLifecycle() MUST advance exactly one step and MUST NOT set ANCHORED. Only anchor() may set ANCHORED.

Data Structures

enum LifecycleState {
    INDEXED,
    CLAIMED,
    VERIFIED,
    ANCHORED
}

enum ProductionStatus {
    PRE_EXPLORATION,
    EXPLORATION,
    PRE_FEASIBILITY,
    FEASIBILITY,
    DEVELOPMENT,
    PRODUCTION,
    CARE_AND_MAINTENANCE,
    CLOSED
}

enum DisclosureStatus {
    UNKNOWN,
    CONFIRMED,
    REFERENCED,
    NOT_DISCLOSED,
    NOT_APPLICABLE
}

Production status transitions MUST use an explicit transition table rather than numeric enum ordering:

PRE_EXPLORATION -> EXPLORATION
EXPLORATION -> PRE_FEASIBILITY
PRE_FEASIBILITY -> FEASIBILITY
FEASIBILITY -> DEVELOPMENT
DEVELOPMENT -> PRODUCTION
PRODUCTION -> CARE_AND_MAINTENANCE
CARE_AND_MAINTENANCE -> PRODUCTION
PRODUCTION -> CLOSED
CARE_AND_MAINTENANCE -> CLOSED

CLOSED is terminal. PRODUCTION <-> CARE_AND_MAINTENANCE is permitted because suspension and restart are legitimate operating states.

struct IRO {
    bytes32 assetId;
    string depositName;
    string primaryCommodity;
    string reportingStandard;
    uint256 jorcEffectiveDate;
    uint256 totalResourceMt;
    uint256 totalGrade;
    string gradeUnit;
    uint256 totalContainedMetal;
    string containedMetalUnit;
    string[] classifications;
    uint256[] classTonnesMt;
    uint256[] classGrades;
    uint256[] classContained;
    bool oreReserveDeclared;
    uint256 reserveTotalMt;
    uint256 reserveGrade;
    uint256 reserveContained;
    bool explorationTargetPresent;
    uint256 cutOffGrade;
    string cutOffUnits;
    string cpNameResources;
    string cpBodyResources;
    string cpNameReserves;
    string cpBodyReserves;
    bytes32 objectHash;
    uint256 anchoredAt;
    uint8 extractionConfidence;
}

IRO.reserveTotalMt MUST be less than or equal to IRO.totalResourceMt. Exploration target data is off-chain and any display of such data MUST include the relevant conceptual warning required by the reporting standard.

struct IGO {
    bytes32 assetId;
    string depositStyle;
    string hostRock;
    string mineralogy;
    uint256 strikeLengthKm;
    uint256 depthMaxM;
    uint256 drillingTotalMetres;
    uint256 drillingHolesCount;
    string estimationMethod;
    string blockModelSoftware;
    uint256 compositeIntervalM;
    bool qaqcBlanks;
    bool qaqcStandards;
    bool qaqcDuplicates;
    bool qaqcUmpireAssays;
    string laboratoryName;
    bool laboratoryAccreditedISO;
    string cpName;
    string cpMembershipBody;
    string cpMembershipNumber;
    uint256 cpSiteVisitDate;
    bool cpSiteVisitConfirmed;
    uint256 reportDate;
    bytes32 objectHash;
    uint256 anchoredAt;
    uint8 extractionConfidence;
}

If cpSiteVisitConfirmed == true, then cpSiteVisitDate > 0 MUST hold in the off-chain object being anchored.

struct ICO {
    bytes32 assetId;
    string jurisdictionCountry;
    string jurisdictionState;
    string regulatoryBodyMining;
    string regulatoryBodyEnvironment;
    string regulatoryBodyWater;
    string legalFramework;
    string tenureType;
    uint8 tenureStatus;
    string legalEntityName;
    string parentCompany;
    uint256 ownershipPct;
    string[] jvPartners;
    bool cpAttestationPresent;
    uint8 cpCount;
    string royaltyType;
    bool royaltyRateDisclosed;
    bool indigenousAgreement;
    string indigenousAgreementCounterparty;
    string indigenousFramework;
    uint8 environmentalPermitsStatus;
    bool allTenureOnGranted;
    bytes32 objectHash;
    uint256 anchoredAt;
    uint8 extractionConfidence;
}

ownershipPct is scaled by 1e4, where 10000 == 100%.

struct IEXO {
    bytes32 assetId;
    ProductionStatus productionStatus;
    uint256 productionCommencedYear;
    uint256 annualGuidanceMin;
    uint256 annualGuidanceMax;
    string annualGuidanceUnit;
    string annualGuidanceCurrency;
    uint256 aiscMin;
    uint256 aiscMax;
    string aiscCurrency;
    string aiscUnit;
    uint256 processingPlantCapacityMtpa;
    uint256 processingRecoveryPct;
    string processingMethod;
    string mineType;
    uint256 depletionReportedMt;
    bool depletionReported;
    bytes32 objectHash;
    uint256 anchoredAt;
    uint8 extractionConfidence;
}

depletionReportedMt MUST only increase. updateDepletion() MUST require productionStatus == PRODUCTION.

struct IEOEnvironmental {
    DisclosureStatus eiaStatus;
    DisclosureStatus waterRights;
    bool tailingsFacilityPresent;
    DisclosureStatus tailingsManagementPlan;
    DisclosureStatus rehabilitationPlan;
    DisclosureStatus acidRockDrainageStudy;
    bool ghgScope1;
    bool ghgScope2;
    bool ghgScope3;
    bool netZeroTarget;
    uint256 netZeroTargetYear;
    bool renewableEnergyPresent;
    DisclosureStatus tcfdAlignment;
}

struct IEOSocial {
    DisclosureStatus communityConsultation;
    DisclosureStatus indigenousConsultation;
    DisclosureStatus grievanceMechanism;
    DisclosureStatus healthSafetyKPIs;
    bool resettlementRequired;
    DisclosureStatus resettlementPlan;
    DisclosureStatus localEmploymentPlan;
}

struct IEOGovernance {
    DisclosureStatus boardESGCommittee;
    DisclosureStatus antiBriberyPolicy;
    DisclosureStatus thirdPartyESGAudit;
    bool irmaAligned;
    bool icmmMember;
    bool sustainabilityReportPresent;
    uint256 sustainabilityReportDate;
    string sustainabilityReportFramework;
}

struct IEO {
    bytes32 assetId;
    IEOEnvironmental environmental;
    IEOSocial social;
    IEOGovernance governance;
    bytes32 objectHash;
    uint256 anchoredAt;
    uint8 extractionConfidence;
}

DisclosureStatus values MUST NOT map to HIGH_RISK, RISK_BAND, or "Risk Band".

struct SML {
    bytes32 assetId;
    uint256 currentScore;
    string scoreVersion;
    string rulebookVersion;
    bytes32 smlHash;
    uint256 anchoredAt;
}

struct CPAttestation {
    bytes32 assetId;
    bytes32 objectHash;
    string cpName;
    string cpBody;
    string cpMembershipNumber;
    uint256 attestationTimestamp;
    address signerAddress;
    bytes signature;
}

struct AssetRecord {
    bytes32 assetId;
    string mineName;
    string mineSlug;
    LifecycleState lifecycleState;
    string schemaVersion;
    string rulebookVersion;
    bytes32 iroHash;
    bytes32 igoHash;
    bytes32 icoHash;
    bytes32 iexoHash;
    bytes32 ieoHash;
    bytes32 smlHash;
    bytes32 reportPdfHash;
    string reportIpfsUri;
    uint256 anchoredAt;
    address anchoredBy;
    bool active;
}

currentScore is informational only. It MUST be in [0, 1000] and MUST NOT be used for financial decisions, collateralisation, covenants, or liquidation triggers.

Interface

interface IERC8097 is IERC165 {
    event AssetRegistered(bytes32 indexed assetId, string mineSlug, address registeredBy, uint256 timestamp);
    event AssetAnchored(
        bytes32 indexed assetId,
        string mineSlug,
        bytes32 iroHash,
        bytes32 igoHash,
        bytes32 icoHash,
        bytes32 iexoHash,
        bytes32 ieoHash,
        bytes32 smlHash,
        bytes32 reportPdfHash,
        uint256 anchoredAt
    );
    event AssetReAnchored(bytes32 indexed assetId, bytes32 previousIroHash, bytes32 newIroHash, uint256 reAnchoredAt);
    event LifecycleAdvanced(bytes32 indexed assetId, LifecycleState fromState, LifecycleState toState, uint256 timestamp);
    event CPAttestationRecorded(
        bytes32 indexed assetId,
        bytes32 indexed objectHash,
        string cpName,
        string cpBody,
        address signerAddress,
        uint256 attestationTimestamp
    );
    event DepletionUpdated(bytes32 indexed assetId, uint256 previousDepletionMt, uint256 newDepletionMt, uint256 timestamp);
    event SMLUpdated(bytes32 indexed assetId, bytes32 newSmlHash, uint256 currentScore, uint256 timestamp);
    event ProductionStatusAdvanced(bytes32 indexed assetId, ProductionStatus fromStatus, ProductionStatus toStatus, uint256 timestamp);

    function register(string calldata mineSlug, string calldata mineName, string calldata schemaVersion)
        external returns (bytes32 assetId);

    function anchor(
        bytes32 assetId,
        bytes32 iroHash,
        bytes32 igoHash,
        bytes32 icoHash,
        bytes32 iexoHash,
        bytes32 ieoHash,
        bytes32 smlHash,
        uint256 currentScore,
        bytes32 reportPdfHash,
        string calldata reportIpfsUri,
        string calldata rulebookVersion,
        string calldata schemaVersion
    ) external;

    function advanceLifecycle(bytes32 assetId, LifecycleState newState) external;

    function recordCPAttestation(
        bytes32 assetId,
        bytes32 objectHash,
        string calldata cpName,
        string calldata cpBody,
        string calldata cpMembershipNumber,
        address expectedSigner,
        uint256 attestationTimestamp,
        bytes calldata signature
    ) external;

    function advanceProductionStatus(bytes32 assetId, ProductionStatus newStatus) external;
    function updateDepletion(bytes32 assetId, uint256 newDepletionMt) external;
    function updateSML(bytes32 assetId, bytes32 newSmlHash, uint256 currentScore) external;
    function getAsset(bytes32 assetId) external view returns (AssetRecord memory);
    function getAssetBySlug(string calldata mineSlug) external view returns (bytes32);
    function getLifecycleState(bytes32 assetId) external view returns (LifecycleState);
    function getCPAttestations(bytes32 objectHash) external view returns (CPAttestation[] memory);
    function isAnchored(bytes32 assetId) external view returns (bool);
    function getCurrentScore(bytes32 assetId) external view returns (uint256);
    function getDepletion(bytes32 assetId) external view returns (uint256);
    function getProductionStatus(bytes32 assetId) external view returns (ProductionStatus);
    function getSML(bytes32 assetId) external view returns (SML memory);
    function getDomainSeparator() external view returns (bytes32);
    function isProductionTransitionAllowed(ProductionStatus from, ProductionStatus to) external view returns (bool);
}

EIP-712 Domain and Signing

CP/QP attestations MUST use EIP-712 typed structured data signing.

The EIP-712 domain MUST be:

EIP712Domain({
    name: "ERC-8097 In-Ground Asset Token",
    version: "1",
    chainId: <current chain ID>,
    verifyingContract: <address of the implementing contract>
})

The signed type hash is:

bytes32 constant CP_ATTESTATION_TYPEHASH = keccak256(
    "CPAttestation(bytes32 assetId,bytes32 objectHash,string cpName,string cpBody,string cpMembershipNumber,uint256 attestationTimestamp)"
);

The digest MUST include caller-supplied attestationTimestamp, not block.timestamp. The recovered signer MUST equal the caller-supplied expectedSigner, and the recovered signer address MUST be stored and emitted.

Implementations MUST reject replayed attestation digests and MUST reject attestations where objectHash does not match one of the asset's current iroHash, igoHash, icoHash, iexoHash, ieoHash, or smlHash.

Professional-body membership and CP/QP identity verification are off-chain responsibilities. Implementations MAY add an authorised-signer registry extension to enforce this on-chain.

Deposit Identifier

The ERC-8097 asset ID MUST be deterministic for a registry slug and chain:

bytes32 assetId = keccak256(abi.encode(block.chainid, mineSlug));

The mineSlug MUST match the off-chain registry slug and MUST be unique.

Transfer Gating

ERC-8097 is a metadata and anchoring standard. Implementations that extend token standards MAY gate token transfers on lifecycle state, current object hashes, CP/QP attestations, jurisdictional rules, or external compliance modules. Such transfer logic is out of scope for this ERC.

Normative Rules

RuleRequirement
R-FORBIDDEN-TERMSHIGH_RISK, RISK_BAND, and "Risk Band" MUST NOT appear in ABI-visible names, return values, or runtime strings. NO_DATA displays as "No Data".
R-LIFECYCLE-MONOTONICadvanceLifecycle() MUST advance exactly one step and MUST NOT accept ANCHORED; only anchor() may set ANCHORED.
R-PRODUCTION-TRANSITIONSProduction status MUST use an explicit transition table; PRODUCTION <-> CARE_AND_MAINTENANCE is permitted and CLOSED is terminal.
R-DEPLETION-MONOTONICDepletion MUST only increase and requires PRODUCTION status.
R-CP-EIP-712CP/QP attestations MUST use EIP-712 typed data with caller-supplied attestationTimestamp.
R-CP-IDENTITYThe contract proves a signer address signed; professional credential verification is off-chain.
R-CP-REPLAYEvery EIP-712 digest MUST be accepted at most once.
R-CP-OBJECT-MATCHAttested objectHash MUST match one of the asset's current six object hashes.
R-REPORT-REQUIREDanchor() MUST reject missing reportPdfHash or empty reportIpfsUri.
R-SCORE-RANGEcurrentScore MUST be in [0, 1000] in both anchor() and updateSML().
R-SLUG-UNIQUEmineSlug MUST be unique.
R-REANCHOR-HISTORYCurrent storage may update; historical hashes MUST remain recoverable from events.
R-NO-SCORINGThe contract MUST NOT compute IGA Score or disclosure-quality indices.
R-NO-RISK-ASSESSMENTThe contract MUST NOT perform financial, investment, or geological-quality assessment.
R-FRESHNESS-OFFCHAINFreshness decay and lambda parameters are off-chain.

Recommended CI checks:

forge inspect ERC8097 abi | grep -i "HIGH_RISK\|RISK_BAND" \
  && echo "ERROR: forbidden term in ABI" && exit 1 || echo "OK: ABI clean"

grep -En '"[^"]*HIGH_RISK[^"]*"|"[^"]*RISK_BAND[^"]*"' contracts/*.sol \
  && echo "ERROR: forbidden term in runtime string" && exit 1 || echo "OK: strings clean"

Rationale

Why five separate objects rather than one struct? Each object has a different update lifecycle. Geological facts, legal records, extraction state, and ESG disclosures evolve independently. Separating them allows partial re-anchoring without invalidating the whole record.

Why hash anchoring rather than full storage? Full mining and ESG documents are too large and costly for on-chain storage. SHA-256 hashes of canonical off-chain JSON and reports provide tamper-evident verification while keeping the contract gas-efficient.

Why register before anchor? Creating an asset directly as ANCHORED can produce an invalid record with no object hashes. The register() -> advanceLifecycle() -> anchor() flow enforces administrative review before an asset becomes fully anchored.

Why explicit production transitions? Production state is not strictly numeric. PRODUCTION -> CARE_AND_MAINTENANCE -> PRODUCTION is a legitimate real-world cycle. A transition table encodes these allowed non-linear moves while keeping CLOSED terminal.

Why caller-supplied attestation timestamp? CP/QP signatures are produced off-chain before transaction submission. Using block.timestamp in the signed digest would require the signer to know a future block timestamp. A caller-supplied timestamp solves this while remaining tamper-evident because it is included in the EIP-712 payload.

Why replay protection and object-hash matching? Replay protection prevents duplicate submissions of the same signed payload. Object-hash matching prevents a valid CP/QP signature from being attached to an unrelated hash under an asset ID.

Why no on-chain scoring? Disclosure-quality scoring depends on document dates, field completeness, decay weights, ESG document counts, and governance-controlled parameters. These are off-chain concerns. The contract anchors the resulting score history hash and exposes currentScore as informational only.

Backwards Compatibility

This ERC introduces a new interface and does not modify any existing ERC.

Implementations MAY extend ERC-20, ERC-721, ERC-1155, or permissioned-token systems and add ERC-8097 compliance as a metadata and disclosure layer. Such token logic is OPTIONAL and out of scope for this specification.

Reference Implementation

The reference implementation is available in the assets folder:

Key implementation notes:

  • register() creates an INDEXED asset and enforces slug uniqueness
  • advanceLifecycle() is single-step and cannot set ANCHORED
  • anchor() requires a VERIFIED asset, all object hashes, a report hash/URI, and score range validation
  • recordCPAttestation() verifies EIP-712 signatures, rejects replayed digests, and requires object-hash matching
  • advanceProductionStatus() uses an explicit transition table
  • updateDepletion() requires PRODUCTION and only accepts increasing depletion values

Security Considerations

CP/QP key compromise: If a CP/QP signing key is compromised, an attacker may produce fraudulent attestations. Implementations SHOULD support off-chain revocation workflows and re-attestation procedures.

CP/QP identity verification: This contract verifies that an address signed the EIP-712 payload. It does not verify that the address belongs to a credentialled CP/QP. Professional-body membership verification is off-chain unless an implementation adds an authorised-signer registry extension.

Attestation replay: Implementations MUST reject reused EIP-712 digests. Without this, the same signed payload can be submitted repeatedly.

Object-hash mismatch: Implementations MUST reject attestations for hashes that are not current hashes of the asset. Otherwise a valid signature could be attached to unrelated content.

Lifecycle bypass: Implementations MUST ensure advanceLifecycle() cannot skip states and cannot set ANCHORED. Only anchor() may set ANCHORED.

Depletion oracle trust: updateDepletion() depends on authorised relayers and off-chain evidence. The contract cannot independently verify physical extraction.

Re-anchoring and history: The current AssetRecord stores latest hashes. Historical hashes are recoverable from AssetAnchored and AssetReAnchored event logs. Verifiers SHOULD inspect both current state and event history.

Score as informational only: getCurrentScore() returns an off-chain computed disclosure-quality value. It MUST NOT be used as a risk rating, credit rating, investment recommendation, covenant trigger, collateralisation input, or liquidation trigger.

Forbidden term enforcement: The EVM cannot fully enforce source-language restrictions. Conformance depends on ABI/runtime-string CI checks and implementation discipline.

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