Skip to content

About

Farm-to-table food traceability on VeChainThor blockchain — batch lifecycle, split/merge provenance tree, QR code consumer page, VIP-191 fee delegation

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

Chadenn — Food Traceability on VeChainThor

From field to plate — Every production batch, transformation, transport and store reception is recorded immutably on the VeChainThor blockchain. The end consumer scans a QR code to view the full product history.

Solidity VeChain Tests License


Table of Contents


Overview

Chadenn implements a supply chain traceability system on VeChainThor. Each food batch travels through a chain of actors, each recording a checkpoint on-chain:

Producer → Processor → Logistician → Distributor → Consumer (read-only)

Key features:

  • Batch split (1 pallet → N boxes) and merge (N ingredients → 1 product)
  • Full provenance tree reconstruction across splits and merges
  • IPFS references for documents (certificates, photos) — only the CID is stored on-chain
  • Public consumer page — no wallet required to view a product's history
  • Fee delegation (VIP-191) — actors don't need to hold VTHO; the operator sponsors gas

Architecture

┌─────────────────────────────────────────────────────────────┐
│                    FRONTEND  (React + Vite)                  │
│                                                             │
│  /                 Landing + role detection                 │
│  /batch/:id        Public consumer page (no wallet)        │
│  /producer         Create batch                            │
│  /processor        Transform / split / merge               │
│  /logistician      Record transport checkpoint             │
│  /distributor      Confirm store reception                 │
│  /admin            Manage roles + generate QR codes        │
└─────────────────────────────────────────────────────────────┘
                          │ @vechain/dapp-kit-react v2
                          │ ABIContract + Clause (sdk-core)
                          ▼
┌─────────────────────────────────────────────────────────────┐
│                  VECHAIN THOR NODE                           │
│      Solo (local Docker)  /  Testnet  /  Mainnet            │
└─────────────────────────────────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────────┐
│                   SMART CONTRACTS                            │
│                                                             │
│  RoleManager.sol      OpenZeppelin AccessControl            │
│    PRODUCER_ROLE  PROCESSOR_ROLE                           │
│    LOGISTICIAN_ROLE  DISTRIBUTOR_ROLE                      │
│                                                             │
│  BatchRegistry.sol    Core traceability logic               │
│    createBatch()        → PRODUCER_ROLE                    │
│    splitBatch()         → PROCESSOR_ROLE (1 → N)           │
│    addTransformation()  → PROCESSOR_ROLE (N → 1)           │
│    addTransport()       → LOGISTICIAN_ROLE                 │
│    addReception()       → DISTRIBUTOR_ROLE                 │
│    getBatchHistory()    → public view (free)               │
└─────────────────────────────────────────────────────────────┘

Data Model

Batch {
  batchId     bytes32       // keccak256(actor + timestamp + nonce)
  creator     address
  productType string
  parcelId    string
  harvestDate uint256       // block.timestamp (±10s precision on VeChainThor)
  ipfsRef     string        // IPFS CID of associated documents
  parentIds   bytes32[]     // source batches (split: 1 parent, merge: N parents)
  childIds    bytes32[]     // derived batches
  history     Checkpoint[]  // ordered chronologically
}

Checkpoint {
  cpType      enum          // CREATION | TRANSFORMATION | TRANSPORT | RECEPTION
  actor       address
  timestamp   uint256
  ipfsRef     string
  extraData   bytes         // ABI-encoded — see table below
}
Checkpoint type extraData encoding
TRANSFORMATION abi.encode(bytes32[] inputBatchIds, string process)
TRANSPORT abi.encode(int16 tempMin, int16 tempMax, uint32 durationMinutes)
RECEPTION abi.encode(string storeId, string location)

Tech Stack

