PRIV ProtocolPRIV Docs
Contracts

DataXchange

Decentralized marketplace for privacy-preserving data exchange with timelock protection.

Overview

DataXchange is a decentralized marketplace that enables users to list and sell anonymized datasets. Buyers pay with PRIV tokens and receive unique access tokens to retrieve purchased data.

PropertyValue
Protocol Fee2.5%
Price Update Delay1 hour
Treasury Update Delay2 days
NetworkBase (Chain ID: 8453)

Deployed Addresses

Base Sepolia (Testnet)

Base Sepolia0x832e...c44c

Key Features

  • Dataset Listings: Sellers list data with metadata hash and price
  • 2.5% Protocol Fee: Sent to FeeManager for burn/distribute
  • Access Token Generation: Unique tokens generated on purchase
  • Price Update Timelock: 1-hour delay prevents front-running
  • Treasury Timelock: 2-day delay for treasury address changes
  • Deactivation/Reactivation: Sellers can pause listings

How It Works

Seller lists dataset  -->  Buyer purchases  -->  Access token generated
       |                         |                        |
       v                         v                        v
   metadataHash              PRIV payment            Buyer retrieves
   + price set             (2.5% fee)              data via token
  1. Seller calls createListing() with price and metadata hash
  2. Buyer approves PRIV tokens and calls purchaseListing()
  3. Contract generates unique access token from buyer, listing, timestamp, and random data
  4. Buyer uses access token to retrieve data from off-chain storage (IPFS/Arweave)

Price Update Flow (Timelock Protected)

Price updates require a 1-hour delay to prevent front-running:

1. proposeListingUpdate()  -->  2. Wait 1 hour  -->  3. executeListingUpdate()
         |                            |                        |
         v                            v                        v
    Proposal created            Delay elapsed              Update applied

Contract Interface

Constants

/// @notice Protocol fee percentage (250 = 2.5%)
uint256 public constant PROTOCOL_FEE_BPS = 250;

/// @notice Basis points denominator (10000 = 100%)
uint256 public constant BPS_DENOMINATOR = 10000;

/// @notice Delay before price updates take effect (1 hour)
uint256 public constant PRICE_UPDATE_DELAY = 1 hours;

/// @notice Delay before treasury updates take effect (2 days)
uint256 public constant TREASURY_UPDATE_DELAY = 2 days;

Structs

Listing

struct Listing {
    address seller;        // Seller address
    uint256 price;         // Price in PRIV tokens (18 decimals)
    bytes32 metadataHash;  // IPFS/Arweave hash of metadata
    bool active;           // Whether listing is active
    uint256 totalSales;    // Total number of sales
    uint256 createdAt;     // Creation timestamp
    uint256 updatedAt;     // Last update timestamp
}

Purchase

struct Purchase {
    address buyer;         // Buyer address
    uint256 listingId;     // ID of purchased listing
    bytes32 accessToken;   // Unique access token
    uint256 pricePaid;     // Price paid in PRIV
    uint256 purchasedAt;   // Purchase timestamp
}

PendingPriceUpdate

struct PendingPriceUpdate {
    uint256 newPrice;           // New price
    bytes32 newMetadataHash;    // New metadata hash
    uint256 effectiveTime;      // When update can be executed
    bool exists;                // Whether update exists
}

Functions

Listing Management

createListing

Create a new data listing.

function createListing(
    uint256 price,
    bytes32 metadataHash
) external whenNotPaused returns (uint256 listingId)
ParameterTypeDescription
priceuint256Price in PRIV tokens (18 decimals)
metadataHashbytes32IPFS/Arweave hash of dataset metadata

Requirements:

  • Price must be > 0
  • Metadata hash must not be zero
  • Contract must not be paused

Returns: The ID of the created listing.


proposeListingUpdate

Propose a price/metadata update (starts 1-hour timelock).

