AdNetwork
Privacy-preserving advertising network with multi-oracle consensus for impression verification.
Overview
AdNetwork is a decentralized advertising platform that enables advertisers to run campaigns and publishers to earn PRIV tokens for displaying ads. It uses multi-oracle consensus (M-of-N) for verifying impressions and clicks.
| Property | Value |
|---|---|
| Protocol Fee | 2.5% |
| Min Deposit | 1 PRIV |
| Min Claim Threshold | 10 PRIV |
| Max Price/Impression | 1 PRIV |
| Max Price/Click | 10 PRIV |
| Max Oracles | 10 |
| Network | Base (Chain ID: 8453) |
Deployed Addresses
Base Sepolia (Testnet)
Key Features
- Advertiser Deposits: Advertisers deposit PRIV to fund campaigns
- Campaign Creation: Create campaigns with budget, CPM/CPC pricing, and duration
- Multi-Oracle Consensus: M-of-N oracle confirmations for impression verification
- Publisher Earnings: Publishers earn from verified impressions/clicks (minus 2.5% fee)
- Oracle Heartbeat: Staleness checks prevent stale data from being used
- Emergency Claims: Publishers can claim below threshold when contract is paused
How It Works
Advertiser deposits PRIV --> Creates campaign --> Publisher shows ads
| | |
v v v
Balance updated Budget allocated Impressions tracked
|
v
Oracles confirm batch
|
v
Publisher claims earningsMulti-Oracle Consensus Flow
For impression verification, multiple oracles must agree:
Oracle 1 confirms --> Oracle 2 confirms --> Consensus reached
| | |
v v v
count = 1 count = 2 Publisher credited
(if minOracleConfirmations = 2)Contract Interface
Constants
/// @notice Protocol fee percentage (250 = 2.5%)
uint256 public constant PROTOCOL_FEE_BPS = 250;
/// @notice Minimum deposit amount (1 PRIV)
uint256 public constant MIN_DEPOSIT = 1e18;
/// @notice Minimum claim threshold (10 PRIV)
uint256 public constant MIN_CLAIM_THRESHOLD = 10e18;
/// @notice Maximum price per impression (1 PRIV)
uint256 public constant MAX_PRICE_PER_IMPRESSION = 1e18;
/// @notice Maximum price per click (10 PRIV)
uint256 public constant MAX_PRICE_PER_CLICK = 10e18;
/// @notice Maximum number of oracles
uint256 public constant MAX_ORACLES = 10;Structs
Campaign
struct Campaign {
address advertiser; // Campaign owner
bool active; // Whether campaign is active
uint256 budget; // Total budget in PRIV
uint256 spent; // Amount spent so far
uint256 pricePerImpression; // CPM price (per impression)
uint256 pricePerClick; // CPC price (per click)
bytes32 metadataHash; // IPFS/Arweave hash of campaign metadata
uint256 totalImpressions; // Total recorded impressions
uint256 totalClicks; // Total recorded clicks
uint48 createdAt; // Creation timestamp
uint48 expiresAt; // Expiration timestamp
}PublisherStats
struct PublisherStats {
uint256 totalImpressions; // Total impressions served
uint256 totalClicks; // Total clicks generated
uint256 totalEarnings; // Total lifetime earnings
uint256 pendingEarnings; // Earnings available to claim
uint256 lastClaimAt; // Last claim timestamp
}Functions
Advertiser Functions
deposit
Deposit PRIV tokens into advertiser balance.
function deposit(uint256 amount) external nonReentrant whenNotPaused| Parameter | Type | Description |
|---|---|---|
amount | uint256 | Amount to deposit (min: 1 PRIV) |
Requirements:
- Amount must be at least MIN_DEPOSIT (1 PRIV)
- User must have approved the contract
withdraw
Withdraw PRIV tokens from advertiser balance.
function withdraw(uint256 amount) external nonReentrant| Parameter | Type | Description |
|---|---|---|
amount | uint256 | Amount to withdraw |
createCampaign
Create a new advertising campaign.
function createCampaign(
uint256 budget,
uint256 pricePerImpression,
uint256 pricePerClick,
bytes32 metadataHash,
uint256 duration
) external whenNotPaused returns (uint256 campaignId)| Parameter | Type | Description |
|---|---|---|
budget | uint256 | Total budget (from advertiser balance) |
pricePerImpression | uint256 | CPM price (max: 1 PRIV) |
pricePerClick | uint256 | CPC price (max: 10 PRIV) |
metadataHash | bytes32 | Campaign metadata hash |
duration | uint256 | Campaign duration in seconds |
Requirements:
- Budget must be greater than 0 and not exceed advertiser balance
- At least one pricing model must be set (pricePerImpression or pricePerClick greater than 0)
- Prices must not exceed maximums
- Duration must be greater than 0
pauseCampaign
Pause an active campaign.
function pauseCampaign(uint256 campaignId) externalresumeCampaign
Resume a paused campaign.
function resumeCampaign(uint256 campaignId) external whenNotPausedtopUpCampaign
Add more budget to an existing campaign.
function topUpCampaign(uint256 campaignId, uint256 amount) external whenNotPausedwithdrawCampaignBudget
Withdraw remaining budget from an expired or paused campaign.
function withdrawCampaignBudget(uint256 campaignId) external nonReentrantMulti-Oracle Consensus
confirmImpressionBatch
Confirm an impression batch as part of M-of-N consensus.
function confirmImpressionBatch(
uint256 campaignId,
address publisher,
uint256 impressions,
uint256 clicks,
bytes32 batchId
) external onlyOracle notStaleOracle whenNotPaused| Parameter | Type | Description |
|---|---|---|
campaignId | uint256 | Campaign ID |
publisher | address | Publisher address to credit |
impressions | uint256 | Number of impressions |
clicks | uint256 | Number of clicks |
batchId | bytes32 | Unique batch identifier (prevents replay) |
Requirements:
- Caller must be an authorized oracle
- Oracle must have fresh heartbeat
- Batch must not already be processed
- Campaign must be active and not expired
- At least one of impressions or clicks must be greater than 0
Consensus:
When minOracleConfirmations oracles confirm the same batch parameters, the publisher is automatically credited (minus 2.5% protocol fee).
Legacy Oracle Functions (Single Oracle Mode)
These functions only work when minOracleConfirmations == 1:
recordImpressions
function recordImpressions(
uint256 campaignId,
address publisher,
uint256 count
) external onlyOracle notStaleOracle whenNotPausedrecordClicks
function recordClicks(
uint256 campaignId,
address publisher,
uint256 count
) external onlyOracle notStaleOracle whenNotPausedPublisher Functions
claimEarnings
Claim accumulated earnings.
function claimEarnings() external nonReentrant whenNotPausedRequirements:
- Pending earnings must be at least MIN_CLAIM_THRESHOLD (10 PRIV)
- Contract must not be paused
emergencyClaim
Emergency claim for publishers below threshold (only when paused).
function emergencyClaim() external nonReentrant whenPausedNote: This allows claims of any amount but only works when the contract is paused (emergency mode).
View Functions
getCampaign
Get full campaign details.
function getCampaign(uint256 campaignId) external view returns (Campaign memory)getPublisherStats
Get publisher statistics.
function getPublisherStats(address publisher) external view returns (PublisherStats memory)isCampaignRunning
Check if a campaign is active, not expired, and has budget.
function isCampaignRunning(uint256 campaignId) external view returns (bool)getRemainingBudget
Get remaining budget for a campaign.
function getRemainingBudget(uint256 campaignId) external view returns (uint256)getOracles
Get all authorized oracle addresses.
function getOracles() external view returns (address[] memory)isOracleFresh
Check if an oracle's data is fresh (not stale).
function isOracleFresh(address oracleAddress) external view returns (bool)computeBatchHash
Compute the hash for a batch of impressions.
function computeBatchHash(
uint256 campaignId,
address publisher,
uint256 impressions,
uint256 clicks,
bytes32 batchId
) external pure returns (bytes32)Events
/// @notice Emitted when an advertiser deposits
event Deposited(address indexed advertiser, uint256 amount);
/// @notice Emitted when an advertiser withdraws
event Withdrawn(address indexed advertiser, uint256 amount);
/// @notice Emitted when a campaign is created
event CampaignCreated(
uint256 indexed campaignId,
address indexed advertiser,
uint256 budget,
uint256 pricePerImpression,
uint256 pricePerClick,
bytes32 metadataHash,
uint48 expiresAt
);
/// @notice Emitted when a campaign is paused
event CampaignPaused(uint256 indexed campaignId);
/// @notice Emitted when a campaign is resumed
event CampaignResumed(uint256 indexed campaignId);
/// @notice Emitted when a campaign is topped up
event CampaignToppedUp(uint256 indexed campaignId, uint256 amount);
/// @notice Emitted when impressions are recorded
event ImpressionsRecorded(
uint256 indexed campaignId,
address indexed publisher,
uint256 count,
uint256 earnings
);
/// @notice Emitted when clicks are recorded
event ClicksRecorded(
uint256 indexed campaignId,
address indexed publisher,
uint256 count,
uint256 earnings
);
/// @notice Emitted when an oracle confirms a batch
event BatchConfirmed(
bytes32 indexed batchId,
address indexed oracle,
uint256 confirmationCount,
uint256 requiredConfirmations
);
/// @notice Emitted when a batch reaches consensus
event BatchProcessed(
bytes32 indexed batchId,
uint256 indexed campaignId,
address indexed publisher,
uint256 impressions,
uint256 clicks,
uint256 earnings
);
/// @notice Emitted when a publisher claims earnings
event EarningsClaimed(address indexed publisher, uint256 amount);Errors
error InvalidAddress();
error InvalidAmount();
error InvalidPrice();
error InvalidDuration();
error InsufficientBalance();
error CampaignNotFound();
error CampaignNotActive();
error CampaignExpired();
error NotCampaignOwner();
error BelowClaimThreshold();
error NothingToClaim();
error OnlyOracle();
error CampaignBudgetExceeded();
error OracleDataStale();
error BatchAlreadyProcessed();
error BatchAlreadyConfirmed();
error ConsensusRequired();Usage Examples
Create a CPM Campaign
import { parseEther } from 'viem'
async function createCampaign() {
const budget = parseEther('1000') // 1000 PRIV
const cpm = parseEther('0.5') // 0.5 PRIV per impression
const cpc = parseEther('2') // 2 PRIV per click
const duration = 30 * 24 * 60 * 60 // 30 days
const metadataHash = '0x...' // Campaign metadata IPFS hash
// 1. Deposit funds
await writeContract({
address: PRIV_TOKEN_ADDRESS,
abi: privTokenAbi,
functionName: 'approve',
args: [ADNETWORK_ADDRESS, budget],
})
await writeContract({
address: ADNETWORK_ADDRESS,
abi: adNetworkAbi,
functionName: 'deposit',
args: [budget],
})
// 2. Create campaign
await writeContract({
address: ADNETWORK_ADDRESS,
abi: adNetworkAbi,
functionName: 'createCampaign',
args: [budget, cpm, cpc, metadataHash, duration],
})
}Check Publisher Earnings
function usePublisherStats(publisher: string) {
return useReadContract({
address: ADNETWORK_ADDRESS,
abi: adNetworkAbi,
functionName: 'getPublisherStats',
args: [publisher],
})
}Claim Earnings
async function claimEarnings() {
await writeContract({
address: ADNETWORK_ADDRESS,
abi: adNetworkAbi,
functionName: 'claimEarnings',
})
}Oracle: Confirm Impression Batch
// Called by authorized oracles
async function confirmBatch(
campaignId: bigint,
publisher: string,
impressions: bigint,
clicks: bigint,
batchId: `0x${string}`
) {
await writeContract({
address: ADNETWORK_ADDRESS,
abi: adNetworkAbi,
functionName: 'confirmImpressionBatch',
args: [campaignId, publisher, impressions, clicks, batchId],
})
}Campaign Metadata Schema
Recommended schema for metadataHash:
{
"name": "Q1 2025 Brand Campaign",
"description": "Awareness campaign for DeFi users",
"creatives": [
{
"type": "banner",
"size": "300x250",
"url": "ipfs://Qm.../banner-300x250.png"
},
{
"type": "banner",
"size": "728x90",
"url": "ipfs://Qm.../banner-728x90.png"
}
],
"targeting": {
"categories": ["defi", "nft", "trading"],
"geoTargeting": ["US", "EU", "APAC"]
},
"landingPage": "https://example.com/campaign",
"trackingPixel": "https://track.example.com/pixel"
}Security Notes
-
Multi-Oracle Consensus: A single compromised oracle cannot drain campaign budgets. Multiple oracles must agree on impression counts before publishers are credited.
-
Oracle Heartbeat: Oracles must maintain fresh heartbeats. Stale oracles (default: 1 hour without activity) cannot confirm batches.
-
Batch ID Replay Prevention: Each batch ID can only be processed once. Oracles must use unique batch IDs for each submission.
-
Protocol Fee: The 2.5% fee is deducted from publisher earnings, not advertiser deposits. Publishers receive 97.5% of the gross earnings.
-
Minimum Claim Threshold: Publishers must accumulate at least 10 PRIV before claiming. This reduces gas costs from frequent small claims. Emergency claims bypass this when the contract is paused.
-
Price Caps: Maximum prices prevent misconfigured campaigns from draining budgets too quickly.