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.
| Property | Value |
|---|---|
| Protocol Fee | 2.5% |
| Price Update Delay | 1 hour |
| Treasury Update Delay | 2 days |
| Network | Base (Chain ID: 8453) |
Deployed Addresses
Base Sepolia (Testnet)
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- Seller calls
createListing()with price and metadata hash - Buyer approves PRIV tokens and calls
purchaseListing() - Contract generates unique access token from buyer, listing, timestamp, and random data
- 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 appliedContract 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)| Parameter | Type | Description |
|---|---|---|
price | uint256 | Price in PRIV tokens (18 decimals) |
metadataHash | bytes32 | IPFS/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| Parameter | Type | Description |
|---|---|---|
listingId | uint256 | ID of the listing to update |
newPrice | uint256 | New price in PRIV tokens |
newMetadataHash | bytes32 | New 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 whenNotPausedRequirements:
- 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) externaldeactivateListing
Deactivate a listing (soft delete).
function deactivateListing(uint256 listingId) externalNote: Also cancels any pending price updates.
reactivateListing
Reactivate a previously deactivated listing.
function reactivateListing(uint256 listingId) external whenNotPausedPurchasing
purchaseListing
Purchase access to a data listing.
function purchaseListing(
uint256 listingId
) external nonReentrant whenNotPaused returns (uint256 purchaseId, bytes32 accessToken)| Parameter | Type | Description |
|---|---|---|
listingId | uint256 | ID 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 recordaccessToken: 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
-
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.
-
Treasury Timelock: The 2-day delay on treasury updates gives users time to react if a malicious admin attempts to redirect fees.
-
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.
-
Double Purchase Prevention: The contract prevents buying the same listing twice via the
hasAccessmapping. -
Self-Purchase Prevention: Sellers cannot purchase their own listings.