Layer Technology
Smart contracts Solidity 0.8.20, OpenZeppelin v5
Dev environment Hardhat + @vechain/sdk-hardhat-plugin v1.2
VeChain SDK @vechain/sdk-core, @vechain/sdk-network
Local node VeChain Thor solo node via Docker
Tests Hardhat / Mocha / Chai (44 tests)
Frontend React 18, TypeScript, Vite 5
Wallet integration @vechain/dapp-kit-react v2, VeWorld
QR codes qrcode.react
Routing React Router v6

VET / VTHO Model

VeChainThor uses a dual-token model:

Token Role
VET Value token — used for value transfers between accounts
VTHO Gas token — consumed to execute transactions (smart contracts)

VTHO is automatically generated by every account holding VET at a rate of 5×10⁻⁸ VTHO per VET per block. This decouples gas costs from the network's value token, keeping transaction fees predictable.


Fee Delegation (VIP-191)

Fee delegation allows a third party (the delegator, typically the system operator) to pay VTHO on behalf of transaction senders. This means actors in the supply chain (farmers, processors, logisticians…) don't need to hold any VTHO to use the system.

How it works:

  1. The actor signs the transaction
  2. The delegator co-signs (commits to paying the gas)
  3. The transaction is broadcast — VTHO is deducted from the delegator, not the actor

Configuration: set DELEGATOR_URL in .env to enable delegation.
Without it, actors pay their own VTHO (standard mode).


IPFS Storage (Pinata)

Large files (photos, PDF certificates) are not stored on-chain — only the IPFS CID is recorded in ipfsRef. The frontend integrates Pinata's free tier for uploading files directly from the browser.

Setup (free)

  1. Create an account at app.pinata.cloud
  2. Go to API Keys → + New Key — generate a JWT with pinFileToIPFS permission
  3. Add to frontend/.env:
VITE_PINATA_JWT=eyJhbGci...
VITE_PINATA_GATEWAY=https://gateway.pinata.cloud   # optional

How it works

Each dashboard form has a file upload button. When a file is selected:

  1. It is uploaded to Pinata via POST https://api.pinata.cloud/pinning/pinFileToIPFS
  2. The returned CID is automatically filled in the form
  3. The CID is stored on-chain in the ipfsRef field of the checkpoint
  4. The consumer page renders a clickable link to view the file at gateway.pinata.cloud/ipfs/{CID}

If VITE_PINATA_JWT is not set, the upload button shows a hint and you can paste a CID manually.

Free tier limits

Limit Value
Storage 1 GB
Files 100 uploads / month
Bandwidth Unlimited (public gateway)

Sufficient for a pilot with ~300 lots/year at ~3 MB per lot.


Prerequisites

  • Node.js ≥ 18
  • Docker + Docker Compose (for the local solo node)
  • VeWorld browser extension (for wallet connection in the frontend)

Installation

git clone https://github.com/your-username/chadenn.git
cd chadenn

# Install all dependencies (root + frontend via workspaces)
npm install

# Copy environment file and fill in your values
cp .env.example .env

Quick Start

# 1. Start the local VeChain solo node
docker compose up -d

# 2. Compile contracts + generate TypeScript types
npm run compile

# 3. Run the full demo (deploys contracts, records a complete lifecycle, prints the consumer URL)
npm run demo

# 4. Start the frontend
cd frontend
cp .env.example .env   # addresses are auto-filled by npm run demo
npm run dev
# → open http://localhost:5173

Tip: npm run demo automatically writes contract addresses into frontend/.env.
Just run cd frontend && npm run dev afterwards.


Running Tests

Tests run against the built-in Hardhat network — no Docker required.

npm test
  BatchRegistry
    createBatch          ✔ 9 tests
    addTransformation    ✔ 7 tests
    splitBatch           ✔ 5 tests
    addTransport         ✔ 4 tests
    addReception         ✔ 2 tests
    full lifecycle       ✔ 2 tests
    unauthorized writes  ✔ 5 tests

  RoleManager            ✔ 11 tests

  44 passing (2s)

Coverage report:

npm run test:coverage

Deployment

Solo node (local)

# Start Docker first
docker compose up -d

