A decentralized content monetization platform where creators publish content as NFTs and collectors "steal" them through Dutch auctions, earning rewards based on their stake.
- Overview
- How It Works
- Core Concepts
- Smart Contracts
- Architecture Diagram
- Token Economics
- Getting Started
- Integration Guide
- Testing
- Security
- License
Content Engine is a revolutionary platform that turns content into collectible assets. Here's the basic idea:
- Creators publish content as NFTs (articles, art, videos, etc.)
- Collectors can "steal" content by paying a Dutch auction price
- Previous owners receive 80% of what the new collector pays
- Everyone earns rewards based on how much they've paid for content
Think of it like a game where content continuously changes hands, and holding valuable content earns you rewards over time.
Key Benefits:
- For Creators: Earn 2% every time your content changes hands, forever
- For Collectors: Earn rewards proportional to your stake, plus 80% when someone steals from you
- For Teams: Earn 2% of all collections as the platform launcher
- For the Platform: 15% goes to treasury, fueling ecosystem growth
Creating content is simple:
-
Call
create()on the Content contract with:- Your address (you become the creator)
- A token URI pointing to your content metadata (IPFS, Arweave, etc.)
-
Your content becomes an NFT that can be collected
-
Earn 2% forever every time your content changes hands
Creator publishes content
|
v
NFT is minted to creator
|
v
Content is now collectible
|
v
Every collection = 2% to creator
Collecting content works like this:
- Browse available content and check current prices
- Call
collect()to "steal" content you want - The content is yours until someone steals it from you
- Earn rewards based on your stake (the price you paid)
- When someone steals from you, you get 80% of what they paid
This is what makes Content Engine unique. Unlike normal NFT ownership where you buy and hold, content can always be "stolen":
Alice creates content (owns it for free)
|
v
Bob pays 1 ETH to steal it from Alice
Alice receives: 0.8 ETH (80% of Bob's payment)
Bob now owns the content, stake = 1 ETH
|
v
Charlie pays 2 ETH to steal it from Bob
Bob receives: 1.6 ETH (80% of Charlie's payment)
Charlie now owns the content, stake = 2 ETH
|
v
And so on...
Why would you want your content stolen?
- You get 80% of the next person's payment
- If the price goes up, you profit!
- You also earn rewards while holding based on your stake
The price of collecting content follows a Dutch auction:
Price
|
| 2.0 ETH ████
| ███
| ███
| ███
| ███
| ███ → 0
|_________________________________ Time
Start 30 days
How it works:
- Price starts at 2x the last collection price
- Price linearly decays to 0 over 30 days (EPOCH_PERIOD)
- After 30 days, content can be collected for free
Formula:
Current Price = Initial Price × (1 - Time Elapsed / 30 days)
Why Dutch auctions?
- No sniping: Being first means paying MORE, not less
- Fair pricing: Price naturally settles at market value
- No bidding wars: Just decide when the price is right for you
There are two types of rewards in Content Engine:
-
Quote Token Rewards (WETH)
- Come from external sources sent to the Content contract
- Distributed proportionally to stakeholders
-
Unit Token Rewards
- The platform's native token
- Minted through the Minter contract with halving schedule
- Distributed to content collectors based on their stake
Your share of rewards = Your Stake / Total Stakes
Example:
Total stakes in Content: 100 ETH
Your stake: 5 ETH
Your share: 5%
If 10 ETH in rewards are distributed:
You receive: 0.5 ETH
When you collect content, the price you pay becomes your "stake":
- Stake = The price you paid to collect the content
- Total Supply = Sum of all stakes across all content
- Your Rewards = (Your Stake / Total Supply) × Reward Amount
The Rewarder contract uses a Synthetix-style algorithm for efficient reward distribution:
- Rewards accumulate over time (7-day distribution period)
- Claim anytime via the Rewarder contract
- No gas costs for distribution, only for claiming
Unit token emissions follow a Bitcoin-like halving schedule:
Period 1 (Days 1-30): Initial Rate (e.g., 100 UNIT/sec)
Period 2 (Days 31-60): 50% of Initial (e.g., 50 UNIT/sec)
Period 3 (Days 61-90): 25% of Initial (e.g., 25 UNIT/sec)
Period 4 (Days 91-120): 12.5% of Initial (e.g., 12.5 UNIT/sec)
...continues until tail emission rate
This creates:
- Scarcity: Total supply is bounded
- Early Incentive: Higher rewards for early participants
- Sustainability: Tail emissions ensure ongoing rewards
When content is collected, the payment is split:
| Recipient | Percentage | Description |
|---|---|---|
| Previous Owner | 80% | Reward for holding content |
| Treasury | 15% | Funds ecosystem via Auction |
| Creator | 2% | Ongoing royalty for content creator |
| Team | 2% | Platform/launcher team fee |
| Protocol | 1% | Platform maintenance fee |
Example with 1 ETH collection:
Previous Owner: 0.80 ETH
Treasury: 0.15 ETH
Creator: 0.02 ETH
Team: 0.02 ETH
Protocol: 0.01 ETH
Content contracts can operate in two modes:
Unmoderated Mode (Default for some deployments):
- All content is immediately collectible
- Anyone can create and collect
- Fully permissionless
Moderated Mode:
- New content requires moderator approval before collection
- Moderators are set by the contract owner
- Protects against spam/inappropriate content
- Content creators can still create, but collection is blocked until approved
isModerated = true:
Creator -> Create Content -> [Pending] -> Moderator Approves -> [Collectible]
isModerated = false:
Creator -> Create Content -> [Immediately Collectible]
Purpose: Central orchestrator and registry for the entire system.
Key Responsibilities:
- Launch new content ecosystems
- Store configuration parameters
- Maintain protocol fee address
- Register all deployed contracts
Key Functions:
// Launch a new content ecosystem
function launch(LaunchParams calldata params) external returns (
address unit,
address minter,
address auction,
address content
);LaunchParams:
| Parameter | Description |
|---|---|
launcher |
Owner of deployed contracts |
name |
Token name |
symbol |
Token symbol |
uri |
Metadata URI |
contentName |
NFT collection name |
contentSymbol |
NFT collection symbol |
contentUri |
NFT collection metadata URI |
isModerated |
Whether content requires approval |
initialUps |
Initial Unit emission rate |
tailUps |
Minimum emission rate |
halvingPeriod |
Time between halvings |
contentMinInitPrice |
Minimum auction starting price |
auctionInitPrice |
Treasury auction starting price |
auctionMinInitPrice |
Treasury auction floor price |
auctionEpochPeriod |
Treasury auction duration |
auctionPriceMultiplier |
Treasury auction price increase |
Purpose: ERC721 NFT contract where content lives and can be collected.
Key Features:
- Content creation by anyone
- Dutch auction collection mechanism
- Stake tracking for rewards
- Disabled transfers (only collect() works)
- Moderation support
Key Functions:
// Create new content
function create(address to, string memory tokenUri) external returns (uint256 tokenId);
// Collect (steal) content
function collect(
address to,
uint256 tokenId,
uint256 epochId, // Frontrun protection
uint256 deadline, // Transaction deadline
uint256 maxPrice // Slippage protection
) external returns (uint256 price);
// Distribute rewards to Rewarder
function distribute() external;
// Get current price for content
function getPrice(uint256 tokenId) external view returns (uint256);Events:
event Content__Created(address indexed who, address indexed to, uint256 indexed tokenId, string uri);
event Content__Collected(address indexed who, address indexed to, uint256 indexed tokenId, uint256 epochId, uint256 price);
event Content__Distributed(uint256 quoteAmount, uint256 unitAmount);Constants:
| Constant | Value | Description |
|---|---|---|
PREVIOUS_OWNER_FEE |
8000 (80%) | Fee to previous owner |
TREASURY_FEE |
1500 (15%) | Fee to treasury |
CREATOR_FEE |
200 (2%) | Fee to creator |
TEAM_FEE |
200 (2%) | Fee to team |
PROTOCOL_FEE |
100 (1%) | Fee to protocol |
EPOCH_PERIOD |
30 days | Dutch auction duration |
PRICE_MULTIPLIER |
2e18 (2x) | Price increase per epoch |
Purpose: Factory contract for deploying new Content contracts.
Key Functions:
// Deploy a new Content contract
function deploy(
string memory name,
string memory symbol,
string memory uri,
address unit,
address quote,
address treasury,
address core,
uint256 minInitPrice,
bool isModerated
) external returns (address content);Purpose: ERC20 reward token with controlled minting.
Key Features:
- Only the designated minter can mint tokens
- Minter is set once and cannot be changed
- Standard ERC20 functionality
Key Functions:
// Set the minter address (one-time)
function setMinter(address _minter) external;
// Mint tokens (only minter)
function mint(address to, uint256 amount) external;Purpose: Factory contract for deploying new Unit tokens.
Key Functions:
// Deploy a new Unit token
function deploy(
string memory name,
string memory symbol,
string memory uri,
address owner
) external returns (address unit);Purpose: Bitcoin-style halving emission controller.
Key Features:
- Calculates current emission rate based on halving schedule
- Mints tokens to a designated receiver
- Tracks last mint time for accurate emissions
- Owned by Content contract for permission control
Key Functions:
// Mint accumulated emissions
function mint() external returns (uint256 amount);
// Get current emission rate (units per second)
function getUps() external view returns (uint256);
// Get pending mintable amount
function getPendingMint() external view returns (uint256);Halving Calculation:
function getUps() public view returns (uint256) {
uint256 timePassed = block.timestamp - startTime;
uint256 halvings = timePassed / halvingPeriod;
if (halvings > 64) return tailUps;
uint256 ups = initialUps >> halvings; // Divide by 2^halvings
return ups > tailUps ? ups : tailUps;
}Purpose: Factory contract for deploying new Minter contracts.
Key Functions:
// Deploy a new Minter
function deploy(
address unit,
address receiver,
uint256 initialUps,
uint256 tailUps,
uint256 halvingPeriod
) external returns (address minter);Purpose: Synthetix-style staking rewards distributor.
Key Features:
- Tracks stakes for each user
- Distributes multiple reward tokens
- 7-day reward distribution period
- Efficient O(1) reward calculation
Key Functions:
// Called by Content when stake changes
function deposit(address account, uint256 amount) external;
function withdraw(address account, uint256 amount) external;
// Users claim their rewards
function getReward(address account) external;
// Content contract notifies new rewards
function notifyRewardAmount(address rewardToken, uint256 amount) external;
// Add a new reward token
function addReward(address rewardToken) external;
// View functions
function earned(address account, address rewardToken) external view returns (uint256);
function left(address rewardToken) external view returns (uint256);Reward Calculation:
rewardPerToken += (rewardRate * timeDelta * 1e18) / totalSupply
earned = balance * (rewardPerToken - userRewardPerTokenPaid) / 1e18 + storedRewards
Purpose: Factory contract for deploying new Rewarder contracts.
Key Functions:
// Deploy a new Rewarder
function deploy(address stakingToken) external returns (address rewarder);Purpose: Dutch auction for treasury assets.
Key Features:
- Burns LP tokens (or other payment tokens) as payment
- Distributes accumulated treasury assets
- Dutch auction pricing with configurable parameters
Key Functions:
// Buy treasury assets via auction
function buy(
address[] calldata assets,
address assetsReceiver,
uint256 epochId,
uint256 deadline,
uint256 maxPaymentTokenAmount
) external returns (uint256 paymentAmount);
// View current price
function getPrice() external view returns (uint256);Purpose: Factory contract for deploying new Auction contracts.
Key Functions:
// Deploy a new Auction
function deploy(
address paymentToken,
uint256 initPrice,
uint256 minInitPrice,
uint256 epochPeriod,
uint256 priceMultiplier
) external returns (address auction);Purpose: Helper contract for batching operations and ETH wrapping.
Key Features:
- Wrap ETH to WETH automatically
- Batch multiple operations
- Query contract states efficiently
Key Functions:
// Collect content with ETH (auto-wraps)
function collect(
address content,
uint256 tokenId,
uint256 epochId,
uint256 deadline,
uint256 maxPrice
) external payable;
// Get Content state
function getContent(
address content,
uint256[] calldata tokenIds,
address account
) external view returns (ContentState memory);
// Get Auction state
function getAuction(
address auction,
address account
) external view returns (AuctionState memory);
// Get Minter state
function getMinter(address minter) external view returns (MinterState memory); ┌─────────────────────────────────────────────────────┐
│ CORE │
│ (Orchestrator & Registry) │
└────────────────────────┬────────────────────────────┘
│
┌──────────────┬────────────────────┼────────────────────┬──────────────┐
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ UnitFactory │ │MinterFactory│ │ContentFactory│ │AuctionFactory│ │RewarderFactory│
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │ │ │
▼ ▼ ▼ ▼ ▼
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Unit │ │ Minter │ │ Content │ │ Auction │ │ Rewarder │
│ (ERC20) │ │ (Emissions) │ │ (NFTs) │ │ (Treasury) │ │ (Stakes) │
└─────────────┘ └──────┬──────┘ └──────┬──────┘ └─────────────┘ └──────┬──────┘
│ │ │
│ │ │
└──────────────────┴──────────────────────────────────┘
│
▼
┌─────────────────────┐
│ Multicall │
│ (Helper & Queries) │
└─────────────────────┘
Flow of Value:
═══════════════
Collector pays WETH to collect content
│
├──► 80% to Previous Owner
├──► 15% to Treasury (Auction)
├──► 2% to Creator
├──► 2% to Team
└──► 1% to Protocol
Minter produces UNIT tokens
│
└──► Sent to Content contract
│
└──► Distributed via Rewarder to stakeholders
Treasury accumulates fees
│
└──► Auctioned to LP holders (LP burned)
The Unit token has a bounded supply determined by:
Max Supply ≈ Initial Rate × Halving Period × 2 + Tail Rate × ∞
Example with default parameters:
- Initial Rate: 1e18 UNIT/second (1 UNIT/sec)
- Halving Period: 30 days
- Tail Rate: 1e15 UNIT/second (0.001 UNIT/sec)
First 30 days: 1 UNIT/sec = 2,592,000 UNIT
Next 30 days: 0.5 UNIT/sec = 1,296,000 UNIT
Next 30 days: 0.25 UNIT/sec = 648,000 UNIT
...and so on until tail rate
┌─────────────────┐
│ Collectors │
│ (Pay to collect)│
└────────┬────────┘
│ WETH
▼
┌─────────────────┐ ┌─────────────────┐
│ Content │────►│ Rewarder │
│ Contract │ │ (Distributes) │
└────────┬────────┘ └────────┬────────┘
│ │
┌────┴────┬────┬────┐ │ WETH + UNIT rewards
│ │ │ │ ▼
▼ ▼ ▼ ▼ ┌─────────────────┐
80% 15% 4% 1% │ Stakeholders │
Prev Treas Creator Proto │ (Claim rewards) │
Owner ury └─────────────────┘
# Clone the repository
git clone https://github.com/your-repo/content-engine-hardhat.git
cd content-engine-hardhat
# Install dependencies
npm install
# Compile contracts
npx hardhat compile- Configure environment:
# Create .env file
PRIVATE_KEY=your_deployer_private_key
RPC_URL=https://mainnet.base.org
SCAN_API_KEY=your_basescan_api_key- Configure deployment parameters in deployment script:
const WETH_ADDRESS = "0x4200000000000000000000000000000000000006";
const PROTOCOL_FEE_ADDRESS = "0x...";- Deploy:
npx hardhat run scripts/deploy.js --network baseCore Parameters:
| Parameter | Typical Value | Description |
|---|---|---|
minDonutForLaunch |
100e18 | Minimum DONUT to launch |
initialUnitMintAmount |
1_000_000e18 | Initial LP Unit amount |
Content Parameters:
| Parameter | Typical Value | Description |
|---|---|---|
minInitPrice |
1e15 (0.001 ETH) | Minimum starting auction price |
EPOCH_PERIOD |
30 days | Auction duration (constant) |
PRICE_MULTIPLIER |
2e18 (2x) | Price increase (constant) |
Minter Parameters:
| Parameter | Typical Value | Description |
|---|---|---|
initialUps |
1e18 | Initial emission rate (UNIT/sec) |
tailUps |
1e15 | Minimum emission rate |
halvingPeriod |
30 days | Time between halvings |
Auction Parameters:
| Parameter | Typical Value | Description |
|---|---|---|
initPrice |
1e18 | Starting auction price |
minInitPrice |
1e15 | Floor price |
epochPeriod |
7 days | Auction duration |
priceMultiplier |
1.5e18 | Price increase per epoch |
const { ethers } = require("ethers");
// Connect to Content contract
const content = new ethers.Contract(contentAddress, contentAbi, signer);
// Create content
const tx = await content.create(
creatorAddress, // Content creator address
"ipfs://QmYourContentHash" // Metadata URI
);
const receipt = await tx.wait();
// Get token ID from event
const event = receipt.events.find(e => e.event === "Content__Created");
const tokenId = event.args.tokenId;
console.log(`Created content #${tokenId}`);// Check current price
const price = await content.getPrice(tokenId);
console.log(`Current price: ${ethers.utils.formatEther(price)} ETH`);
// Get auction data for epochId
const auction = await content.getAuction(tokenId);
const epochId = auction.epochId;
// Set deadline (5 minutes from now)
const deadline = Math.floor(Date.now() / 1000) + 300;
// Set max price with slippage (5% above current)
const maxPrice = price.mul(105).div(100);
// Approve WETH spending
const weth = new ethers.Contract(wethAddress, erc20Abi, signer);
await weth.approve(contentAddress, maxPrice);
// Collect the content
const tx = await content.collect(
collectorAddress, // Who receives the NFT
tokenId, // Token ID to collect
epochId, // Frontrun protection
deadline, // Transaction deadline
maxPrice // Slippage protection
);
await tx.wait();
console.log("Content collected!");// Get current price and epoch
const price = await content.getPrice(tokenId);
const auction = await content.getAuction(tokenId);
const epochId = auction.epochId;
const deadline = Math.floor(Date.now() / 1000) + 300;
const maxPrice = price.mul(105).div(100);
// Collect with ETH via Multicall (auto-wraps to WETH)
const multicall = new ethers.Contract(multicallAddress, multicallAbi, signer);
const tx = await multicall.collect(
contentAddress,
tokenId,
epochId,
deadline,
maxPrice,
{ value: maxPrice } // Send ETH
);
await tx.wait();// Connect to Rewarder contract
const rewarder = new ethers.Contract(rewarderAddress, rewarderAbi, signer);
// Check earned rewards
const earnedWeth = await rewarder.earned(userAddress, wethAddress);
const earnedUnit = await rewarder.earned(userAddress, unitAddress);
console.log(`Earned WETH: ${ethers.utils.formatEther(earnedWeth)}`);
console.log(`Earned UNIT: ${ethers.utils.formatEther(earnedUnit)}`);
// Claim all rewards
const tx = await rewarder.getReward(userAddress);
await tx.wait();
console.log("Rewards claimed!");// Listen for new content creation
content.on("Content__Created", (who, to, tokenId, uri) => {
console.log(`New content #${tokenId} created by ${who} for ${to}`);
});
// Listen for collections
content.on("Content__Collected", (who, to, tokenId, epochId, price) => {
console.log(`Content #${tokenId} collected by ${to} for ${ethers.utils.formatEther(price)} ETH`);
});
// Listen for reward distributions
content.on("Content__Distributed", (quoteAmount, unitAmount) => {
console.log(`Distributed: ${ethers.utils.formatEther(quoteAmount)} WETH, ${ethers.utils.formatEther(unitAmount)} UNIT`);
});# Run all tests
npx hardhat test
# Run specific test file
npx hardhat test tests/testContent.js
# Run with gas reporting
REPORT_GAS=true npx hardhat test
# Run tests in parallel
npx hardhat test --parallel| File | Description | Tests |
|---|---|---|
testContent.js |
Content contract core functionality | Creation, collection, fees |
testSecurityAudit.js |
Security-focused tests | Reentrancy, access control, exploits |
testExtreme.js |
Stress and edge case tests | Attack vectors, integration |
testBoundary.js |
Boundary condition tests | Min/max values, overflow |
testInvariants.js |
Invariant property tests | State consistency |
Total Tests: 263 passing
This codebase has undergone comprehensive security review:
| Category | Finding |
|---|---|
| Critical | 0 |
| High | 0 |
| Medium | 0 |
| Low | 2 (accepted) |
| Informational | 3 |
1. Reentrancy Protection
- All external calls use
nonReentrantmodifier - State changes before external calls (CEI pattern)
- SafeERC20 for token transfers
2. Front-Running Protection
epochIdparameter prevents replay attacksdeadlineprevents stale transactionsmaxPriceprovides slippage protection
3. Access Control
- Owner-only functions protected by Ownable
- Moderator system for content approval
- One-time minter setting for Unit token
4. Safe Math
- Solidity 0.8.19 with built-in overflow checks
- Explicit bounds on price calculations
- ABS_MAX_INIT_PRICE prevents overflow in price multiplier
5. Input Validation
- Zero address checks
- Empty string checks
- Price bounds validation
Immutable After Deployment:
- Rewarder address
- Unit token address
- Quote token address
- Core address
- Minimum initial price
- Fee percentages (constants)
- Epoch period (constant)
- Price multiplier (constant)
Owner Controlled:
- Treasury address
- Team address
- Moderation mode
- Moderator list
- Metadata URI
Not Possible:
- Minting Unit outside of Minter
- Collecting without paying auction price
- Withdrawing stakes directly
- Bypassing fee distribution
- Transferring content NFTs (only collect works)
-
Block Timestamp Dependence: Dutch auction prices depend on
block.timestamp, which can be manipulated by miners within ~15 second range. This is acceptable given the 30-day auction period. -
First Content Free: When content is created, the creator owns it with 0 stake. They earn nothing from Rewarder until someone collects from them.
-
Moderation Centralization: In moderated mode, owner/moderators control which content can be collected. This is by design for spam prevention.
MIT
For questions, issues, or contributions:
- Open an issue on GitHub
- Submit a pull request
- Join our Discord community