A comprehensive Ethereum smart contract system for managing user consent and data provenance on-chain. The system provides GDPR-compliant consent tracking, Kantara Initiative-compliant consent receipts, and complete data lineage management.
Testnet deployment: The DataProvenance contract is currently deployed on Base Sepolia (testnet). The other contracts in this repository (ConsentReceipt, Kantara, etc.) are available for local development but not yet deployed on a public network.
- Features
- Architecture
- Prerequisites
- Installation
- Quick Start
- Contract Documentation
- Usage Examples
- Testing
- Security
- API Reference
- License
- Consent Management: Give, revoke, and track user consent with expiration support
- Kantara-Compliant Receipts: Full implementation of Kantara Initiative consent receipt specification with 30+ consent types and 70+ purpose categories
- Data Provenance: Track data ownership, transformations, and access history using content hashes
- EIP-712 Signatures: Meta-transaction support for gasless consent operations
- Role-Based Access Control: Admin, Operator, and Auditor roles with granular permissions
- Delegated Operations: Authorize delegates to register data on behalf of users
- Audit Logging: Immutable audit trail for all consent and data operations
- Batch Operations: Gas-efficient batch processing for multiple operations
- Data Access Control: Granular access levels (Read, Transform, Full) with time-limited grants
- Right to Deletion: GDPR-compliant data deletion with cryptographic proofs
- Purpose Registry: Standardized purpose definitions aligned with GDPR legal bases
The system consists of interconnected smart contracts organized in layers:
┌─────────────────────────────────────────────────────────────────┐
│ Application Layer │
├─────────────────────────────────────────────────────────────────┤
│ IntegratedConsentProvenanceSystem │
│ (Coordinator linking consent verification with data operations)│
└─────────────────────────────────────────────────────────────────┘
│
┌─────────────────────┼─────────────────────┐
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ConsentReceipt │ │DataProvenance │ │ Supporting │
│ (Basic) │ │ (Tracking) │ │ Contracts │
└───────────────┘ └───────────────┘ └───────────────┘
│ │ │
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ Kantara │ │DataAccessCtrl │ │ ConsentProxy │
│ConsentReceipt │ │ │ │ (Delegation) │
└───────────────┘ └───────────────┘ └───────────────┘
│
┌───────────────────┼───────────────────┐
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ DataDeletion │ │ AuditLog │ │PurposeRegistry│
└───────────────┘ └───────────────┘ └───────────────┘
| Contract | Purpose |
|---|---|
ConsentReceipt |
Basic consent tracking with EIP-712 meta-transactions |
KantaraConsentReceipt |
Full Kantara Initiative-compliant consent receipts |
DataProvenance |
Data ownership, transformation, and access tracking with RBAC |
IntegratedConsentProvenanceSystem |
Coordinator enforcing consent before data operations |
| Contract | Purpose |
|---|---|
DataAccessControl |
Granular access level management (Read/Transform/Full) |
ConsentProxy |
Purpose-scoped delegation with time limits |
ConsentAuditLog |
Immutable audit trail for compliance |
DataDeletion |
GDPR right-to-erasure with deletion proofs |
PurposeRegistry |
Standardized purpose definitions |
- Node.js >= 18.0.0
- npm >= 9.0.0
- Docker and Docker Compose (optional, for containerized development)
# Clone the repository
git clone https://github.com/datafund/ConsentsBasedDataProvenance.git
cd ConsentsBasedDataProvenance
# Install dependencies
npm install
# Compile contracts
npm run compile# Start the Hardhat node
docker-compose up -d
# Compile contracts
docker-compose exec hardhat npx hardhat compile
# Run tests
docker-compose exec hardhat npx hardhat test# Using npm
npm run node
# Using Docker
docker-compose up# Using npm
npm run deploy:local
# Using Docker
docker-compose exec hardhat npx hardhat run scripts/deploy.ts --network localhostimport { ethers } from "hardhat";
// Get deployed contracts
const consentReceipt = await ethers.getContractAt("ConsentReceipt", CONSENT_ADDRESS);
const dataProvenance = await ethers.getContractAt("DataProvenance", PROVENANCE_ADDRESS);
// Give consent for a purpose
await consentReceipt.giveConsent("analytics");
// Register data with consent verification
const dataHash = ethers.keccak256(ethers.toUtf8Bytes("my-data"));
await integratedSystem.registerDataWithConsent(dataHash, "document", "analytics");Basic consent management with purpose-based tracking and EIP-712 signature support.
// Give consent for a purpose
function giveConsent(string memory _purpose) public;
function giveConsent(string memory _purpose, uint256 _expiryTime) public;
// Give consent via meta-transaction (gasless)
function giveConsentBySig(
address _user,
string memory _purpose,
uint256 _expiryTime,
uint256 _deadline,
uint8 _v, bytes32 _r, bytes32 _s
) public;
// Revoke consent
function revokeConsent(uint256 _index) public;
// Check consent status
function getConsentStatus(address _user, string memory _purpose) public view returns (bool);
// Batch operations
function batchGiveConsent(string[] memory _purposes, uint256[] memory _expiryTimes) public;
function batchRevokeConsent(uint256[] memory _indices) public;Full Kantara Initiative specification with 30 consent types and 70+ purpose categories.
// Give comprehensive consent
function giveConsent(
address _dataController,
Purpose[] memory _purposes,
bytes32[] memory _piCategories,
ConsentType _consentType,
uint256 _expiryTime,
bool _thirdPartyDisclosure,
string memory _policyUrl
) public returns (bytes32 receiptId);
// Check consent validity
function hasValidConsent(
address _dataSubject,
address _dataController,
Purpose _purpose
) public view returns (bool);
// Revoke consent
function revokeConsent(bytes32 _receiptId) public;Consent Types Include:
Express,Implicit,ThirdParty,VerbalExplicitAffirmative,InformedExplicit,DoubleOptInParentalConsent,ProxyConsent,DynamicConsent- And 20+ more...
Purpose Categories Include:
- Service & Contract:
ServiceProvision,ContractFulfillment,CustomerSupport - Legal:
LegalCompliance,RegulatoryReporting,AuditingInternal - Marketing:
DirectMarketing,AdvertisingPersonalization,MarketResearch - And 60+ more...
Data lineage tracking with RBAC, delegated ownership, and bidirectional lineage traversal.
// Register data
function registerData(bytes32 _dataHash, string memory _dataType) public;
function registerDataFor(bytes32 _dataHash, string memory _dataType, address _actualOwner) public;
// Record data transformation (single source → derived output)
function recordTransformation(
bytes32 _originalDataHash,
bytes32 _newDataHash,
string memory _transformation
) public;
// Record merge/join transformation (multiple sources → single output)
function recordMergeTransformation(
bytes32[] memory _sourceDataHashes,
bytes32 _newDataHash,
string memory _transformation,
string memory _newDataType
) public;
// Lineage traversal
function getTransformationLinks(bytes32 _dataHash) public view returns (TransformationLink[] memory); // Forward: parent → children (with descriptions)
function getChildHashes(bytes32 _dataHash) public view returns (bytes32[] memory); // Forward: parent → children (hashes only)
function getTransformationParents(bytes32 _dataHash) public view returns (bytes32[] memory); // Reverse: child → parents
// Record data access
function recordAccess(bytes32 _dataHash) public;
// Ownership management
function transferDataOwnership(bytes32 _dataHash, address _newOwner) public;
function setDataStatus(bytes32 _dataHash, DataStatus _newStatus) public;
// Delegation
function setDelegate(address _delegate, bool _authorized) public;
// RBAC
function grantRole(bytes32 role, address account) public; // Admin only
function revokeRole(bytes32 role, address account) public; // Admin only
function operatorSetDataStatus(bytes32 _dataHash, DataStatus _newStatus) public; // Operator role
// Batch operations
function batchRegisterData(bytes32[] memory _dataHashes, string[] memory _dataTypes) public;
function batchRecordAccess(bytes32[] memory _dataHashes) public;Lineage Traversal:
| Direction | Method | Returns |
|---|---|---|
| Forward (parent → children) | getChildHashes(hash) |
bytes32[] of child hashes |
| Forward (with descriptions) | getTransformationLinks(hash) |
TransformationLink[] (hash + description) |
| Reverse (child → parents) | getTransformationParents(hash) |
bytes32[] — 1 element for transforms, N for merges, empty for roots |
Data Statuses:
Active: Normal operational stateRestricted: Limited access (e.g., after consent revocation)Deleted: Marked for deletion
Roles:
ADMIN_ROLE: Can grant/revoke rolesOPERATOR_ROLE: Can change data status for complianceAUDITOR_ROLE: Read-only access for auditing
Enforces consent verification before all data operations.
// Consent-verified operations
function registerDataWithConsent(
bytes32 _dataHash,
string memory _dataType,
string memory _consentPurpose
) public;
function registerDataForWithConsent(
bytes32 _dataHash,
string memory _dataType,
string memory _consentPurpose,
address _actualOwner
) public;
function accessDataWithConsent(bytes32 _dataHash, string memory _consentPurpose) public;
function transformDataWithConsent(
bytes32 _originalDataHash,
bytes32 _newDataHash,
string memory _transformation,
string memory _consentPurpose
) public;
function mergeDataWithConsent(
bytes32[] memory _sourceDataHashes,
bytes32 _newDataHash,
string memory _transformation,
string memory _newDataType,
string memory _consentPurpose
) public;
// Restrict data after consent revocation
function restrictDataForPurpose(string memory _consentPurpose) public;// User gives consent for marketing
await consentReceipt.connect(user)["giveConsent(string)"]("marketing");
// Check if consent exists
const hasConsent = await consentReceipt.getConsentStatus(user.address, "marketing");
console.log("Has marketing consent:", hasConsent); // true
// Revoke consent
await consentReceipt.connect(user).revokeConsent(0);// Give consent that expires in 30 days
const thirtyDays = 30 * 24 * 60 * 60;
const block = await ethers.provider.getBlock("latest");
const expiryTime = block.timestamp + thirtyDays;
await consentReceipt.connect(user)["giveConsent(string,uint256)"]("analytics", expiryTime);// User signs consent off-chain
const domain = {
name: "ConsentReceipt",
version: "1",
chainId: (await ethers.provider.getNetwork()).chainId,
verifyingContract: await consentReceipt.getAddress()
};
const types = {
Consent: [
{ name: "user", type: "address" },
{ name: "purpose", type: "string" },
{ name: "expiryTime", type: "uint256" },
{ name: "nonce", type: "uint256" },
{ name: "deadline", type: "uint256" }
]
};
const nonce = await consentReceipt.getNonce(user.address);
const deadline = Math.floor(Date.now() / 1000) + 3600;
const value = {
user: user.address,
purpose: "analytics",
expiryTime: 0,
nonce: nonce,
deadline: deadline
};
const signature = await user.signTypedData(domain, types, value);
const sig = ethers.Signature.from(signature);
// Relayer submits on behalf of user
await consentReceipt.connect(relayer).giveConsentBySig(
user.address,
"analytics",
0,
deadline,
sig.v, sig.r, sig.s
);// User gives consent
await consentReceipt.connect(user)["giveConsent(string)"]("analytics");
// Register data (requires consent)
const dataHash = ethers.keccak256(ethers.toUtf8Bytes("user-document-content"));
await integratedSystem.connect(user).registerDataWithConsent(
dataHash,
"document",
"analytics"
);
// Track transformation
const newHash = ethers.keccak256(ethers.toUtf8Bytes("transformed-content"));
await integratedSystem.connect(user).transformDataWithConsent(
dataHash,
newHash,
"anonymization",
"analytics"
);// Owner authorizes delegate
await dataProvenance.connect(owner).setDelegate(delegate.address, true);
await dataProvenance.connect(owner).setDelegate(await integratedSystem.getAddress(), true);
// Owner gives consent
await consentReceipt.connect(owner)["giveConsent(string)"]("research");
// Delegate registers data on owner's behalf
const dataHash = ethers.keccak256(ethers.toUtf8Bytes("research-data"));
await integratedSystem.connect(delegate).registerDataForWithConsent(
dataHash,
"research-data",
"research",
owner.address
);// Give comprehensive consent following Kantara specification
const tx = await kantaraConsent.connect(user).giveConsent(
dataControllerAddress,
[0, 1], // ServiceProvision, ContractFulfillment
[ethers.keccak256(ethers.toUtf8Bytes("email")), ethers.keccak256(ethers.toUtf8Bytes("name"))],
10, // ExplicitAffirmative consent type
expiryTime,
true, // Allow third-party disclosure
"https://example.com/privacy-policy"
);
const receipt = await tx.wait();
const receiptId = receipt.logs[0].args[0]; // Get receipt ID from event
// Verify consent
const isValid = await kantaraConsent.hasValidConsent(
user.address,
dataControllerAddress,
0 // ServiceProvision
);The examples/ directory contains 40+ runnable TypeScript scripts demonstrating real-world usage across multiple industries:
| Category | Examples | Description |
|---|---|---|
| Getting Started | 4 | Basic consent, expiry, revocation, integrated system |
| Healthcare | 5 | Patient consent, HIPAA, medical records, deletion |
| Financial Services | 5 | KYC, credit checks, regulatory reporting |
| Marketing | 5 | Email consent, cookies, GDPR/CCPA compliance |
| Research | 4 | Study enrollment, anonymization, multi-institution |
| IoT & Supply Chain | 4 | Device registration, sensor data, manufacturing |
| Advanced Patterns | 5 | Meta-transactions, delegation, RBAC, batch ops |
| Compliance & Audit | 5 | GDPR, Kantara, audit queries, breach investigation |
| Integration Patterns | 4 | Events, backend services, error handling |
# Start local blockchain
docker-compose up -d
# Run any example via npm script
npm run example:basic-consent
npm run example:healthcare:hipaa
npm run example:compliance:gdpr
npm run example:integration:events
# Or run directly
npx hardhat run examples/01-getting-started/04-integrated-system.ts --network localhostSee examples/README.md for the complete guide.
# Using npm
npm test
# Using Docker
docker-compose exec hardhat npx hardhat test# Run security tests
npx hardhat test test/Security.test.ts
# Run integration tests
npx hardhat test test/Integration.test.ts
# Run with gas reporting
REPORT_GAS=true npx hardhat testnpm run test:coveragetest/
├── ConsentReceipt.test.ts # Basic consent tests
├── KantaraConsentReceipt.test.ts # Kantara compliance tests
├── DataProvenance.test.ts # Data tracking tests
├── IntegratedConsentProvenanceSystem.test.ts
├── Integration.test.ts # End-to-end tests
├── Security.test.ts # Security/attack tests
├── ConsentAuditLog.test.ts
├── ConsentProxy.test.ts
├── DataAccessControl.test.ts
├── DataDeletion.test.ts
└── PurposeRegistry.test.ts
Current Test Count: 299 tests
All contracts enforce limits to control gas costs and prevent abuse. These are per-record or per-call — they don't limit the overall system size.
| Limit | Value | Meaning |
|---|---|---|
MAX_TRANSFORMATIONS |
100 | Max transformations branching from a single data record. Each new version is its own record with its own budget, so provenance chains can be arbitrarily deep. |
MAX_MERGE_SOURCES |
50 | Max source datasets in a single recordMergeTransformation call. |
MAX_ACCESSORS |
1000 | Max unique addresses recorded as accessors of a single data record. Duplicates are deduplicated automatically. |
MAX_ACCESS_DURATION |
2 years | Longest access grant via DataAccessControl. |
MAX_DELEGATION_DURATION |
1 year | Longest consent delegation via ConsentProxy. |
| Field | Max Length |
|---|---|
dataType |
64 chars |
transformation |
256 chars |
purpose (ConsentReceipt) |
256 chars |
policyUrl (KantaraConsentReceipt) |
512 chars |
| Operation | Max Items per Call |
|---|---|
batchRegisterData |
50 |
batchRecordAccess |
100 |
batchSetDataStatus |
50 |
batchGiveConsent |
50 |
batchRevokeConsent |
50 |
Kantara batchRevokeConsent |
50 |
Kantara batchCheckConsent |
100 |
| Kantara purposes per receipt | 20 |
| Kantara PI categories per receipt | 50 |
- Total records — no cap on how many data records exist system-wide
- Provenance chain depth — each transformation creates a new record, so chains can go as deep as needed
- Total users — no cap on addresses interacting with the contracts
- Contract storage — grows as needed (each write costs gas, reads are free)
-
Input Validation
- String length limits (see table above)
- Zero address checks
- Empty input rejection
-
Bounded Arrays
- All per-record and batch limits enforced (see table above)
-
Access Control
- Role-Based Access Control (RBAC) for administrative functions
- Owner-only operations for data modification
- Delegate authorization with revocation
-
Replay Protection
- Nonce-based receipt ID generation
- EIP-712 signatures with deadline and nonce
- DOMAIN_SEPARATOR includes chainId
-
State Management
- Accessor deduplication prevents storage bloat
- Status change validation (no duplicate state changes)
- Expiration handling for consent and delegations
The security test suite (test/Security.test.ts) includes 91 tests covering:
- RBAC Bypass Attempts: Non-admin role manipulation, privilege escalation
- Delegation Exploits: Unauthorized delegation, revoked delegate actions
- Ownership Attacks: Non-owner transfers, status changes
- DoS/Resource Exhaustion: Array bounds, batch limits
- EIP-712 Signature Attacks: Replay, expired signatures, wrong signer, tampering
- Cross-Contract Attacks: State desynchronization, front-running
- Input Validation: Zero values, empty strings, length limits
- On-Chain Data: All consent records are stored on-chain. Consider privacy implications.
- Gas Costs: Large batch operations may exceed block gas limits.
- Immutability: Once registered, data hashes cannot be modified (by design).
event ConsentGiven(address indexed user, string purpose, uint256 timestamp, uint256 expiryTime);
event ConsentRevoked(address indexed user, string purpose, uint256 timestamp);
event ConsentGivenBySig(address indexed user, string purpose, address indexed relayer);event DataRegistered(bytes32 indexed dataHash, address indexed owner, string dataType);
event DataTransformed(bytes32 indexed originalDataHash, bytes32 indexed newDataHash, string transformation);
event DataMerged(bytes32 indexed newDataHash, bytes32[] sourceDataHashes, string transformation);
event DataAccessed(bytes32 indexed dataHash, address indexed accessor);
event DataStatusChanged(bytes32 indexed dataHash, DataStatus oldStatus, DataStatus newStatus);
event DataOwnershipTransferred(bytes32 indexed dataHash, address indexed previousOwner, address indexed newOwner);
event DelegateAuthorized(address indexed owner, address indexed delegate, bool authorized);
event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender);
event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender);event ConsentGiven(bytes32 indexed receiptId, address indexed dataSubject, address indexed dataController, uint256 expiryTime);
event ConsentRevoked(bytes32 indexed receiptId, address indexed dataSubject);
event ConsentUpdated(bytes32 indexed receiptId, address indexed dataSubject);| Error | Contract | Meaning |
|---|---|---|
Purpose cannot be empty |
ConsentReceipt | Empty purpose string provided |
Purpose too long |
ConsentReceipt | Purpose exceeds 256 characters |
Expiry must be in future |
ConsentReceipt | Expiry timestamp is in the past |
Invalid consent index |
ConsentReceipt | Index out of bounds |
Consent already revoked |
ConsentReceipt | Attempting to revoke inactive consent |
Signature expired |
ConsentReceipt | EIP-712 signature deadline passed |
Invalid signature |
ConsentReceipt | Signature verification failed |
Invalid data hash |
DataProvenance | Zero hash provided |
Data type cannot be empty |
DataProvenance | Empty data type string |
Data already registered |
DataProvenance | Hash already exists |
Not the owner |
DataProvenance | Caller is not data owner |
Data is not active |
DataProvenance | Data status is Restricted or Deleted |
Max transformations reached |
DataProvenance | 100 transformation limit hit |
Max accessors reached |
DataProvenance | 1000 accessor limit hit |
Not authorized delegate |
DataProvenance | Caller not authorized to act for owner |
AccessControl: admin role required |
DataProvenance | Caller lacks admin role |
No valid consent for this purpose |
IntegratedSystem | Required consent not found |
Consent still valid |
IntegratedSystem | Cannot restrict while consent active |
# Legacy deploy script
npm run deploy:local
# Hardhat Ignition (deploys all 9 contracts with dependency ordering)
npm run node # Start local node in another terminal
npm run deploy:ignition:localSupported testnets: Sepolia, Gnosis Chiado, Base Sepolia.
# 1. Copy .env.example and configure
cp .env.example .env
# Edit .env: set TESTNET_DEPLOYER_PRIVATE_KEY and (optionally) RPC URLs / API keys
# 2. Deploy to a testnet
npm run deploy:ignition:sepolia
npm run deploy:ignition:chiado
npm run deploy:ignition:baseSepoliaIgnition deploys all 9 contracts in dependency order:
- Batch 1: ConsentReceipt, DataProvenance, KantaraConsentReceipt, ConsentAuditLog, ConsentProxy, PurposeRegistry
- Batch 2: DataAccessControl, DataDeletion, IntegratedConsentProvenanceSystem
Deployed addresses are saved to ignition/deployments/chain-<chainId>/deployed_addresses.json and a human-readable summary is auto-generated at deployments.md.
Contracts can be verified on Blockscout (no API key needed) — verification is manual, not automatic. Base Sepolia and Chiado use Blockscout. Sepolia uses Etherscan (requires ETHERSCAN_API_KEY).
# Verify individual contracts on Blockscout (no API key needed)
npx hardhat verify --network baseSepolia <CONTRACT_ADDRESS>
npx hardhat verify --network chiado <CONTRACT_ADDRESS>
# Verify on Etherscan (requires ETHERSCAN_API_KEY in .env)
npx hardhat verify --network sepolia <CONTRACT_ADDRESS>See deployments.md for the auto-generated reference. Summary:
| Network | Contract | Address | Explorer |
|---|---|---|---|
| Base Sepolia | ConsentAuditLog | 0x050384eA3e3ab6706afA6dB9DfA9FCd3A24901f4 |
Blockscout |
| Base Sepolia | ConsentProxy | 0x5B96F9d0b896f4AD9E0fE368259479eF369853FE |
Blockscout |
| Base Sepolia | ConsentReceipt | 0xa88a0D18cABcd5d2eA06A028210713d98FccF5BF |
Blockscout |
| Base Sepolia | DataProvenance | 0xD4a724CD7f5C4458cD2d884C2af6f011aC3Af80a |
Blockscout |
| Base Sepolia | KantaraConsentReceipt | 0x64CC6738E43dc2c6D2E70120f351Bc85f563481C |
Blockscout |
| Base Sepolia | PurposeRegistry | 0x50F132Dc634C80d940b93551D1659B87c82599F1 |
Blockscout |
| Base Sepolia | DataAccessControl | 0xe3056301801b11a2dF5c31A9E86f46c9c604414A |
Blockscout |
| Base Sepolia | DataDeletion | 0x77CD9F9c67D4F3067f4088dfBE9B54bb5AC99aBf |
Blockscout |
| Base Sepolia | IntegratedConsentProvenanceSystem | 0x91EafB1BcE558c183AA04c3fC090DE31671AfdD7 |
Blockscout |
| Network | Contract | Address | Explorer | Notes |
|---|---|---|---|---|
| Base Sepolia | DataProvenance (v1) | 0x9a3c6F47B69211F05891CCb7aD33596290b9fE64 |
Blockscout | Pre-TransformationLink struct, standalone deploy |
ABIs are available from multiple sources:
dist/abis/<ContractName>.json— ABI-only JSON files, auto-generated after each deployment (npm run post-deploy)- Blockscout (verified contracts only): Contract page → "Contract" tab → ABI section, or API:
https://base-sepolia.blockscout.com/api?module=contract&action=getabi&address=<ADDRESS> - Local build: Run
npx hardhat compile, full artifacts inartifacts/contracts/<Name>.sol/<Name>.json
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit changes (
git commit -m 'Add amazing feature') - Push to branch (
git push origin feature/amazing-feature) - Open a Pull Request
- All contracts use Solidity 0.8.20
- Follow existing code style and patterns
- Add tests for new functionality
- Update documentation as needed
MIT License - see LICENSE file for details.
- Kantara Initiative - Consent receipt specification
- OpenZeppelin - Security patterns and best practices
- Hardhat - Development environment