# Set SOLO_PRIVATE_KEY in .env (see pre-funded accounts in VeChain SDK docs)
npm run deploy:solo
# → deployments/solo.json

Testnet

# Fund your account via https://faucet.vecha.in/
# Set TESTNET_PRIVATE_KEY in .env
npm run deploy:testnet
# → deployments/testnet.json
# → verify on https://explore-testnet.vechain.org

Environment variables

Variable Description
SOLO_PRIVATE_KEY Private key for the local solo node deployer
SOLO_URL Solo node URL (default: http://localhost:8669)
TESTNET_PRIVATE_KEY Private key for testnet deployments
TESTNET_URL Testnet URL (default: https://testnet.vechain.org)
DELEGATOR_URL Fee delegator server URL (optional)
REPORT_GAS Set to true to print gas usage per function

Demo Script

The demo script simulates a complete product lifecycle on-chain:

Harvest (tomatoes) → Split into lots → Create basil batch
→ Merge Lot A + Basil → Tomato basil sauce
→ Transport to store → Store reception
→ Consumer reads full history + provenance tree
npm run demo

Output includes a ready-to-use consumer URL:

→ http://localhost:5173/batch/0x5477f0071ac5be8755f...

If deployments/solo.json already exists, the demo reuses the deployed contracts instead of redeploying — keeping the consumer URL consistent with the frontend.

After a Docker restart (blockchain state is lost):

rm deployments/solo.json
npm run demo

Frontend

cd frontend
cp .env.example .env   # fill VITE_BATCH_REGISTRY_ADDRESS and VITE_ROLE_MANAGER_ADDRESS
npm run dev            # → http://localhost:5173

Pages

Route Description Wallet required
/ Landing page + role detection No
/batch/:id Full product history + QR code No
/producer Create a new production batch Yes
/processor Transform, split or merge batches Yes
/logistician Record transport checkpoint Yes
/distributor Confirm store reception + generate QR Yes
/admin Grant/revoke roles + QR code generator Yes

Frontend environment variables

Variable Description
VITE_NODE_URL VeChain node URL (default: http://localhost:8669)
VITE_GENESIS Network: solo | test | main
VITE_BATCH_REGISTRY_ADDRESS Deployed BatchRegistry address
VITE_ROLE_MANAGER_ADDRESS Deployed RoleManager address

Smart Contracts

RoleManager.sol

Extends OpenZeppelin AccessControl. Only the DEFAULT_ADMIN_ROLE can grant or revoke actor roles.

function grantActorRole(bytes32 role, address account) external onlyRole(DEFAULT_ADMIN_ROLE)
function revokeActorRole(bytes32 role, address account) external onlyRole(DEFAULT_ADMIN_ROLE)

Accepted roles: PRODUCER_ROLE, PROCESSOR_ROLE, LOGISTICIAN_ROLE, DISTRIBUTOR_ROLE.

BatchRegistry.sol

// Create a new production batch (PRODUCER_ROLE)
function createBatch(
    string calldata productType,
    string calldata parcelId,
    uint256 harvestDate,       // 0 = use block.timestamp
    string calldata ipfsRef,
    bytes32[] calldata parentIds   // empty for fresh batch, [parentId] for split child
) external returns (bytes32 batchId)

// Transform N input batches into 1 output batch (PROCESSOR_ROLE)
function addTransformationCheckpoint(
    bytes32[] calldata inputBatchIds,
    string calldata productType,
    string calldata ipfsRef,
    string calldata process
) external returns (bytes32 outputBatchId)

// Split 1 batch into N children — minimum 2 (PROCESSOR_ROLE)
function splitBatch(
    bytes32 parentBatchId,
    SplitParams[] calldata children
) external returns (bytes32[] memory childBatchIds)

// Record transport (LOGISTICIAN_ROLE)
function addTransportCheckpoint(
    bytes32 batchId, int16 tempMin, int16 tempMax,
    uint32 durationMinutes, string calldata ipfsRef
) external

// Record store reception (DISTRIBUTOR_ROLE)
function addReceptionCheckpoint(
    bytes32 batchId, string calldata storeId,
    string calldata location, string calldata ipfsRef
) external

// Read full batch history — public, free, no wallet needed
function getBatchHistory(bytes32 batchId) external view returns (BatchInfo memory)

Events

Event Emitted by
BatchCreated(batchId, creator, productType) createBatch, addTransformationCheckpoint, splitBatch
CheckpointAdded(batchId, cpType, actor, timestamp) All write functions
BatchSplit(parentId, childIds[]) splitBatch
BatchMerged(parentIds[], outputId) addTransformationCheckpoint (N > 1 inputs)

Project Structure

chadenn/
├── contracts/
│   ├── interfaces/
│   │   ├── IBatchRegistry.sol      Public interface + type definitions
│   │   └── IIPFSStorage.sol        Mockable IPFS storage interface
│   ├── RoleManager.sol
│   └── BatchRegistry.sol
│
├── test/
│   ├── RoleManager.test.ts         11 tests
│   └── BatchRegistry.test.ts       33 tests (lifecycle, split/merge, access control)
│
├── scripts/
│   ├── _vechain-deploy.ts          VeChain deployment helper (address fix)
│   ├── deploy-solo.ts              Deploy to local solo node
│   ├── deploy-testnet.ts           Deploy to VeChain testnet
│   └── demo.ts                     Full lifecycle simulation
│
├── frontend/
│   └── src/
│       ├── hooks/
│       │   ├── useBatchRegistry.ts Contract reads + writes
│       │   └── useRoles.ts         Role detection + management
│       ├── pages/
│       │   ├── BatchPublicPage.tsx  /batch/:id  (public)
│       │   ├── ProducerDashboard.tsx
│       │   ├── ProcessorDashboard.tsx
│       │   ├── LogisticianDashboard.tsx
│       │   ├── DistributorDashboard.tsx
│       │   └── AdminDashboard.tsx
│       ├── components/
│       │   ├── Timeline.tsx        Checkpoint timeline
│       │   ├── QRCodeDisplay.tsx   QR code generator
│       │   ├── Navbar.tsx
│       │   └── TxStatus.tsx        Transaction feedback
│       ├── types/index.ts
│       ├── utils/helpers.ts        ABI decoding, formatting
│       └── config.ts               Addresses + role hashes
│
├── deployments/                    Generated after deploy (gitignored)
├── docker-compose.yml              VeChain Thor solo node
├── hardhat.config.ts               Networks: vechain_solo, vechain_testnet
├── .env.example
└── README.md

Known Issues

Hardhat plugin contract address mismatch

@vechain/sdk-hardhat-plugin v1.x computes deployed contract addresses using the Ethereum formula (keccak256(rlp([sender, nonce]))), but VeChainThor uses a different derivation. As a result, ContractFactory.deploy().getAddress() returns an incorrect address.

Fix: scripts/_vechain-deploy.ts works around this by sending the deployment transaction manually via signer.sendTransaction() and reading the actual address from the VeChain REST API receipt (outputs[0].contractAddress).

Hardhat receipt logs

The VeChain Hardhat plugin does not populate receipt.logs in the standard Ethers.js format. Events must be fetched from the VeChain REST API (GET /transactions/:txid/receipt) and decoded manually. The helper function getVeChainEvents() in _vechain-deploy.ts handles this.

block.timestamp precision

VeChainThor has a ~10-second block interval. block.timestamp is reliable for ordering but not for sub-minute precision. This is documented in the contract source.


Contributing

  1. Fork the repository
  2. Create a feature branch: git checkout -b feat/my-feature
  3. Commit your changes (run npm test first)
  4. Open a pull request

License

MIT

About

Farm-to-table food traceability on VeChainThor blockchain — batch lifecycle, split/merge provenance tree, QR code consumer page, VIP-191 fee delegation

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages