---
eip: 8047
title: Forensic Token (Forest)
description: A DAG-based token enabling hierarchical traceability and efficient enforcement for compliant digital assets.
author: Sirawit Techavanitch (@MASDXI) <sirawit_tec@live4.utcc.ac.th>, Supachate Innet <supachate_inn@utcc.ac.th>
discussions-to: https://ethereum-magicians.org/t/erc-8047-forensic-token-forest/25786
status: Draft
type: Standards Track
category: ERC
created: 2025-10-15
requires: 165, 1155, 5615
---

## Abstract

Forensic Token (Forest) is a directed acyclic graph (DAG) inspired token model designed to enhance traceability and regulatory compliance in digital currency or e-Money systems. By introducing hierarchical token tracking, it enables efficient enforcement on any token linked to suspicious activity with depth/root. Enforcement actions, such as freezing specific tokens or partitioning all tokens with relational links, are optimized to operate at $O(1)$ complexity.

## Motivation

The Central Bank Digital Currency and Private Money concept aim to utilize the advantages of Blockchain or Distributed Ledger Technology that provide immutability, transparency, and security, and it adopts smart contracts, which play a key role in creating programmable money. However, technology itself gives an advantage and eliminates the ideal problem of compliance with the regulator and the Anti-Money Laundering and Countering the Financing of Terrorism (AML/CFT) standard, but it does not seem practical to be done in the real world and is not efficiently responsible for the financial crime or incidents that occur in the open network of economics.

Financial crime incident response actions, like freezing accounts or funds, typically necessitate further analysis to pinpoint illicit transactions. This process is off-chain; it can be slow and inefficient. Many existing solutions focus primarily on prevention by attempting to predict bad actors in advance; however, human behavior changes over time, sometimes immediately, especially during periods of economic stress, which may make such approaches unreliable.

Therefore, preventive controls alone cannot fully eliminate bad actors, an inevitable risk in open financial systems. Rather than attempting to predict malicious behavior, there is a need for systems that can respond to incidents faster and more precisely once they occur. The Forensic Token (Forest) is designed to address this need by providing native, on-chain traceability and enforcement at the token depth, enabling targeted actions that reduce operational metrics such as Mean Time To Resolve (MTTR) and Mean Time To Fix (MTTF) while preserving on-chain programmability.

## 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](https://www.rfc-editor.org/rfc/rfc2119) and [RFC 8174](https://www.rfc-editor.org/rfc/rfc8174).

Compatible implementations MUST implement the `IERC8047` interface and MUST inherit from [ERC-1155](./eip-1155.md) and [ERC-5615](./eip-5615.md) interfaces. All functions defined in the interface MUST be present and all function behavior MUST meet the behavior specification requirements below.

```solidity
// SPDX-License-Identifier: CC0-1.0
pragma solidity >=0.8.0 <0.9.0;

/**
 * @title ERC-8047 interface
 */

// import "./IERC1155.sol";
// import "./IERC5615.sol";

// The EIP-165 identifier of this interface is `0xa4afd005`.
interface IERC8047 /**is IERC1155, IERC5615 */ {
    /**
     * @dev Structure representing a token (node) within the Forest DAG.
     */
    struct Token {
        uint256 root;
        uint256 parent;
        uint256 value;
        uint96 depth;
        address owner;
    }

    /**
     * @notice Emitted when a new token is created within a DAG.
     * @param root The root token ID of the DAG to which the new token belongs.
     * @param id The ID of the newly created token.
     * @param from The address that created/minted the token.
     */
    event TokenCreated(
        uint256 indexed root,
        uint256 id,
        address indexed from
    );

    /**
     * @notice Emitted when a token is spent or partially spent.
     * @param root The root token ID of the DAG to which the new token belongs.
     * @param id The ID of the token being spent.
     * @param value The amount of the token that was spent.
     */
    event TokenSpent(
      uint256 indexed root,
      uint256 indexed id,
      uint256 value
    );

    /**
     * @notice Emitted when multiple tokens are successfully merged into a single new token.
     * @param ids The array of original token IDs that were consumed in the merge.
     * @param id The ID of the newly created merged token.
     * @param from The address of the token owner who initiated the merge.
     * @param mergeType A flag indicating the rule set used for the merge.
     * `0` represents the default merge (all tokens from the same DAG).
     * values > 0 are reserved for custom implementations (e.g., cross dags merges).
     */
    event TokenMerged(uint256[] ids, uint256 indexed id, address indexed from, uint8 mergeType);

    /**
     * @notice Retrieves the latest (highest) depth of the DAG that a given token belongs to.
     * @param id The ID of the token.
     * @return uint256 The latest DAG depth for the token.
     */
    function latestDAGDepthOf(uint256 id) external view returns (uint256);

    /**
     * @notice Retrieves the depth of a token within its DAG.
     * @param id The ID of the token.
     * @return uint256 The depth of the token in the DAG.
     */
    function depthOf(uint256 id) external view returns (uint256);

    /**
     * @notice Retrieves the owner of a given token.
     * @param id The ID of the token.
     * @return address The address that owns the token.
     */
    function ownerOf(uint256 id) external view returns (address);

    /**
     * @notice Retrieves the parent token ID of a given token.
     * @param id The ID of the token.
     * @return uint256 The ID of the parent token. Retrieves 0 if the token is a root.
     */
    function parentOf(uint256 id) external view returns (uint256);

    /**
     * @notice Retrieves the root token ID of the DAG to which a given token belongs.
     * @param id The ID of the token.
     * @return uint256 The root token ID of the DAG.
     */
    function rootOf(uint256 id) external view returns (uint256);

    /**
     * @notice Retrieves token detail from given token id.
     * @param id The ID of the token.
     * @return Token struct containing the token's detailed properties.
     */
    function token(uint256 id) external view returns (Token memory);

    /**
     * @notice Retrieves the total value of all tokens currently in circulation.
     * Each token contributes its current `value` to the total.
     * @custom:overloading of {IERC5615.totalSupply}
     * @return uint256 The sum of all token values currently in circulation.
     */
    function totalSupply() external view returns (uint256);
}

```

