Aman Gupta
← All projects

Ethereum RWA Tokenization & Indexing Platform

A full-stack Ethereum project for tokenizing real-world assets, indexing on-chain activity, and exposing the resulting state through a backend API.

Role -
Independent Project
Date -
Ongoing
Stack -
Solidity, Foundry, Ethereum, Node.js, Ethers.js, PostgreSQL, Indexing

I built this project to explore what a real-world asset tokenization system looks like beyond the smart contract itself. It combines a Solidity asset registry with ERC-20 and ERC-721 token contracts, a Node.js indexer that processes on-chain events, PostgreSQL for indexed state, and an Express API for querying that data.

The main focus was understanding the full flow from asset registration and token transfers on Ethereum to the backend systems needed to index, store and serve that data.

Architecture

The project is split into an on-chain protocol layer and an off-chain indexing and API layer.

                          Ethereum / EVM

                         AssetRegistry

                   ┌────────────┴────────────┐
                   │                         │
             ERC-20 AssetToken         ERC-721 AssetNFT
                   │                         │
            Fractional ownership       Unique ownership
                   │                         │
                   └────────────┬────────────┘

                              Events


                         Node.js Indexer

                  ┌─────────────┼─────────────┐
                  │             │             │
                  ▼             ▼             ▼
               assets       transfers    ownership state

                                  ┌─────────┴─────────┐
                                  │                   │
                           token_balances      token_ownership
                                  │                   │
                                  └─────────┬─────────┘

                                        PostgreSQL


                                      Express REST API


                                      API consumers

The AssetRegistry remains the authoritative source for asset lifecycle and verification state. The database is a derived representation used for querying and application-facing APIs.

Asset Registry

The AssetRegistry contract is responsible for registering and managing tokenized assets.

Each asset stores information such as:

  • Asset ID
  • Name
  • Asset type
  • Token standard
  • Valuation
  • Total token supply
  • Token contract address
  • Issuer
  • Asset lifecycle status
  • Verification state
  • Document hash
  • Verifier
  • Verification expiry
  • Status updater

The supported asset types are:

  • Real estate
  • Private equity
  • Debt
  • Commodity

The asset lifecycle currently includes:

Pending
Active
Paused
Closed
Rejected

Verification has its own state machine:

Pending
Verified
Rejected
Expired

The registry also controls authorized issuers and administrator-only verification and lifecycle operations.

I wanted the registry to handle more than token deployment. It acts as the place where the protocol keeps the authoritative state of the underlying asset and determines whether that asset can currently be transferred.

ERC-20 and ERC-721 Tokenization

Each registered asset receives its own token contract based on the selected token standard.

ERC-20

ERC-20 assets represent fractional ownership.

Asset #1
Real Estate Asset


AssetToken
ERC-20
0x...

This gives each tokenized ERC-20 asset its own dedicated token contract.

ERC-721

ERC-721 assets represent unique ownership.

Asset #2
Unique Asset


AssetNFT
ERC-721
0x...

The current implementation represents the asset as a single NFT and uses the asset ID as the token ID.

The two standards share the same registry but model ownership differently. ERC-20 assets maintain fungible balances, while ERC-721 assets maintain ownership of a specific token ID.

ERC-1155 is being evaluated separately for future use cases involving multiple token classes or semi-fungible representations, but it is not currently part of the implementation.

Transfer Restrictions

One of the parts I wanted to make explicit on-chain was the relationship between asset verification and token transfers.

Both token implementations consult:

AssetRegistry.isAssetTransferable(assetId)

before allowing secondary transfers.

The asset must be:

Active
+
Verified
+
Verification not expired

This means transferability is enforced by the token contracts themselves rather than relying on the backend or frontend to enforce the rule.

The administrator can also pause or close an asset, which prevents transfers even if the token holder otherwise has enough balance.

This makes the asset lifecycle part of the transfer rules instead of treating the token and the underlying asset as completely separate systems.

Event-Driven Blockchain Indexer

The off-chain side of the project is a Node.js indexer built with ethers.

The indexer continuously turns blockchain events into queryable PostgreSQL state.

