--- eip: 8097 title: In-Ground Asset Token description: A framework for natural resource assets on Ethereum with geological metadata, CP/QP attestation, depletion-aware state, and ESG primitives. author: Solomon Ashok (@solomonashok) discussions-to: https://ethereum-magicians.org/t/erc-8097-in-ground-asset-token-standard-for-geological-metadata-compliance-extraction-aware-token-models/26869 status: Review type: Standards Track category: ERC created: 2025-11-24 requires: 55, 165, 712 --- ## 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](./eip-712.md) 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](./eip-20.md), [ERC-721](./eip-721.md), [ERC-1155](./eip-1155.md), [ERC-3643](./eip-3643.md)) 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. | Abbreviation | Full name | Reference | |---|---|---| | JORC | Australasian Joint Ore Reserves Committee Code | jorc.org/code | | NI 43-101 | Canadian National Instrument 43-101 | securities-administrators.ca | | SAMREC | South African Mineral Reporting Codes | samcode.co.za | | PERC | Pan-European Reserves and Resources Reporting Committee | percreserves.eu | | S-K 1300 | US Securities and Exchange Commission mining disclosure standard | sec.gov | | CP/QP | Competent Person or Qualified Person — a named professional with specified credentials who signs or attests the technical report | Per 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](./eip-8097.md) implementation maintains a lifecycle-governed asset record and anchors six off-chain JSON object hashes: | Object | Code | Primary purpose | |---|---|---| | In-Ground Resource Object | IRO | What is in the ground | | In-Ground Geology Object | IGO | How the resource is known | | In-Ground Compliance Object | ICO | Legal, tenure, ownership, and permitting record | | In-Ground Extraction Object | IEXO | Production status and depletion record | | In-Ground Environmental Object | IEO | Environmental, social, and governance disclosures | | Score Mutation Log | SML | Off-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()`. ```text INDEXED -> CLAIMED -> VERIFIED -> ANCHORED ``` `advanceLifecycle()` MUST advance exactly one step and MUST NOT set `ANCHORED`. Only `anchor()` may set `ANCHORED`. ### Data Structures ```solidity 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: ```text 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. ```solidity 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. ```solidity 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. ```solidity 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%`. ```solidity 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`. ```solidity 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"`. ```solidity 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 ```solidity 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](./eip-712.md) typed structured data signing. The [EIP-712](./eip-712.md) domain MUST be: ```solidity EIP712Domain({ name: "ERC-8097 In-Ground Asset Token", version: "1", chainId: , verifyingContract:
}) ``` The signed type hash is: ```solidity 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](./eip-8097.md) asset ID MUST be deterministic for a registry slug and chain: ```solidity 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](./eip-8097.md) 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 | Rule | Requirement | |---|---| | R-FORBIDDEN-TERMS | `HIGH_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-MONOTONIC | `advanceLifecycle()` MUST advance exactly one step and MUST NOT accept `ANCHORED`; only `anchor()` may set `ANCHORED`. | | R-PRODUCTION-TRANSITIONS | Production status MUST use an explicit transition table; `PRODUCTION <-> CARE_AND_MAINTENANCE` is permitted and `CLOSED` is terminal. | | R-DEPLETION-MONOTONIC | Depletion MUST only increase and requires `PRODUCTION` status. | | R-CP-EIP-712 | CP/QP attestations MUST use [EIP-712](./eip-712.md) typed data with caller-supplied `attestationTimestamp`. | | R-CP-IDENTITY | The contract proves a signer address signed; professional credential verification is off-chain. | | R-CP-REPLAY | Every EIP-712 digest MUST be accepted at most once. | | R-CP-OBJECT-MATCH | Attested `objectHash` MUST match one of the asset's current six object hashes. | | R-REPORT-REQUIRED | `anchor()` MUST reject missing `reportPdfHash` or empty `reportIpfsUri`. | | R-SCORE-RANGE | `currentScore` MUST be in `[0, 1000]` in both `anchor()` and `updateSML()`. | | R-SLUG-UNIQUE | `mineSlug` MUST be unique. | | R-REANCHOR-HISTORY | Current storage may update; historical hashes MUST remain recoverable from events. | | R-NO-SCORING | The contract MUST NOT compute IGA Score or disclosure-quality indices. | | R-NO-RISK-ASSESSMENT | The contract MUST NOT perform financial, investment, or geological-quality assessment. | | R-FRESHNESS-OFFCHAIN | Freshness decay and lambda parameters are off-chain. | Recommended CI checks: ```bash 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](./eip-20.md), [ERC-721](./eip-721.md), [ERC-1155](./eip-1155.md), or permissioned-token systems and add [ERC-8097](./eip-8097.md) 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: - [IERC-8097.sol](../assets/eip-8097/contracts/IERC-8097.sol) — the interface as defined in this specification - [ERC-8097.sol](../assets/eip-8097/contracts/ERC-8097.sol) — concrete implementation of IERC-8097 - [ERC-8097.t.sol](../assets/eip-8097/test/ERC-8097.t.sol) — Foundry test suite covering normative requirements - [foundry.toml](../assets/eip-8097/foundry.toml) — build configuration - [README.md](../assets/eip-8097/README.md) — setup and usage instructions 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 Copyright and related rights waived via [CC0](../LICENSE.md).