### Behavior Specification

#### Minting

- In the interface does not define an explicit `mint` function.
  A `mint` operation is identified by intent: any operation that
  creates a new token, thereby adding to the total circulating supply, is considered a `mint`. Implementations MAY expose a `mint` function or
  integrate minting logic within another operation, provided
  the resulting token satisfies the properties defined below.
- The `value` MUST NOT be zero. If value is zero, the mint operation MUST revert.
- When minting a token, the `id` MUST NOT be supplied by the minter; the `id` MUST be generated via a contract-side mechanism. See [Contract-side ID Generation](#contract-side-id-generation) for the reasoning behind this requirement.
- When minting a token, the `root` property of the new token MUST be set to its own `id` and the `parent` property of the new token MUST be set to zero to explicitly indicate that the token serves as the `root` of a new DAG.
- The event `TokenCreated` MUST be emitted when the minting token operation is successful.
- The `TokenCreated` event MUST be emitted with `root` set to zero when minting a new root token, enabling off-chain indexers to identify and enumerate all DAG origins by filtering on `root` equal to zero.

#### Example Minting Scenario

Scenario when a new token is created without a parent. The resulting token serves as the root of a new DAG.

<div align="center">
  <img alt="mint" src="../assets/eip-8047/erc8047-mint.svg" width="420"/>
</div>

Mint Events emitted:

- TokenCreated(0x1A..., 0x1A..., address(0))
- TransferSingle(operator, address(0), Alice, 0x1A..., 100)

---

Scenario when the token is spent, a new child token is created. The parent token is either mutated partial spend or full spend.

<div align="center">
  <img alt="partial spend" src="../assets/eip-8047/erc8047-partial-spend.svg" width="420"/>
</div>

Partial Spend Events emitted:

- TokenSpent(0x0A..., 0x0A..., 50)
- TokenCreated(0x0A..., 0x1A, Alice)
- TransferSingle(operator, Alice, address(0), 0x0A..., 50)
- TransferSingle(operator, address(0), Bob, 0x1A..., 50)

---

<div align="center">
  <img alt="full spend" src="../assets/eip-8047/erc8047-full-spend.svg" width="420"/>
</div>

Full Spend Events emitted:

- TokenSpent(0x0A..., 0x0A..., 100)
- TokenCreated(0x0A..., 0x01A..., Alice)
- TransferSingle(operator, Alice, address(0), 0x0A..., 100)
- TransferSingle(operator, address(0), Bob, 0x1A..., 100)

#### Burning

- The interface does not define an explicit `burn` function.
  A `burn` operation is identified by intent: any operation that removes value from the total circulating supply by reducing a token's value is considered a `burn`. Implementations MAY expose a `burn` function or integrate burning logic within another operation, provided the resulting state satisfies the properties defined below.
- Burning a token is a soft delete operation. The token `id` MUST NOT be removed from the DAG. Instead, its `value` MUST be reduced by the burn amount (e.g., a token with a `value` of 1000 burned by 1000 results in a `value` of zero — the token `id` remains in the DAG with its full lineage intact).
- The burned token MUST NOT transfer ownership to the zero address nor create a new token to the zero address.
- The `TokenSpent` event MUST be emitted when the burning operation is successful.

#### Example Burning Scenario

Scenario partial burn, the token's `value` is reduced by the burn amount. The token remains in the DAG with its remaining value.

<div align="center">
  <img alt="partial burn" src="../assets/eip-8047/erc8047-partial-burn.svg" height="210"/>
</div>

Partial Burn Events emitted:

- TokenSpent(0xFF..., 0x2A..., 50)
- TransferSingle(operator, Alice, address(0), 0x2A..., 50)

---

Scenario full burn, the token's `value` is reduced to zero. The token id remains in the DAG with its lineage intact but is no longer spendable.

<div align="center">
  <img alt="full burn" src="../assets/eip-8047/erc8047-full-burn.svg" height="210"/>
</div>

Full Burn Events emitted:

- TokenSpent(0xFF..., 0x2A..., 100)
- TransferSingle(operator, Alice, address(0), 0x2A..., 100)

#### Existence

- To ensure conformance with [ERC-5615](./eip-5615.md), the `exists` function MUST return `true` for any `id` that has been created, even if its `value` is zero. Implementations MUST determine existence by verifying that the `root` of the `id` is not zero. Checking the token's `value` MUST NOT be used as an existence check, as a burned token retains its `id` in the DAG with a `value` of zero. See [Soft Delete and Forensic Persistence](#soft-delete-and-forensic-persistence) for the reasoning behind this requirement.

#### Spending

- The `safeTransferFrom` MUST verify that the `id` exists. If it does not, the function MUST revert.
- The `safeTransferFrom` MUST revert if the `from` address is equal to the `to` address.
- The `from` MUST be the owner of the `id` or an approved operator.
- The `value` to be spent MUST NOT be zero.
- The `value` to be spent MUST NOT exceed the `value` of the `id`. If it does, the function MUST revert.
- The `safeTransferFrom` function MUST mint a new `id` as a child of the `id` being spent. The new `id` MUST have its `parent` set to the `id` that was spent and its `depth` MUST be incremented by one relative to the `parent`.
- When `value` is less than the token's current `value`, the operation is considered a **partial spend**. The parent token's `value` MUST be reduced by the spent `value`.
- When `value` is equal to the token's current `value`, the operation is considered a **full spend**. The parent token's `value` MUST be set with zero.
- To maintain compatibility with [ERC-1155](./eip-1155.md), `safeTransferFrom` MUST emit two `TransferSingle` events on **full spend** to reflect the parent–child token behavior.
- One for burning the parent token `TransferSingle(operator, from, address(0), id, value)`.
- One for minting the new child token `TransferSingle(operator, address(0), to, newId, value)`.

- On **partial spend**, `safeTransferFrom` MUST emit two `TransferSingle` events. The first reflects the reduction of the parent token's `value`. The parent token remains in the DAG with a reduced `value`.
- One for reducing the parent token's `value` `TransferSingle(operator, from, address(0), id, value)`.
- One for minting the new child token `TransferSingle(operator, address(0), to, newId, value)`.
- Similarly, `safeBatchTransferFrom` MUST emit two `TransferBatch` events, preserving token order.
- First, for burning or reducing all parent tokens in the batch, MUST follow the order provided by the input `ids` array.
- Second, for minting all corresponding child tokens, they MUST match the same order of `ids` as the parent batch.

- The `TokenSpent` event MUST be emitted with the spent amount whenever the token is spent, whether partial or full.
- The `TokenCreated` event MUST be emitted whenever a new child token is successfully created.

#### Merging

- To maintain compatibility with standard indexers and wallets that support [ERC-1155](./eip-1155.md), implementations MUST emit the standard `TransferBatch` event transferring the consumed `ids` from the owner to the zero address to reflect their consumption. Additionally, a standard `TransferSingle` and `TokenCreated` event MUST be emitted for the newly minted merged token `id`.
- To merge multiple tokens into a new `id`, all input tokens MUST share the same `root`. The resulting lineage is defined by selecting the input token with the greatest `depth` as the new parent, breaking any ties by choosing the first token listed in the input array. The depth of the new token is then set to the selected parent's `depth` plus one.
- The `TokenMerged` event MUST be emitted, including all `ids` involved in the merge, when the merging operation is successful.
- Implementations MAY allow merging tokens from different `root`. If a merge occurs across different DAGs, the implementation MUST define a deterministic rule for assigning the `root` of the new token. (e.g., inheriting the `root` of the token with the highest `value` or the lowest `depth`). Implementers MUST carefully consider the consequences of cross-DAG merging, as it combines previously independent asset lineages. This makes the lineage less clean and complicates forensic tracking, as enforcement actions or risk profiles associated with any of the original `root` will now propagate to the newly merged token `id`. Before executing a cross-DAG merge, implementations MAY enforce rules ensuring sufficient transaction confirmations or adequate confidence levels. Furthermore, when signaling this custom behavior via the `TokenMerged` event, implementations MUST use a `mergeType` flag strictly greater than zero, as zero is reserved exclusively for the `default` same DAG merge operation.
- The validation step MAY be implemented before the merging logic executes. This leaves room for implementation-specific rules, such as gatekeeper, limit amount, etc.

#### URI JSON Schema

In this proposal, each token has a unique `id` to track its movement in the `DAG` (like serial numbers), but all tokens representing the same asset share a single metadata URI. This reflects the fungible nature of the asset (like fiat currency).

- All tokens of the same asset MUST reference the same URI, regardless of their individual `id`.
- Implementations SHOULD follow the JSON Schema definition provided below for consistency across client implementations.

```json
{
  "title": "Token Metadata",
  "description": "Metadata schema for ERC-8047: Forensic Token (Forest).",
  "type": "object",
  "properties": {
    "name": {
      "type": "string",
      "description": "Human-readable name of the asset represented by this token."
    },
    "symbol": {
      "type": "string",
      "description": "Ticker symbol or shorthand identifier for the token."
    },
    "decimals": {
      "type": "integer",
      "description": "Number of decimal places used to display token amounts. For example, 18 means the token amount should be divided by 10^18 to get its user representation."
    },
    "description": {
      "type": "string",
      "description": "Detailed description of the asset represented by this token."
    },
    "image": {
      "type": "string",
      "format": "uri",
      "description": "A URI pointing to an image (MIME type image/*) that visually represents the asset. Recommended image width: 320–1080 pixels; aspect ratio: between 1.91:1 and 4:5."
    },
    "properties": {
      "type": "object",
      "description": "Container for extended metadata such as compliance, traceability, and DAG lineage.",
      "properties": {
        "compliance": {
          "type": "object",
          "description": "Compliance and policy information for the asset.",
          "properties": {
            "issuer": {
              "type": "string",
              "description": "Legal entity responsible for issuing or managing this asset."
            },
            "jurisdiction": {
              "type": "string",
              "description": "Legal jurisdiction or regulatory domain governing this asset."
            },
            "policies": {
              "type": "string",
              "format": "uri",
              "description": "URI linking to AML/CFT, compliance, or risk policy documentation."
            },
            "enforcement_authority": {
              "type": "string",
              "format": "uri",
              "description": "URI to the entity or endpoint responsible for enforcement actions (e.g., freeze, revoke)."
            }
          },
          "required": ["issuer", "policies"]
        }
      },
      "required": ["compliance"]
    }
  },
  "required": ["name", "description", "image", "properties"]
}
```

A complete JSON Schema reference for [ERC-8047](./eip-8047.md) metadata is provided below for validation and implementation guidance.

```json
{
  "name": "United States Dollar",
  "symbol": "USD",
  "decimals": 18,
  "description": "A compliant, traceable digital representation of the U.S. Dollar using the ERC-8047: Forensic Token (Forest).",
  "image": "https://acmee-finance.invalid/assets/images/USD_icon.png",
  "properties": {
    "compliance": {
      "issuer": "Acmee Finance Inc.",
      "jurisdiction": "US-NY",
      "policies": "https://acmee-finance.invalid/policies",
      "enforcement_authority": "https://acmee-finance.invalid/enforcement"
    }
  }
}
```

## Rationale

### Contract-side ID Generation

The token ID is generated dynamically by the contract-side upon execution, rather than supplied by the caller. Because employs a Unspent Transaction Output (UTXO)-like mechanism where each transfer effectively spends an existing token and mints a new one to continue the DAG lineage, allowing caller-supplied IDs for these newly spawned tokens introduces critical attack vectors. Such vulnerabilities include ID collisions, unauthorized overwriting of lineage records, or root impersonation. Enforcing deterministic, contract-side ID generation at the time of the call guarantees global uniqueness and preserves the structural integrity of the lineage.

### Transaction Flow Consistency

Unlike the UTXO model, the Forest architecture permits stateful mutations of existing tokens while enforcing strict parent–child lineage. Tokens support fractional, iterative expenditures until depletion. By natively embedding parent references within each token, the architecture optimizes for reverse topological traversal. This enables highly efficient back-to-root queries—isolating a specific token's lineage up to its origin without the computational overhead of full DAG traversal. This continuous topology inextricably links all child nodes back to their roots, guaranteeing deterministic forensic traceability that traditional, aggregated account-based standards like [ERC-20](./eip-20.md) or [ERC-3643](./eip-3643.md) fundamentally lack this granular traceability, as they obfuscate individual token flows into aggregated account balances.

<div align="center">
  <img alt="A" src="../assets/eip-8047/erc8047-bau.svg" width="800"/>
</div>

<div align="center">
  <img alt="A" src="../assets/eip-8047/erc8047-depth2.svg" width="800"/>
</div>

<div align="center">
  <img alt="A" src="../assets/eip-8047/erc8047-depth3.svg" width="800"/>
</div>

### Reverse Topological Ordering of Tokens

The forest token-based model it natively supports reverse topological traversal. Each token stores a reference to its parent token, allowing to efficiently iterate from any given token back to its root token of the DAG. This back-to-root traversal differs from a full DAG traversal. It only follows the lineage of a specific token ID up to its root, rather than visiting all tokens in the DAG.

### Variable Packing

The property depth returns `uint96` as this offers the maximum possible precision that fits within the same storage slot as the owner address. Since an address occupies 160 bits, exactly 96 bits remain available in the 256 bits word. Utilizing `uint96` ensures zero wasted space.

From a functional perspective, `uint96` allows for a tree depth of , which is for all practical purposes infinite. Even in an extreme scenario on a high-performance network or Layer 2 with a 250ms block time that produces 4 blocks per second, assuming a transaction increases the tree depth every single block

| Metric                 | Value / Calculation                                                                                                            |
| :--------------------- | :----------------------------------------------------------------------------------------------------------------------------- |
| **Network block time** | 250 ms                                                                                                                         |
| **Seconds per year**   | &approx; 31,536,000 seconds                                                                                                    |
| **Blocks per year**    | 4 &times; 31,536,000 = 126,144,000 blocks                                                                                      |
| **Years to overflow**  | 2<sup>96</sup> / 126,144,000 = 79,228,162,514,264,337,593,543,950,336 / 126,144,000 &approx; 6.2 &times; 10<sup>20</sup> years |

This timeframe is orders of magnitude longer than the current known age of the universe (&approx; 1.38 &times; 10<sup>10</sup> years). Therefore, limiting the depth to `uint96` to achieve storage packing imposes no realistic constraint on the system's longevity or throughput.

### Soft Delete and Forensic Persistence

Tokens are never removed from the DAG when it's create. Removing a burned token would destroy its lineage record, breaking the forensic chain between parent and child tokens. Any enforcement action applied to a root or depth must remain traceable to all tokens that were ever part of that DAG family, including those that have been fully spent. Hard deletion is therefore incompatible with the forensic guarantees this standard provides.

### Multi-Depth Compliance Enforcement

Traditional systems are enforced at the account level. This often means freezing an entire wallet just to stop one bad transaction, which unfairly locks up a user's legitimate funds. Forest solves this by applying rules to both the account and the individual tokens. It works like pruning a tree rather than chopping it down. This precision allows authorities to target only the specific illicit assets while leaving the rest of the user's portfolio untouched and fully operational.

### Constant-Time Enforcement

The constant-time enforcement claim refers to the cost of applying an enforcement action relative to the size of the DAG, total token count, or number of tokens sharing the same root. Tokens sharing the same root form a single DAG family. Enforcement actions applied at the root or depth propagate implicitly to all linked tokens within that family without iteration. Regardless of how large the DAG grows, enforcement cost remains constant. For a reference implementation, see [Token Policy Enforcement (TPEn)](#token-policy-enforcement-tpen).

### Spendable Balance via off-chain

On-chain iteration to retrieve spendable balance can be gas-intensive and inefficient, especially for large DAGs or multiple sets of DAGs. To address this, the current spendable balance of account can be determined off-chain by deploying a service that subscribes to events emitted by the contract. This service calculates the spendable balance by reconciling the account's total balance of with any tokens that have been frozen or restricted due to hierarchical or forensic rules, providing an accurate representation of the amount available for spend.

## Backwards Compatibility

This standard is fully compatible with [ERC-1155](./eip-1155.md) and [ERC-5615](./eip-5615.md).

## Reference Implementation

For reference implementation can be found [here](../assets/eip-8047/README.md),

### Token Policy Enforcement (TPEn)

The following abstract contract provides a reference implementation of the TPEn. It demonstrates the gas-optimized logic required to evaluate and apply topological DAG quarantines using 256-bit storage packing and bitwise operations. Furthermore, this bucket-based design natively enables mass-quarantine capabilities, laying the groundwork for regulators to simultaneously freeze or unfreeze up to 256 distinct topological depths in a single transaction by passing a pre-computed bitmask.

Each DAG depth maps to a 256-bit storage bucket and a specific bit position within that bucket using bitwise operations:

| Operation | Formula                          | Example depth = 300 |
| --------- | -------------------------------- | ------------------- |
| bucket    | depth >> 8 (i.e., depth / 256)   | 300 >> 8 = 1        |
| bitIndex  | depth & 0xFF (i.e., depth % 256) | 300 & 0xFF = 44     |

Each bucket covers 256 consecutive depths. A single `uint256` storage slot represents

depths `bucket^256` to `(bucket + 1)^256 - 1`.

| Bucket | Depths Covered            |
| ------ | ------------------------- |
| `0`    | `0` – `255`               |
| `1`    | `256` – `511`             |
| `2`    | `512` – `767`             |
| `n`    | `n^256 – (n + 1)^256 - 1` |

Freezing a depth sets the corresponding bit to 1 via bitwise OR. Unfreezing sets it to 0 via bitwise AND NOT. Checking freeze status reads the bit via bitwise AND.

```solidity
// SPDX-License-Identifier: CC0-1.0
pragma solidity >=0.8.0 <0.9.0;

/**
 * @title AbstractTokenPolicyEnforcement (TPEn)
 * @dev Abstract contract for managing O(1) multi-dimensional token quarantines.
 * @notice This contract allows regulators to freeze and unfreeze tokens using topological bounds, bitmasks, and discrete mapping.
 */
abstract contract AbstractTokenPolicyEnforcement {
    enum FREEZE_TYPES {
        NONE,
        LOWER_BOUND,
        UPPER_BOUND,
        DEPTH,
        DISCRETE
    }

    struct Policy {
        // uint128 is enough, since {IERC8047.tokens} store depth with uint92.
        uint128 beforeDepth;
        uint128 afterDepth;
        mapping(uint256 => bool) tokens;
        mapping(uint256 => uint256) bitmasks;
    }

    mapping(uint256 => Policy) private _policies;

    error TokenFrozen();
    error TokenNotFrozen();
    error DepthFrozen();
    error DepthNotFrozen();
    error ConflictingBounds();
    error InvalidUnfreezeTypes();
    error BoundNotSet();

    event FrozenToken(uint256 indexed tokenId);
    event FrozenBefore(uint256 indexed root, uint256 depth);
    event FrozenAfter(uint256 indexed root, uint256 depth);
    event FrozenDepth(uint256 indexed root, uint256 depth);

    event UnfrozenToken(uint256 indexed tokenId);
    event UnfrozenBefore(uint256 indexed root, uint256 depth);
    event UnfrozenAfter(uint256 indexed root, uint256 depth);
    event UnfrozenDepth(uint256 indexed root, uint256 depth);

    /**
     * @notice Calculates the 256-bit storage bucket and specific bit index for a given DAG depth.
     * @dev Uses pure bitwise operations in assembly for gas optimization.
     * @param depth The chronological depth (Y-axis) of the token in the DAG.
     * @return bucket The exact 256-depth chunk where the state is stored.
     * @return bitIndex The specific bit position (0-255) within that bucket.
     */
    function calcTokenBucketAndBitIndex(uint256 depth) private pure returns (uint256 bucket, uint256 bitIndex) {
        assembly ("memory-safe") {
            // right shift by 8 bits (equivalent to depth / 256)
            bucket := shr(8, depth)
            // bitwise AND 255 (equivalent to depth % 256)
            bitIndex := and(depth, 0xFF)
        }
    }

    /**
     * @notice Internal function to update the discrete frozen status of a specific token.
     * @param root The identifier of the DAG transaction family.
     * @param tokenId The unique identifier of the discrete asset.
     * @param freeze The target status (true to freeze, false to unfreeze).
     */
    function updateFreezeToken(uint256 root, uint256 tokenId, bool freeze) private {
        _policies[root].tokens[tokenId] = freeze;
        if (freeze) {
            emit FrozenToken(tokenId);
        } else {
            emit UnfrozenToken(tokenId);
        }
    }

    /**
     * @notice Evaluates if a token is frozen.
     * @param root The DAG transaction family ID.
     * @param tokenId The specific discrete asset token ID.
     * @param depth The topological depth of the token.
     * @return isFrozen Boolean indicating if the token is frozen.
     * @return freezeType The specific freeze type.
     */
    function isTokenFrozen(uint256 root, uint256 tokenId, uint256 depth) public view returns (bool, FREEZE_TYPES) {
        Policy storage policy = _policies[root];

        // boundary checks
        uint128 beforeDepth = policy.beforeDepth;
        uint128 afterDepth = policy.afterDepth;

        if (beforeDepth != 0 && depth <= beforeDepth) return (true, FREEZE_TYPES.LOWER_BOUND);
        if (afterDepth != 0 && depth >= afterDepth) return (true, FREEZE_TYPES.UPPER_BOUND);

        // bitmask check
        (uint256 bucket, uint256 bitIndex) = calcTokenBucketAndBitIndex(depth);
        if ((policy.bitmasks[bucket] & (1 << bitIndex)) != 0) {
            return (true, FREEZE_TYPES.DEPTH);
        }

        // specific token check
        if (policy.tokens[tokenId]) {
            return (true, FREEZE_TYPES.DISCRETE);
        }

        // fallback case
        return (false, FREEZE_TYPES.NONE);
    }

    /**
     * @notice Establishes a continuous lower bound. All tokens at or below this depth are frozen.
     * @dev Reverts if the requested depth overlaps with an existing upper bound.
     * @param root The DAG transaction family ID.
     * @param depth The DAG depth limit.
     */
    function freezeTokenBefore(uint256 root, uint256 depth) public virtual {
        Policy storage policy = _policies[root];
        if (policy.afterDepth != 0 && depth >= policy.afterDepth) revert ConflictingBounds();

        policy.beforeDepth = uint128(depth);
        emit FrozenBefore(root, depth);
    }

    /**
     * @notice Establishes a continuous upper bound. All tokens at or above this depth are frozen.
     * @dev Reverts if the requested depth overlaps with an existing lower bound.
     * @param root The DAG transaction family ID.
     * @param depth The DAG depth limit.
     */
    function freezeTokenAfter(uint256 root, uint256 depth) public virtual {
        Policy storage policy = _policies[root];
        if (policy.beforeDepth != 0 && depth <= policy.beforeDepth) revert ConflictingBounds();

        policy.afterDepth = uint128(depth);

        emit FrozenAfter(root, depth);
    }

    /**
     * @notice Completely lifts the continuous lower bound quarantine for a DAG family.
     * @param root The DAG transaction family ID.
     * @param depth The previous bound depth (logged for off-chain indexing).
     */
    function unfreezeTokenBefore(uint256 root, uint256 depth) public virtual {
        Policy storage policy = _policies[root];
        if (policy.beforeDepth == 0) revert BoundNotSet();

        policy.beforeDepth = 0;

        emit UnfrozenBefore(root, depth);
    }

    /**
     * @notice Completely lifts the continuous upper bound quarantine for a DAG family.
     * @param root The DAG transaction family ID.
     * @param depth The previous bound depth (logged for off-chain indexing).
     */
    function unfreezeTokenAfter(uint256 root, uint256 depth) public virtual {
        Policy storage policy = _policies[root];
        if (policy.afterDepth == 0) revert BoundNotSet();

        policy.afterDepth = 0;

        emit UnfrozenAfter(root, depth);
    }

    /**
     * @notice Applies an O(1) bitmask quarantine to a specific topological depth.
     * @dev Reverts if the targeted depth is already frozen to prevent redundant gas spend and duplicate events.
     * @param root The DAG transaction family ID.
     * @param depth The exact DAG depth to freeze.
     */
    function freezeDepth(uint256 root, uint256 depth) public virtual {
        (uint256 bucket, uint256 bitIndex) = calcTokenBucketAndBitIndex(depth);
        // load the current 256-bit bucket into memory.
        uint256 currentMask = _policies[root].bitmasks[bucket];
        uint256 targetBit = 1 << bitIndex;
        // check if the specific bit is already 1. If yes, revert.
        if ((currentMask & targetBit) != 0) revert DepthFrozen();
        // apply the bitwise OR and write back to storage.
        _policies[root].bitmasks[bucket] = currentMask | targetBit;

        emit FrozenDepth(root, depth);
    }

    /**
     * @notice Removes a specific topological depth from the bitmask quarantine.
     * @dev Reverts if the targeted depth is not currently frozen to prevent redundant gas spend.
     * @param root The DAG transaction family ID.
     * @param depth The exact DAG depth to unfreeze.
     */
    function unfreezeDepth(uint256 root, uint256 depth) public virtual {
        (uint256 bucket, uint256 bitIndex) = calcTokenBucketAndBitIndex(depth);
        // load the current 256-bit bucket into memory.
        uint256 currentMask = _policies[root].bitmasks[bucket];
        uint256 targetBit = 1 << bitIndex;
        // check if the specific bit is already 0. If yes, revert.
        if ((currentMask & targetBit) == 0) revert DepthNotFrozen();
        // apply the bitwise AND NOT and write back to storage.
        _policies[root].bitmasks[bucket] = currentMask & ~targetBit;

        emit UnfrozenDepth(root, depth);
    }

    /**
     * @notice Freezes a specific discrete token ID.
     * @param root The DAG transaction family ID.
     * @param tokenId The unique identifier of the token.
     * @param depth The topological depth of the token.
     */
    function freezeToken(uint256 root, uint256 tokenId, uint256 depth) public virtual {
        (bool isFrozen, ) = isTokenFrozen(root, tokenId, depth);
        if (isFrozen) revert TokenFrozen();

        updateFreezeToken(root, tokenId, true);
    }

    /**
     * @notice Unfreezes a specific discrete token ID.
     * @dev Reverts if the token is locked by a continuous bound or depth mask.
     * @param root The DAG transaction family ID.
     * @param tokenId The unique identifier of the token.
     * @param depth The topological depth of the token.
     */
    function unfreezeToken(uint256 root, uint256 tokenId, uint256 depth) public virtual {
        (bool isFrozen, FREEZE_TYPES types) = isTokenFrozen(root, tokenId, depth);

        if (!isFrozen) revert TokenNotFrozen();
        if (types != FREEZE_TYPES.DISCRETE) revert InvalidUnfreezeTypes();

        updateFreezeToken(root, tokenId, false);
    }
}

```

## Security Considerations

**Denial of Service (DoS) via Unbounded Loops**

When executing operations such as `safeBatchTransferFrom` or merging multiple tokens, the contract must iterate over arrays of token IDs. If these arrays are arbitrarily large, the transaction may exceed the network's block gas limit, causing the transaction to revert and temporarily locking the assets. Contract implementations and interacting decentralized applications (dApps) must enforce strict array length bounds (e.g., maximum batch limits) to prevent out-of-gas (OOG) attack vectors.

**Storage Overhead and Dust Accumulation**

Because forest represents assets as discrete nodes rather than aggregated account balances, active ledgers will continuously generate new token structs. This naturally leads to higher state storage consumption compared to standard fungible tokens. If a malicious actor spams an account with fractional micro-transactions, it could inflate the DAG and make subsequent batch-spending prohibitively expensive for the victim. To mitigate state bloat, implementations should consider establishing minimum transfer thresholds (dust limits) or restricting decimal precision to prevent unnecessary state fragmentation.

**Lineage Contamination during Merges**

If a custom implementation allows cross-DAG merging (combining tokens with different `root` properties), the resulting merged token will inextricably link the histories of both inputs. Wallet interfaces and smart contract routers must exercise extreme caution when aggregating inputs to fulfill a payment. Blindly merging tokens to optimize gas fees—as is common in standard UTXO wallets—may inadvertently contaminate a clean asset with the compliance risk profile of a tainted asset. Client applications should partition unspent tokens by their `root` identifiers to maintain lineage hygiene.

**Public Graph Exposure**

Forest provide transparent forensic auditability. Consequently, the parent-child linkages explicitly map the flow of funds in plaintext on the public ledger. While user addresses remain pseudonymous, the asset graph is trivial for third-party observers to trace. Implementers deploying to permissionless networks must operate under the assumption that all token derivation paths are public. Any requirements for transactional confidentiality must be handled at the application layer or via secondary privacy protocols.

## Copyright

Copyright and related rights waived via [CC0](../LICENSE.md).