It indexes:

  • AssetRegistered
  • ERC-20 Transfer
  • ERC-721 Transfer
  • Asset verification updates
  • Asset status updates

A simplified flow looks like:

Ethereum blocks


Node.js indexer

      ├── AssetRegistered
      ├── VerificationUpdated
      ├── StatusUpdated
      └── Transfer


         PostgreSQL

Instead of knowing every token contract in advance, the indexer discovers newly registered token addresses from the AssetRegistry.

It also tracks the token standard for each token address so the same ERC-20/ERC-721 Transfer event signature can be decoded and processed according to the correct ownership model.

Block Processing

The indexer processes blockchain data in bounded ranges rather than attempting to scan an arbitrary historical range in one operation.

The current implementation processes blocks in batches of 1000 and uses a five-block confirmation depth before indexing a block range.

The basic flow is:

latest block


subtract confirmation depth


safe block


process bounded block range


commit database changes


store checkpoint

The last processed block is stored in the indexer_state table, allowing the indexer to resume from its previous checkpoint after a restart.

Each batch is processed transactionally. If processing fails, database writes are rolled back and the checkpoint remains unchanged, allowing the batch to be retried.

The indexer also uses duplicate protection for transfer events through (transaction_hash, log_index), and the materialized balance updates are applied only when a new transfer record is inserted.

Transfer Indexing

ERC-20 and ERC-721 transfers use the same Transfer event topic, but the event has different semantics for the two standards.

The indexer therefore maintains a token-address-to-standard mapping and processes the event according to the registered token type.

For ERC-20 transfers, the indexer reads:

from
 to
amount

and updates the materialized balance state.

For ERC-721 transfers, the event contains a token ID and the indexer updates the current owner of that specific NFT.

Transfer records are identified using:

(transaction_hash, log_index)

so an already-indexed log is not inserted again.

Transfer discovery uses filtered eth_getLogs queries across the token contracts discovered through the registry rather than issuing a separate transfer query for every token contract.

PostgreSQL

The database stores both historical blockchain activity and derived current state.

assets

Stores the latest indexed state of each asset, including its token standard, metadata, lifecycle state, verification state, token address, issuer and verification information.

transfers

Stores historical transfer events for both ERC-20 and ERC-721 assets.

Each record includes:

  • Block number
  • Transaction hash
  • Log index
  • Token address
  • Sender
  • Recipient
  • Amount

For ERC-20 transfers, amount represents the transferred fungible quantity.

For ERC-721 transfers, it is stored as 1.

token_balances

Stores current materialized ERC-20 balances:

(token_address, holder_address) → balance

This allows holder and balance queries without reconstructing the complete transfer history for every request.

token_ownership

Stores current ERC-721 ownership:

(token_address, token_id) → owner_address

indexer_state

Stores the last processed block used by the indexer as its synchronization checkpoint.

REST API

The API is currently read-oriented and exposes the indexed blockchain state through Express.

Assets

GET /assets
GET /assets/:id

Asset discovery supports pagination and filtering by things such as:

  • Token standard
  • Asset type
  • Issuer
  • Asset status
  • Verification status

For example:

GET /assets?token_standard=ERC20
GET /assets?token_standard=ERC721
GET /assets?limit=50&offset=0
GET /assets?asset_status=Active
GET /assets?verification_status=Verified
GET /assets?asset_type=RealEstate
GET /assets?issuer=0x...

Filters can also be combined.

Pagination responses include the configured limit and offset, the total number of matching results, and whether another page is available.

Asset Transfers

GET /assets/:id/transfers

Returns paginated transfer history for a specific asset.

Asset Holders

GET /assets/:id/holders
GET /assets/:id/holders/:address

The ownership API uses the underlying token standard to determine what ownership data is returned.

For ERC-20 assets, holder queries expose current balances.

For ERC-721 assets, they expose the current owner of the NFT.

Global Transfers

GET /transfers

Global transfer history supports pagination and filtering by sender, recipient and token address:

GET /transfers?from=0x...
GET /transfers?to=0x...
GET /transfers?token=0x...

Address Transfer History

GET /addresses/:address/transfers

Returns indexed transfers in which the address participated as either the sender or recipient.

Token Balances

GET /balances/:token/:address