function proposeListingUpdate(
    uint256 listingId,
    uint256 newPrice,
    bytes32 newMetadataHash
) external whenNotPaused
ParameterTypeDescription
listingIduint256ID of the listing to update
newPriceuint256New price in PRIV tokens
newMetadataHashbytes32New metadata hash

Requirements:

  • Caller must be the listing seller
  • No pending update already exists
  • New price must be > 0

executeListingUpdate

Execute a pending update after timelock expires.

function executeListingUpdate(uint256 listingId) external whenNotPaused

Requirements:

  • Caller must be the listing seller
  • Pending update must exist
  • 1 hour must have elapsed since proposal

cancelListingUpdate

Cancel a pending price update.

function cancelListingUpdate(uint256 listingId) external

deactivateListing

Deactivate a listing (soft delete).

function deactivateListing(uint256 listingId) external

Note: Also cancels any pending price updates.


reactivateListing

Reactivate a previously deactivated listing.

function reactivateListing(uint256 listingId) external whenNotPaused

Purchasing

purchaseListing

Purchase access to a data listing.

function purchaseListing(
    uint256 listingId
) external nonReentrant whenNotPaused returns (uint256 purchaseId, bytes32 accessToken)
ParameterTypeDescription
listingIduint256ID of the listing to purchase

Requirements:

  • Listing must exist and be active
  • Buyer cannot purchase their own listing
  • Buyer cannot have already purchased the listing
  • Buyer must have approved sufficient PRIV tokens

Returns:

  • purchaseId: The ID of the purchase record
  • accessToken: Unique token for data retrieval

Access Token Generation:

accessToken = keccak256(abi.encodePacked(
    msg.sender,      // buyer
    listingId,       // listing
    block.timestamp, // time
    block.prevrandao,// randomness
    nextPurchaseId   // nonce
))

View Functions

getListing

Get full listing details.

function getListing(uint256 listingId) external view returns (Listing memory)

getPurchase

Get full purchase details.

function getPurchase(uint256 purchaseId) external view returns (Purchase memory)

hasAccess

Check if a buyer has access to a listing.

function hasAccess(address buyer, uint256 listingId) external view returns (bool)

getSellerListings

Get all listing IDs for a seller.

function getSellerListings(address seller) external view returns (uint256[] memory)

getBuyerPurchases

Get all purchase IDs for a buyer.

function getBuyerPurchases(address buyer) external view returns (uint256[] memory)

calculateProtocolFee

Calculate the protocol fee for a price.

function calculateProtocolFee(uint256 price) external pure returns (uint256)

getPendingPriceUpdate

Get pending price update details.

function getPendingPriceUpdate(uint256 listingId) external view returns (PendingPriceUpdate memory)

priceUpdateTimeRemaining

Get time remaining until a pending update can be executed.

function priceUpdateTimeRemaining(uint256 listingId) external view returns (uint256)

Events

/// @notice Emitted when a new listing is created
event ListingCreated(
    uint256 indexed listingId,
    address indexed seller,
    uint256 price,
    bytes32 metadataHash
);

/// @notice Emitted when a listing update is proposed
event ListingUpdateProposed(
    uint256 indexed listingId,
    uint256 newPrice,
    bytes32 newMetadataHash,
    uint256 effectiveTime
);

/// @notice Emitted when a listing update is executed
event ListingUpdated(
    uint256 indexed listingId,
    uint256 newPrice,
    bytes32 newMetadataHash
);

/// @notice Emitted when a pending update is cancelled
event ListingUpdateCancelled(uint256 indexed listingId);

/// @notice Emitted when a listing is deactivated
event ListingDeactivated(uint256 indexed listingId);

/// @notice Emitted when a listing is reactivated
event ListingReactivated(uint256 indexed listingId);

/// @notice Emitted when a listing is purchased
event ListingPurchased(
    uint256 indexed purchaseId,
    uint256 indexed listingId,
    address indexed buyer,
    address seller,
    uint256 price,
    uint256 protocolFee,
    bytes32 accessToken
);