Returns the current materialized balance for a holder for a specific token contract.

API Validation

The API validates inputs such as:

  • Ethereum addresses
  • Pagination parameters
  • Asset types
  • Asset lifecycle status
  • Verification status

Invalid requests return structured error responses rather than passing invalid values directly to the database layer.

Local Development & Integration Testing

The project uses:

  • Anvil for local Ethereum execution
  • PostgreSQL for indexed blockchain data
  • Foundry for Solidity compilation, scripting and testing
  • Node.js for the indexer
  • Express for the REST API

There is also a local integration script that automates the main blockchain-to-database flow.

scripts/test-local.sh

The script resets indexed database state, deploys a fresh test registry, registers and verifies an asset, performs transfers, captures the deployed registry address, runs the indexer and prints the indexed results and checkpoint.

This gives the project a repeatable local path for testing the interaction between the contracts, indexer, database and API.

Testing

The Solidity side of the project uses Foundry across multiple testing layers.

Deterministic tests

The test suite covers:

  • Asset registration
  • Input validation
  • Issuer authorization
  • Administrator authorization
  • Verification and rejection
  • Lifecycle transitions
  • Transfer restrictions
  • Verification expiry
  • Token configuration
  • Terminal states
  • Supply boundaries

Fuzz testing

Fuzz tests explore different registration inputs, transfer amounts, transfer attempts before verification, transfer attempts after expiry and token-supply boundaries.

Invariant testing

Stateful invariant tests validate protocol properties such as:

  • Total token supply conservation
  • Holder balance conservation
  • Transfer restrictions while assets are paused
  • Consistency between asset and verification states across randomized lifecycle operations

These tests are useful for a stateful protocol because correctness depends on sequences of operations rather than isolated function calls.

Security Review

I also ran Slither against the Solidity contracts and reviewed the reported findings individually.

The protocol’s main trust assumptions are explicit in the current V1 design.

The administrator can:

  • Authorize issuers
  • Verify assets
  • Reject assets
  • Pause or close assets

Authorized issuers can register assets but cannot independently verify, reject or alter the lifecycle state of those assets.

The current design intentionally treats the administrator as a trusted authority. A production deployment could replace the single administrator with multisig or more granular role-based governance.

Design Decisions

On-chain state vs off-chain state

The contracts remain authoritative for asset lifecycle, verification and transferability.

PostgreSQL is treated as a derived read model rather than the source of truth.

This keeps protocol rules on-chain while allowing the backend to provide efficient application-facing queries.

Historical events vs materialized state

The project stores both original transfer events and current ownership/balance state.

Historical events are useful for activity and audit-style queries, while materialized state makes common balance and ownership queries much simpler.

Token-standard-aware indexing

ERC-20 and ERC-721 both emit Transfer, but the meaning of the event differs between the two standards.

Tracking the token standard for each registered contract lets the indexer share the same discovery pipeline while still applying the correct ownership logic.

Batch processing

Processing blocks in bounded ranges makes synchronization easier to restart and reason about.

The persistent checkpoint and transactional database writes allow a failed batch to be retried without advancing the checkpoint prematurely.

What I Learned

The part I found most useful was working through the boundary between the smart contracts and the backend.

Deploying an ERC-20 or ERC-721 contract is only one part of the problem. Once an application needs to search assets, show transfer history, query balances or determine current ownership, you need infrastructure that can continuously turn blockchain events into useful application state.

Building the indexer made me think much more carefully about things like:

  • Block checkpoints
  • Confirmation depth
  • Event identity
  • Batching
  • Transactional database writes
  • Replaying historical events
  • Materialized state
  • Keeping on-chain and off-chain state aligned

It also made the difference between historical blockchain data and application-friendly state much clearer to me.

Current Status

The project is ongoing.

The core tokenization, asset registry, indexing, PostgreSQL and REST API pieces are implemented, along with Foundry unit tests, fuzz tests, invariant tests and static analysis.

Current work includes improving reorg handling, historical backfill tooling, observability, integration coverage and production-oriented operational concerns.

ERC-1155 is also being evaluated separately for future use cases involving multiple token classes or semi-fungible asset representations, but it is not currently part of the implementation.