Errors

error InvalidAddress();
error InvalidPrice();
error InvalidMetadataHash();
error ListingNotFound();
error ListingNotActive();
error ListingAlreadyActive();
error NotListingSeller();
error AlreadyPurchased();
error CannotPurchaseOwnListing();
error InsufficientAllowance();
error PriceUpdateNotReady();
error NoPendingPriceUpdate();
error PriceUpdateAlreadyPending();

Usage Examples

Create a Listing

import { useWriteContract } from 'wagmi'
import { parseEther, keccak256, toHex } from 'viem'

function useCreateListing() {
  const { writeContract } = useWriteContract()

  return async (price: string, metadataUri: string) => {
    // Hash the metadata URI to get bytes32
    const metadataHash = keccak256(toHex(metadataUri))

    await writeContract({
      address: DATAXCHANGE_ADDRESS,
      abi: dataXchangeAbi,
      functionName: 'createListing',
      args: [parseEther(price), metadataHash],
    })
  }
}

Purchase a Dataset

async function purchaseDataset(listingId: bigint, price: bigint) {
  // 1. Approve PRIV spending
  await writeContract({
    address: PRIV_TOKEN_ADDRESS,
    abi: privTokenAbi,
    functionName: 'approve',
    args: [DATAXCHANGE_ADDRESS, price],
  })

  // 2. Purchase dataset
  const result = await writeContract({
    address: DATAXCHANGE_ADDRESS,
    abi: dataXchangeAbi,
    functionName: 'purchaseListing',
    args: [listingId],
  })

  // Access token returned in event
  return result
}

Update Listing Price (with Timelock)

async function updateListingPrice(listingId: bigint, newPrice: bigint, newMetadataHash: `0x${string}`) {
  // 1. Propose the update
  await writeContract({
    address: DATAXCHANGE_ADDRESS,
    abi: dataXchangeAbi,
    functionName: 'proposeListingUpdate',
    args: [listingId, newPrice, newMetadataHash],
  })

  // 2. Wait 1 hour...

  // 3. Execute the update
  await writeContract({
    address: DATAXCHANGE_ADDRESS,
    abi: dataXchangeAbi,
    functionName: 'executeListingUpdate',
    args: [listingId],
  })
}

Check Access

function useHasAccess(listingId: bigint, buyer: string) {
  return useReadContract({
    address: DATAXCHANGE_ADDRESS,
    abi: dataXchangeAbi,
    functionName: 'hasAccess',
    args: [buyer, listingId],
  })
}

Metadata Schema

Recommended schema for dataset metadata stored at metadataHash:

{
  "name": "Q4 2024 DeFi User Analytics",
  "description": "Anonymized browsing patterns of DeFi users",
  "dataType": "analytics",
  "recordCount": 50000,
  "timeRange": {
    "start": "2024-10-01",
    "end": "2024-12-31"
  },
  "categories": ["defi", "trading", "lending"],
  "preview": "ipfs://Qm.../preview.json",
  "schema": {
    "fields": [
      { "name": "timestamp", "type": "uint64" },
      { "name": "action", "type": "string" },
      { "name": "category", "type": "string" }
    ]
  }
}

Security Notes

  1. Price Timelock: The 1-hour delay on price updates prevents sellers from front-running buyers. If you see a buyer's transaction in the mempool, you cannot instantly raise the price.

  2. Treasury Timelock: The 2-day delay on treasury updates gives users time to react if a malicious admin attempts to redirect fees.

  3. Access Token Security: Access tokens are generated using buyer address, listing ID, timestamp, and block randomness. They should be kept private as they grant access to purchased data.

  4. Double Purchase Prevention: The contract prevents buying the same listing twice via the hasAccess mapping.

  5. Self-Purchase Prevention: Sellers cannot purchase their own listings.


Source Code

View on GitHub