Softlaw Marketplace Contracts
Smart contracts for the Softlaw marketplace, built with Foundry for deployment on Polkadot's Asset Hub (Passet Hub TestNet).
Development Environment
Option 1: DevContainer (Recommended)
This project includes a pre-configured DevContainer with all tools installed:
Requirements:
- Docker
- VS Code with Remote-Containers extension
Setup:
- Open project in VS Code
- Click "Reopen in Container" when prompted
- Add your
PRIVATE_KEYto.envfile - Start developing - Foundry and all dependencies are pre-installed
The DevContainer automatically:
- Installs Foundry and Solidity tools
- Loads your
.envfile - Configures the development environment
- Sets up git and shell aliases
Option 2: Manual Setup
Install Foundry:
curl -L https://foundry.paradigm.xyz | bash
foundryup
Setup Environment:
cp .env.example .env
# Add your PRIVATE_KEY to .env
Get Testnet Tokens: Polkadot Faucet
Build & Test
forge build # Build contracts
forge test # Run all tests
forge test --gas-report # With gas usage
forge coverage # Coverage report
Deployment
See Deployment Guide for complete instructions.
Quick deploy all contracts:
forge script script/DeployProduction.s.sol:DeployProduction \
--rpc-url passetHub \
--broadcast \
--legacy \
-vv
Network Information
Passet Hub (Testnet)
- Network: Polkadot Asset Hub TestNet
- Chain ID:
420420422 - RPC:
https://testnet-passet-hub-eth-rpc.polkadot.io - Explorer: https://blockscout-passet-hub.parity-testnet.parity.io
- Faucet: https://faucet.polkadot.io/?parachain=1111
RPC endpoint configured in foundry.toml as passetHub.
Documentation
Build and View
# Build documentation
./build-docs.sh
# View locally
open docs/book/index.html
# Or serve with live reload
cd docs && mdbook serve --open
What's Included
- Architecture Diagrams: 16 Mermaid diagrams showing system flow, contract interactions, and state machines
- Contract Reference: Auto-generated from NatSpec in interfaces
- User Flows: Step-by-step sequences for key operations
Contract Architecture
The marketplace consists of 5 core contracts:
| Contract | Type | Description |
|---|---|---|
| IPAsset | ERC721 | IP asset ownership and licensing |
| LicenseToken | ERC1155 | License ownership and management |
| GovernanceArbitrator | Governance | Dispute resolution and arbitration |
| Marketplace | Trading | IP asset and license marketplace |
| RevenueDistributor | Logic | Revenue sharing and royalties |
All contracts except RevenueDistributor use UUPS upgradeable pattern.
Project Structure
.
├── src/ # Smart contracts
│ ├── interfaces/ # Contract interfaces
│ ├── IPAsset.sol
│ ├── LicenseToken.sol
│ ├── GovernanceArbitrator.sol
│ ├── Marketplace.sol
│ └── RevenueDistributor.sol
├── script/ # Deployment scripts
│ ├── deployment-guide.md # Deployment instructions
│ ├── DeployProduction.s.sol # Deploy all contracts
│ ├── DeployIPAsset.s.sol # Individual deployments
│ └── SetupAddresses.s.sol # Wire contracts together
└── test/ # Test files
Testing
# Run all tests
forge test
# Specific test file
forge test --match-path "test/IPAsset.t.sol"
# Specific contract tests
forge test --match-contract IPAssetTest
# With gas reporting
forge test --gas-report
# Coverage
forge coverage
forge coverage --report html && open coverage/index.html
Verification
After deployment, verify contracts:
# Check contract exists
cast code <CONTRACT_ADDRESS> --rpc-url passetHub
# Read contract data
cast call <CONTRACT_ADDRESS> "name()(string)" --rpc-url passetHub
# Send transaction
cast send <CONTRACT_ADDRESS> "mintIP(address,string)" $YOUR_ADDRESS "ipfs://metadata" \
--rpc-url passetHub \
--private-key $PRIVATE_KEY \
--legacy
System Architecture Overview
The Softlaw Marketplace consists of five core smart contracts that work together to enable IP asset trading, licensing, and revenue distribution.
Contract Flow
The system flow starts with IP asset creation and follows this sequence:
graph LR
USER[User] -->|1. Mint IP NFT| IP[IPAsset<br/>ERC-721]
IP -->|2. Create License<br/>delegates to| LT[LicenseToken<br/>ERC-1155]
LT -->|3. List License| MP[Marketplace]
BUYER[Buyer] -->|4. Buy + ETH| MP
MP -->|5. Transfer License| BUYER
MP -->|6. Distribute + ETH| RD[RevenueDistributor]
RD -->|7a. Platform Fee| TREASURY[Treasury]
RD -->|7b. Revenue Split| OWNER[IP Owner]
style IP fill:#e1f5ff
style LT fill:#fff4e1
style MP fill:#ffe1f5
style RD fill:#e1ffe1
style TREASURY fill:#ffffcc
Key Points
- IPAsset is the entry point - users mint IP assets as ERC-721 NFTs
- License creation is delegated - IPAsset.mintLicense() calls LicenseToken.mintLicense()
- Payment flow is automatic - when a license sells, payment is immediately distributed
- Platform fee deducted first - RevenueDistributor takes fee, then splits remainder
Contract Architecture
graph TB
subgraph "Main Flow"
IP[IPAsset<br/>ERC-721<br/>Entry Point]
LT[LicenseToken<br/>ERC-1155<br/>License NFTs]
MP[Marketplace<br/>Trading]
RD[RevenueDistributor<br/>Payment Splits]
end
subgraph "Dispute Resolution"
GA[GovernanceArbitrator<br/>Arbitration]
end
IP -->|mints licenses| LT
LT -->|listed on| MP
MP -->|distributes fees| RD
GA -.->|revokes| LT
GA -.->|updates status| IP
LT -.->|updates count| IP
style IP fill:#e1f5ff
style LT fill:#fff4e1
style MP fill:#ffe1f5
style RD fill:#e1ffe1
style GA fill:#f5e1ff
Contract Relationships
1. IPAsset (ERC-721) - Entry Point
Purpose: Represents intellectual property ownership
Key Functions:
- Mints IP assets as NFTs (native or wrapped)
- Wraps external NFTs into IPAsset for licensing (custodial)
- Creates licenses by delegating to LicenseToken
- Tracks active license count for burn protection
- Receives dispute status updates from GovernanceArbitrator
Flow: Users mint native IP assets or wrap existing NFTs, then create licenses to sell.
2. LicenseToken (ERC-1155) - License Management
Purpose: Semi-fungible license tokens
Key Functions:
- Manages license lifecycle (expiry, revocation)
- Controls private metadata access
- Supports one-time and recurring payment licenses
- Per-license penalty rates (configurable 0-50%, defaults to 5%)
- Per-license maxMissedPayments (configurable 1-255, defaults to 3)
- Updates IPAsset license count
Flow: Created by IPAsset, then listed on Marketplace for trading.
3. Marketplace - Trading Platform
Purpose: Lists and sells licenses
Key Functions:
- Lists licenses for sale with fixed prices
- Handles offers with escrow
- Manages recurring payments with penalties
- 3-day grace period before penalties start accruing
- Distributes sale proceeds via RevenueDistributor
Flow: When a license is sold, payment is automatically distributed.
4. RevenueDistributor - Payment Distribution
Purpose: Splits payments according to configured shares
Key Functions:
- Implements EIP-2981 royalty standard
- Configures revenue splits per IP asset
- Handles primary vs secondary sales (tracked by Marketplace contract)
- Per-asset royalty rates (custom or default)
- Handles platform fees (deducted first for primary, royalties for secondary)
- Manages withdrawals for all recipients
Flow: Receives payments from Marketplace sales, distributes to IP owners and collaborators.
5. GovernanceArbitrator - Dispute Arbitration
Purpose: Third-party arbitration for license disputes with 30-day resolution deadline
Key Functions:
- Any party can submit disputes with evidence
- Designated arbitrators resolve disputes within 30 days
- Can revoke licenses when disputes are approved
- Updates IP asset dispute status
- No governance voting - pure arbitration model
Flow: Separate from main trading flow, invoked when disputes arise.
Upgradeability
All contracts except RevenueDistributor use UUPS upgradeable pattern for future improvements.
graph LR
subgraph "Upgradeable (UUPS)"
IP[IPAsset]
LT[LicenseToken]
MP[Marketplace]
GA[GovernanceArbitrator]
end
subgraph "Non-Upgradeable"
RD[RevenueDistributor]
end
style IP fill:#e1f5ff
style LT fill:#fff4e1
style MP fill:#ffe1f5
style GA fill:#f5e1ff
style RD fill:#ffcccc
Access Control
All contracts use OpenZeppelin's AccessControl for role-based permissions:
Admin Operations (DEFAULT_ADMIN_ROLE only):
- Pause/unpause contracts
- Upgrade contracts (UUPS)
- Grant/revoke all roles
- Update contract addresses
- Set default penalty rates and royalty rates
Cross-Contract Roles:
- IP_ASSET_ROLE: Granted to IPAsset → allows minting licenses in LicenseToken
- LICENSE_MANAGER_ROLE: Granted to LicenseToken → allows updating license counts in IPAsset
- ARBITRATOR_ROLE: Granted to GovernanceArbitrator → allows dispute resolution and revocation
- MARKETPLACE_ROLE: Granted to Marketplace → allows auto-revocation for missed payments
- CONFIGURATOR_ROLE: Granted to IPAsset → allows configuring revenue splits in RevenueDistributor
User Flows
Key user journeys through the Softlaw Marketplace system.
Creating IP Assets
Native IP Creation
sequenceDiagram
actor Owner as IP Owner
participant IP as IPAsset
participant LT as LicenseToken
participant RD as RevenueDistributor
Owner->>IP: mintIP(metadata)
IP->>IP: Mint ERC-721 token
IP-->>Owner: tokenId
Owner->>RD: configureSplit(tokenId, recipients, shares)
RD->>RD: Store revenue split
RD-->>Owner: Split configured
Owner->>IP: mintLicense(ipTokenId, licensee, params)
IP->>LT: mintLicense(to, ipAssetId, params)
LT->>LT: Create ERC-1155 license
LT->>IP: updateActiveLicenseCount(tokenId, +1)
LT-->>IP: licenseId
IP-->>Owner: licenseId
Wrapping External NFTs
sequenceDiagram
actor Owner as NFT Owner
participant NFT as External NFT
participant IP as IPAsset
participant LT as LicenseToken
Note over Owner,NFT: Owner has existing NFT<br/>(e.g., Bored Ape, CryptoPunk)
Owner->>NFT: approve(IPAsset, tokenId)
Owner->>IP: wrapNFT(nftContract, nftTokenId, metadata)
IP->>NFT: ownerOf(nftTokenId)
NFT-->>IP: owner address
IP->>IP: Check not already wrapped
IP->>IP: Mint IPAsset token
IP->>NFT: safeTransferFrom(owner, IPAsset, nftTokenId)
NFT->>NFT: Transfer NFT to IPAsset (custody)
IP-->>Owner: ipTokenId
Note over Owner,IP: NFT locked in IPAsset contract<br/>Owner controls via IPAsset token
Owner->>IP: mintLicense(ipTokenId, licensee, params)
IP->>LT: mintLicense(...)
LT-->>IP: licenseId
Unwrapping NFTs
sequenceDiagram
actor Owner as IPAsset Owner
participant IP as IPAsset
participant NFT as External NFT
Note over Owner,IP: Owner wants original NFT back
Owner->>IP: unwrapNFT(ipTokenId)
IP->>IP: Check owner
IP->>IP: Check no active licenses
IP->>IP: Check no active disputes
IP->>IP: Burn IPAsset token
IP->>NFT: safeTransferFrom(IPAsset, owner, nftTokenId)
NFT-->>Owner: Original NFT returned
Note over Owner,NFT: Cannot unwrap if licenses/disputes exist
Buying a Listed License
sequenceDiagram
actor Seller
actor Buyer
participant MP as Marketplace
participant LT as LicenseToken
participant RD as RevenueDistributor
Seller->>LT: approve(Marketplace, licenseId)
Seller->>MP: createListing(nftContract, tokenId, price)
MP->>MP: Store listing
MP-->>Seller: listingId
Buyer->>MP: buyListing(listingId) + ETH
MP->>LT: safeTransferFrom(seller, buyer, licenseId)
MP->>RD: distributePayment(ipAssetId, amount, seller)
RD->>RD: Auto-detect primary/secondary sale
RD->>RD: Calculate fees & splits accordingly
RD->>RD: Update balances
MP-->>Buyer: License transferred
Note over Seller,RD: Primary sale: platform fee + split to recipients<br/>Secondary sale: royalty to IP owners, rest to seller<br/>All parties can withdraw later
Making an Offer
sequenceDiagram
actor Buyer
actor Seller
participant MP as Marketplace
participant LT as LicenseToken
participant RD as RevenueDistributor
Buyer->>MP: createOffer(nftContract, tokenId, expiry) + ETH
MP->>MP: Store offer with escrowed funds
MP-->>Buyer: offerId
Note over Seller: Seller decides to accept
Seller->>LT: approve(Marketplace, tokenId)
Seller->>MP: acceptOffer(offerId)
MP->>LT: safeTransferFrom(seller, buyer, tokenId)
MP->>RD: distributePayment(ipAssetId, amount, seller)
RD->>RD: Auto-detect primary/secondary sale
RD->>RD: Calculate fees & splits accordingly
RD->>RD: Update balances
MP-->>Seller: Offer accepted
Note over Seller,RD: Primary sale: platform fee + split to recipients<br/>Secondary sale: royalty to IP owners, rest to seller
alt Buyer cancels before acceptance
Buyer->>MP: cancelOffer(offerId)
MP->>Buyer: Refund escrowed ETH
end
Recurring Payments (Subscription Licenses)
sequenceDiagram
actor Licensee
participant MP as Marketplace
participant LT as LicenseToken
participant RD as RevenueDistributor
Note over Licensee,MP: License has paymentInterval > 0<br/>maxMissedPayments configured (default: 3)<br/>penaltyRateBPS configured (default: 500 = 5%)
loop Every payment interval
Licensee->>MP: getTotalPaymentDue(licenseContract, licenseId)
MP-->>Licensee: baseAmount, penalty (if > 3 days overdue), total
Note over Licensee,MP: Grace period: 3 days after due date<br/>No penalty if paid within grace period
Licensee->>MP: makeRecurringPayment(licenseContract, licenseId) + ETH
MP->>MP: Calculate missed payments
alt < maxMissedPayments
MP->>RD: distributePayment(ipAssetId, amount, seller)
RD->>RD: Distribute payment (typically primary sale)
MP-->>Licensee: Payment successful
else >= maxMissedPayments
MP->>LT: revokeForMissedPayments(licenseId, missedCount)
LT->>LT: Mark license as revoked
MP-->>Licensee: License auto-revoked
end
end
Dispute Resolution
sequenceDiagram
actor User
actor Arbitrator
participant GA as GovernanceArbitrator
participant LT as LicenseToken
participant IP as IPAsset
User->>GA: submitDispute(licenseId, reason, proofURI)
GA->>LT: Check license is active
GA->>IP: setDisputeStatus(tokenId, true)
GA->>GA: Create dispute record
GA-->>User: disputeId
Note over Arbitrator,GA: Within 30 days
Arbitrator->>GA: resolveDispute(disputeId, approved, reason)
GA->>GA: Update dispute status
GA-->>Arbitrator: Dispute resolved
alt Dispute approved
Arbitrator->>GA: executeRevocation(disputeId)
GA->>LT: revokeLicense(licenseId, reason)
LT->>LT: Mark license revoked
LT->>IP: updateActiveLicenseCount(tokenId, -1)
GA->>IP: setDisputeStatus(tokenId, false)
else Dispute rejected
GA->>IP: setDisputeStatus(tokenId, false)
end
Revenue Withdrawal
sequenceDiagram
actor Recipient
participant RD as RevenueDistributor
Recipient->>RD: getBalance(address)
RD-->>Recipient: balance
Recipient->>RD: withdraw()
RD->>RD: Check balance > 0
RD->>RD: Reset balance to 0
RD->>Recipient: Transfer ETH
RD-->>Recipient: Withdrawal successful
Revenue Flow
How payments are distributed through the system.
Primary vs Secondary Sales
The system automatically detects whether a sale is primary or secondary:
- Primary Sale: First sale of an IP asset or license → Platform fee applies
- Secondary Sale: Subsequent sales of the same IP asset or license → Royalty fee applies
Payment Distribution Overview
RevenueDistributor handles payment splitting differently for primary and secondary sales:
graph TB
PAYMENT[Total Payment<br/>1000 ETH]
subgraph "Step 1: Platform Fee"
FEE[Platform Fee<br/>2.5% = 25 ETH]
NET[Remaining<br/>975 ETH]
end
subgraph "Step 2: Revenue Split"
SPLIT{Split<br/>Configured?}
OWNER_ONLY[All to<br/>IP Owner<br/>975 ETH]
SPLIT_MODE[Split by<br/>Shares]
R1[Owner 70%<br/>682.5 ETH]
R2[Collab 30%<br/>292.5 ETH]
end
subgraph "Balances"
TREASURY[Treasury: 25 ETH]
B1[Owner Balance]
B2[Collab Balance]
end
PAYMENT --> FEE
PAYMENT --> NET
FEE --> TREASURY
NET --> SPLIT
SPLIT -->|No| OWNER_ONLY
SPLIT -->|Yes| SPLIT_MODE
SPLIT_MODE --> R1
SPLIT_MODE --> R2
OWNER_ONLY --> B1
R1 --> B1
R2 --> B2
B1 -.->|withdraw| W1[Withdraws]
B2 -.->|withdraw| W2[Withdraws]
TREASURY -.->|withdraw| W3[Withdraws]
style FEE fill:#ffcccc
style NET fill:#ccffcc
style TREASURY fill:#ffffcc
Distribution Rules
For Primary Sales (first sale of an IP asset or license):
- Platform fee is calculated on total amount and deducted first (e.g., 2.5%)
- If revenue split configured, remaining amount split by shares (must sum to 100%)
- If no split configured, all remaining amount goes to IP asset owner
For Secondary Sales (subsequent sales):
- Royalty fee is calculated on total amount (default or per-asset custom rate)
- Royalty amount is distributed according to revenue split configuration
- Remaining amount goes to the seller
General Rules: 4. Balances accumulate until recipient calls withdraw() 5. Pull-based withdrawals - recipients control when to withdraw
Primary Sale Payment Flow
When this is the first sale of an IP asset or license:
sequenceDiagram
participant Buyer
participant MP as Marketplace
participant RD as RevenueDistributor
participant Treasury
participant IPOwner as IP Owner (Seller)
participant Collaborator
Buyer->>MP: buyListing() + 1000 ETH
MP->>RD: distributePayment(ipAssetId, 1000, ipOwner) + 1000 ETH
Note over RD: Marketplace tracks first sale<br/>→ PRIMARY SALE
Note over RD: Platform fee = 2.5% (25 ETH)
Note over RD: Net amount = 975 ETH
RD->>RD: balances[treasury] += 25 ETH
Note over RD: Split configured:<br/>IP Owner: 70% (700 basis points)<br/>Collaborator: 30% (300 basis points)
RD->>RD: balances[ipOwner] += 682.5 ETH (70% of 975)
RD->>RD: balances[collaborator] += 292.5 ETH (30% of 975)
RD-->>MP: Payment distributed
Note over Treasury,Collaborator: Later, recipients withdraw
IPOwner->>RD: withdraw()
RD->>IPOwner: Transfer 682.5 ETH
Collaborator->>RD: withdraw()
RD->>Collaborator: Transfer 292.5 ETH
Treasury->>RD: withdraw()
RD->>Treasury: Transfer 25 ETH
Secondary Sale Payment Flow
When this is a subsequent sale of a previously sold IP asset or license:
sequenceDiagram
participant Buyer
participant MP as Marketplace
participant Seller as Licensee (Seller)
participant RD as RevenueDistributor
participant IPOwner as IP Owner
participant Collaborator
Buyer->>MP: buyListing() + 1000 ETH
MP->>RD: distributePayment(ipAssetId, 1000, seller) + 1000 ETH
Note over RD: Marketplace tracks subsequent sale<br/>→ SECONDARY SALE
Note over RD: Royalty rate = 10% (100 ETH)
Note over RD: Seller gets = 900 ETH
RD->>RD: balances[seller] += 900 ETH
Note over RD: Royalty split by configured shares:<br/>IP Owner: 70%<br/>Collaborator: 30%
RD->>RD: balances[ipOwner] += 70 ETH (70% of royalty)
RD->>RD: balances[collaborator] += 30 ETH (30% of royalty)
RD-->>MP: Payment distributed
Note over Seller,Collaborator: Later, recipients withdraw
Seller->>RD: withdraw()
RD->>Seller: Transfer 900 ETH
IPOwner->>RD: withdraw()
RD->>IPOwner: Transfer 70 ETH
Collaborator->>RD: withdraw()
RD->>Collaborator: Transfer 30 ETH
Recurring Payment Flow
sequenceDiagram
participant Licensee
participant MP as Marketplace
participant RD as RevenueDistributor
participant IPOwner as IP Owner
Note over Licensee,MP: Payment due: 100 ETH base + 5 ETH penalty
Licensee->>MP: makeRecurringPayment(licenseId) + 105 ETH
MP->>MP: Calculate missedPayments
MP->>MP: Update lastPaymentTime
MP->>RD: distributePayment(ipAssetId, 105) + 105 ETH
Note over RD: Platform fee = 2.5% (2.625 ETH)
Note over RD: Net amount = 102.375 ETH
RD->>RD: balances[treasury] += 2.625 ETH
RD->>RD: balances[ipOwner] += 102.375 ETH
RD-->>MP: Payment distributed
MP-->>Licensee: Payment successful
Note over Licensee,IPOwner: Penalty increases net payment to IP owner
Revenue Split Configuration
Revenue splits are configured per IP asset in basis points (1 basis point = 0.01%).
graph LR
subgraph "Example Split Configuration"
NET[Net Amount<br/>10000 basis points = 100%]
NET --> A[Creator: 5000 bp<br/>50%]
NET --> B[Contributor: 3000 bp<br/>30%]
NET --> C[Investor: 2000 bp<br/>20%]
end
style NET fill:#e1f5ff
style A fill:#ffe1f5
style B fill:#fff4e1
style C fill:#e1ffe1
Requirements
- All shares must sum to exactly 10000 basis points (100%)
- No recipient address can be zero address
- At least one recipient required
- Split can only be configured by IP owner or
CONFIGURATOR_ROLE
Royalty Configuration
Royalty rates can be set globally (default) or per IP asset:
Default Royalty:
- Applied to all IP assets unless overridden
- Set by admin via
setDefaultRoyalty(basisPoints) - Example: 1000 basis points = 10%
Per-Asset Royalty:
- Custom royalty for specific IP assets
- Set by CONFIGURATOR_ROLE via
setAssetRoyalty(ipAssetId, basisPoints) - Overrides default royalty
- Example: High-value IP might have 1500 bp (15%), while others use default
Querying Royalty:
- Use
getAssetRoyalty(ipAssetId)to get effective rate (custom or default) - Returns custom rate if set, otherwise returns default rate
Withdrawal Pattern
All recipients (platform treasury, IP owners, collaborators) use the same withdrawal mechanism:
stateDiagram-v2
[*] --> HasBalance: Revenue distributed
HasBalance --> Withdrawn: withdraw()
Withdrawn --> [*]
HasBalance --> HasMoreBalance: Additional revenue
HasMoreBalance --> Withdrawn: withdraw()
note right of HasBalance
Balance accumulates
from multiple sales
end note
note right of Withdrawn
Balance reset to 0
ETH transferred
end note
Benefits
- Gas efficient (no iterating through recipients)
- Recipients control their own withdrawals
- Supports multiple revenue sources accumulating
- No risk of failed transfers blocking other recipients
State Machines
NFT Wrapping
IPAsset supports wrapping external NFTs into the licensing system using a custodial approach.
How It Works
When an NFT is wrapped:
- The NFT transfers to the IPAsset contract (custody)
- IPAsset mints a new token representing the wrapped NFT
- The IPAsset token grants all licensing rights
- The owner controls everything through the IPAsset token
Why Custodial?
Prevents ownership desync. If Alice wraps her NFT and then sells the original NFT separately, two people would claim ownership. Custodial wrapping locks the NFT, ensuring the IPAsset owner has exclusive control.
Enables clean licensing. Only the IPAsset owner can create licenses. No risk of unauthorized licensing after selling the underlying NFT.
Atomic transfers. Selling the IPAsset transfers all rights in one transaction. No coordination needed.
Key Rules
- Only NFT owner can wrap
- One NFT can only be wrapped once
- Cannot unwrap with active licenses
- Cannot unwrap with active disputes
- Unwrapping burns the IPAsset and returns the original NFT
Functions
wrapNFT()
Wraps an external ERC-721 NFT into an IPAsset.
function wrapNFT(
address nftContract,
uint256 nftTokenId,
string memory metadataURI
) external returns (uint256 ipTokenId)
Requirements:
- Caller must own the NFT
- NFT not already wrapped
- Caller must approve IPAsset contract
unwrapNFT()
Burns the IPAsset and returns the original NFT.
function unwrapNFT(uint256 tokenId) external
Requirements:
- Caller must own the IPAsset
- No active licenses
- No active disputes
isWrapped()
Check if an IPAsset wraps an external NFT.
function isWrapped(uint256 tokenId) external view returns (bool)
getWrappedNFT()
Get details of the wrapped NFT.
function getWrappedNFT(uint256 tokenId)
external view
returns (address nftContract, uint256 nftTokenId)
Returns zero address if not wrapped.
Trade-offs
Advantages:
- No ownership conflicts
- Single source of truth
- Atomic transfers
- Clear legal ownership
Disadvantages:
- Contract holds valuable NFTs (custody risk)
- NFT cannot be used elsewhere while wrapped
- Must unwrap to exit (burns IPAsset)
Security
The IPAsset contract holds all wrapped NFTs. Security measures:
- Thorough audits required
- UUPS upgrade process with admin controls
- Reentrancy protection on wrap/unwrap
- Owner validation on all operations
Use Cases
Existing NFT collections: Wrap Bored Apes, CryptoPunks, or any ERC-721 NFT to license them through Softlaw Marketplace.
IP migration: Move existing IP-backed NFTs into the licensing system without creating new tokens.
Exit strategy: Unwrap to retrieve the original NFT and exit the licensing system.
Contents
IGovernanceArbitrator
Title: IGovernanceArbitrator
Interface for third-party dispute arbitration (no governance)
Manages license disputes with 30-day resolution deadline via designated arbitrators
Functions
initialize
Initializes the GovernanceArbitrator contract (proxy pattern)
Sets up admin roles and contract references
function initialize(address admin, address licenseToken, address ipAsset, address revenueDistributor) external;
Parameters
| Name | Type | Description |
|---|---|---|
admin | address | Address to receive admin role |
licenseToken | address | Address of LicenseToken contract |
ipAsset | address | Address of IPAsset contract |
revenueDistributor | address | Address of RevenueDistributor contract |
submitDispute
Submits a new dispute for a license
Can be submitted by any party (licensee, IP owner, third party)
function submitDispute(uint256 licenseId, string memory reason, string memory proofURI)
external
returns (uint256 disputeId);
Parameters
| Name | Type | Description |
|---|---|---|
licenseId | uint256 | The license being disputed |
reason | string | Human-readable dispute reason |
proofURI | string | URI pointing to evidence/documentation |
Returns
| Name | Type | Description |
|---|---|---|
disputeId | uint256 | Unique identifier for the dispute |
resolveDispute
Resolves a dispute
Only callable by ARBITRATOR_ROLE
function resolveDispute(uint256 disputeId, bool approved, string memory resolutionReason) external;
Parameters
| Name | Type | Description |
|---|---|---|
disputeId | uint256 | The dispute to resolve |
approved | bool | Whether to approve (true) or reject (false) the dispute |
resolutionReason | string | Explanation of the resolution |
executeRevocation
Executes license revocation for an approved dispute
Calls LicenseToken.revokeLicense() and updates dispute status
function executeRevocation(uint256 disputeId) external;
Parameters
| Name | Type | Description |
|---|---|---|
disputeId | uint256 | The approved dispute to execute |
getDispute
Gets full dispute information
function getDispute(uint256 disputeId) external view returns (Dispute memory dispute);
Parameters
| Name | Type | Description |
|---|---|---|
disputeId | uint256 | The dispute ID |
Returns
| Name | Type | Description |
|---|---|---|
dispute | Dispute | The complete dispute struct |
getDisputesForLicense
Gets all dispute IDs for a specific license
function getDisputesForLicense(uint256 licenseId) external view returns (uint256[] memory disputeIds);
Parameters
| Name | Type | Description |
|---|---|---|
licenseId | uint256 | The license ID |
Returns
| Name | Type | Description |
|---|---|---|
disputeIds | uint256[] | Array of dispute IDs |
isDisputeOverdue
Checks if a dispute is overdue (past 30-day deadline)
function isDisputeOverdue(uint256 disputeId) external view returns (bool overdue);
Parameters
| Name | Type | Description |
|---|---|---|
disputeId | uint256 | The dispute ID |
Returns
| Name | Type | Description |
|---|---|---|
overdue | bool | Whether the dispute is overdue |
getTimeRemaining
Gets time remaining for dispute resolution
function getTimeRemaining(uint256 disputeId) external view returns (uint256 timeRemaining);
Parameters
| Name | Type | Description |
|---|---|---|
disputeId | uint256 | The dispute ID |
Returns
| Name | Type | Description |
|---|---|---|
timeRemaining | uint256 | Seconds remaining (0 if overdue) |
getDisputeCount
Gets the total number of disputes submitted
function getDisputeCount() external view returns (uint256 count);
Returns
| Name | Type | Description |
|---|---|---|
count | uint256 | Total dispute count |
pause
Pauses dispute submissions
Only callable by DEFAULT_ADMIN_ROLE
function pause() external;
unpause
Unpauses dispute submissions
Only callable by DEFAULT_ADMIN_ROLE
function unpause() external;
Events
DisputeSubmitted
Emitted when a new dispute is submitted
event DisputeSubmitted(
uint256 indexed disputeId, uint256 indexed licenseId, address indexed submitter, string reason
);
Parameters
| Name | Type | Description |
|---|---|---|
disputeId | uint256 | Unique dispute identifier |
licenseId | uint256 | The license being disputed |
submitter | address | Address submitting the dispute |
reason | string | Dispute reason |
DisputeResolved
Emitted when a dispute is resolved
event DisputeResolved(uint256 indexed disputeId, bool approved, address indexed resolver, string reason);
Parameters
| Name | Type | Description |
|---|---|---|
disputeId | uint256 | The dispute that was resolved |
approved | bool | Whether dispute was approved (true) or rejected (false) |
resolver | address | Address that resolved the dispute |
reason | string | Resolution reasoning |
LicenseRevoked
Emitted when a license is revoked due to dispute
event LicenseRevoked(uint256 indexed licenseId, uint256 indexed disputeId);
Parameters
| Name | Type | Description |
|---|---|---|
licenseId | uint256 | The license that was revoked |
disputeId | uint256 | The dispute that caused revocation |
Errors
EmptyReason
Thrown when dispute reason is empty (BR-005.3)
error EmptyReason();
LicenseNotActive
Thrown when attempting to dispute an inactive license (BR-005.2)
error LicenseNotActive();
DisputeAlreadyResolved
Thrown when attempting to resolve an already resolved dispute
error DisputeAlreadyResolved();
DisputeResolutionOverdue
Thrown when attempting to resolve a dispute after 30-day deadline (BR-005.8)
error DisputeResolutionOverdue();
DisputeNotApproved
Thrown when attempting to execute a dispute that is not approved
error DisputeNotApproved();
NotArbitrator
Thrown when caller does not have ARBITRATOR_ROLE
error NotArbitrator();
NotAuthorizedToDispute
Thrown when caller is neither IP owner nor licensee
error NotAuthorizedToDispute();
Structs
Dispute
Complete dispute information
struct Dispute {
uint256 licenseId;
address submitter;
address ipOwner;
string reason;
string proofURI;
DisputeStatus status;
uint256 submittedAt;
uint256 resolvedAt;
address resolver;
string resolutionReason;
}
Properties
| Name | Type | Description |
|---|---|---|
licenseId | uint256 | The license being disputed |
submitter | address | Address that submitted the dispute (BR-005.1: any party) |
ipOwner | address | IP asset owner (cached from license data) |
reason | string | Human-readable dispute reason (BR-005.3: required) |
proofURI | string | Optional URI to supporting evidence (BR-005.1: optional) |
status | DisputeStatus | Current dispute status |
submittedAt | uint256 | Timestamp when dispute was submitted |
resolvedAt | uint256 | Timestamp when dispute was resolved (0 if pending) |
resolver | address | Address of arbitrator who resolved the dispute |
resolutionReason | string | Human-readable resolution explanation |
Enums
DisputeStatus
Possible states of a dispute
enum DisputeStatus {
Pending,
Approved,
Rejected,
Executed
}
Variants
| Name | Description |
|---|---|
Pending | Dispute submitted, awaiting resolution |
Approved | Dispute resolved in favor of submitter |
Rejected | Dispute resolved against submitter |
Executed | Approved dispute has been executed (license revoked) |
IIPAsset
Title: IIPAsset
Interface for IP Asset NFT contract representing intellectual property ownership
ERC-721 upgradeable contract with metadata versioning and license management
Functions
initialize
Initializes the IPAsset contract (proxy pattern)
Sets up ERC721, AccessControl, Pausable, and UUPS upgradeable patterns
function initialize(
string memory name,
string memory symbol,
address admin,
address licenseToken,
address arbitrator
) external;
Parameters
| Name | Type | Description |
|---|---|---|
name | string | The name for the ERC721 token |
symbol | string | The symbol for the ERC721 token |
admin | address | Address to receive all initial admin roles (DEFAULT_ADMIN, PAUSER, UPGRADER) |
licenseToken | address | Address of the LicenseToken contract |
arbitrator | address | Address of the GovernanceArbitrator contract |
mintIP
Mints a new IP asset NFT
Creates a token with auto-incrementing ID and stores initial metadata
function mintIP(address to, string memory metadataURI) external returns (uint256 tokenId);
Parameters
| Name | Type | Description |
|---|---|---|
to | address | Address to receive the newly minted IP asset |
metadataURI | string | IPFS or HTTP URI pointing to IP metadata |
Returns
| Name | Type | Description |
|---|---|---|
tokenId | uint256 | The ID of the newly minted token |
mintLicense
Creates a new license for an IP asset
Delegates to LicenseToken contract to mint the license. Only the IP asset owner can mint licenses. Emits LicenseRegistered event for off-chain tracking.
function mintLicense(
uint256 ipTokenId,
address licensee,
uint256 supply,
string memory publicMetadataURI,
string memory privateMetadataURI,
uint256 expiryTime,
string memory terms,
bool isExclusive,
uint256 paymentInterval
) external returns (uint256 licenseId);
Parameters
| Name | Type | Description |
|---|---|---|
ipTokenId | uint256 | The IP asset to create a license for |
licensee | address | Address to receive the license |
supply | uint256 | Number of license tokens to mint (ERC-1155 supply) |
publicMetadataURI | string | Publicly visible license metadata URI |
privateMetadataURI | string | Private license terms URI (access controlled) |
expiryTime | uint256 | Unix timestamp when license expires |
terms | string | Human-readable license terms |
isExclusive | bool | Whether this is an exclusive license |
paymentInterval | uint256 | Payment interval in seconds (0 = one-time, >0 = recurring) |
Returns
| Name | Type | Description |
|---|---|---|
licenseId | uint256 | The ID of the newly created license |
updateMetadata
Updates the metadata URI for an IP asset
Only the token owner can update. Creates a new version in history.
function updateMetadata(uint256 tokenId, string memory newURI) external;
Parameters
| Name | Type | Description |
|---|---|---|
tokenId | uint256 | The IP asset token ID |
newURI | string | The new metadata URI |
configureRevenueSplit
Configures revenue split for an IP asset
Only the token owner can configure. Delegates to RevenueDistributor.
function configureRevenueSplit(uint256 tokenId, address[] memory recipients, uint256[] memory shares) external;
Parameters
| Name | Type | Description |
|---|---|---|
tokenId | uint256 | The IP asset token ID |
recipients | address[] | Array of addresses to receive revenue shares |
shares | uint256[] | Array of share amounts in basis points (must sum to 10000) |
setRoyaltyRate
Sets the royalty rate for an IP asset
Only the token owner can set. Delegates to RevenueDistributor.
function setRoyaltyRate(uint256 tokenId, uint256 basisPoints) external;
Parameters
| Name | Type | Description |
|---|---|---|
tokenId | uint256 | The IP asset token ID |
basisPoints | uint256 | Royalty rate in basis points (e.g., 1000 = 10%) |
burn
Burns an IP asset NFT
Only owner can burn. Blocked if active licenses exist or dispute is active.
function burn(uint256 tokenId) external;
Parameters
| Name | Type | Description |
|---|---|---|
tokenId | uint256 | The IP asset token ID to burn |
setDisputeStatus
Sets the dispute status for an IP asset
Only callable by ARBITRATOR_ROLE (GovernanceArbitrator contract)
function setDisputeStatus(uint256 tokenId, bool hasDispute) external;
Parameters
| Name | Type | Description |
|---|---|---|
tokenId | uint256 | The IP asset token ID |
hasDispute | bool | Whether there is an active dispute |
setLicenseTokenContract
Updates the LicenseToken contract address
Only callable by admin
function setLicenseTokenContract(address licenseToken) external;
Parameters
| Name | Type | Description |
|---|---|---|
licenseToken | address | New LicenseToken contract address |
setArbitratorContract
Updates the GovernanceArbitrator contract address
Only callable by admin
function setArbitratorContract(address arbitrator) external;
Parameters
| Name | Type | Description |
|---|---|---|
arbitrator | address | New GovernanceArbitrator contract address |
setRevenueDistributorContract
Updates the RevenueDistributor contract address
Only callable by admin
function setRevenueDistributorContract(address distributor) external;
Parameters
| Name | Type | Description |
|---|---|---|
distributor | address | New RevenueDistributor contract address |
updateActiveLicenseCount
Updates the active license count for an IP asset
Only callable by LICENSE_MANAGER_ROLE (LicenseToken contract)
function updateActiveLicenseCount(uint256 tokenId, int256 delta) external;
Parameters
| Name | Type | Description |
|---|---|---|
tokenId | uint256 | The IP asset token ID |
delta | int256 | Change in license count (positive or negative) |
hasActiveDispute
Checks if an IP asset has an active dispute
function hasActiveDispute(uint256 tokenId) external view returns (bool hasDispute);
Parameters
| Name | Type | Description |
|---|---|---|
tokenId | uint256 | The IP asset token ID |
Returns
| Name | Type | Description |
|---|---|---|
hasDispute | bool | Whether there is an active dispute |
pause
Pauses all state-changing operations
Only callable by DEFAULT_ADMIN_ROLE
function pause() external;
unpause
Unpauses all state-changing operations
Only callable by DEFAULT_ADMIN_ROLE
function unpause() external;
setPrivateMetadata
Sets private metadata for an IP asset
Only the IP asset owner can set private metadata
function setPrivateMetadata(uint256 tokenId, string memory metadata) external;
Parameters
| Name | Type | Description |
|---|---|---|
tokenId | uint256 | The ID of the IP asset |
metadata | string | The private metadata URI (IPFS, HTTP, etc.) |
getPrivateMetadata
Gets private metadata for an IP asset
Only the IP asset owner can read private metadata
function getPrivateMetadata(uint256 tokenId) external view returns (string memory);
Parameters
| Name | Type | Description |
|---|---|---|
tokenId | uint256 | The ID of the IP asset |
Returns
| Name | Type | Description |
|---|---|---|
<none> | string | metadata The private metadata URI |
wrapNFT
Wraps an external NFT into an IPAsset
Transfers the NFT to this contract and mints a new IPAsset token representing it. Only the NFT owner can wrap it. One NFT can only be wrapped once. Use setPrivateMetadata() after wrapping to add private metadata if needed.
function wrapNFT(address nftContract, uint256 nftTokenId, string memory metadataURI)
external
returns (uint256 ipTokenId);
Parameters
| Name | Type | Description |
|---|---|---|
nftContract | address | The address of the ERC721 contract |
nftTokenId | uint256 | The token ID of the NFT to wrap |
metadataURI | string | The metadata URI for the new IPAsset |
Returns
| Name | Type | Description |
|---|---|---|
ipTokenId | uint256 | The ID of the newly minted IPAsset token |
unwrapNFT
Unwraps an IPAsset to retrieve the original NFT
Burns the IPAsset token and returns the original NFT to the caller. Only the IPAsset owner can unwrap. Cannot unwrap if active licenses or disputes exist.
function unwrapNFT(uint256 tokenId) external;
Parameters
| Name | Type | Description |
|---|---|---|
tokenId | uint256 | The IPAsset token ID to unwrap |
isWrapped
Checks if an IPAsset is wrapping an external NFT
function isWrapped(uint256 tokenId) external view returns (bool);
Parameters
| Name | Type | Description |
|---|---|---|
tokenId | uint256 | The IPAsset token ID |
Returns
| Name | Type | Description |
|---|---|---|
<none> | bool | True if the IPAsset wraps an NFT, false if it's a native IPAsset |
getWrappedNFT
Gets the wrapped NFT details for an IPAsset
function getWrappedNFT(uint256 tokenId) external view returns (address nftContract, uint256 nftTokenId);
Parameters
| Name | Type | Description |
|---|---|---|
tokenId | uint256 | The IPAsset token ID |
Returns
| Name | Type | Description |
|---|---|---|
nftContract | address | The wrapped NFT contract address (zero address if not wrapped) |
nftTokenId | uint256 | The wrapped NFT token ID (zero if not wrapped) |
Events
IPMinted
Emitted when a new IP asset is minted
event IPMinted(uint256 indexed tokenId, address indexed owner, string metadataURI);
Parameters
| Name | Type | Description |
|---|---|---|
tokenId | uint256 | The ID of the newly minted token |
owner | address | The address that owns the new IP asset |
metadataURI | string | The URI pointing to the IP metadata |
MetadataUpdated
Emitted when IP metadata is updated
Includes old and new URIs for complete off-chain indexing without state tracking
event MetadataUpdated(uint256 indexed tokenId, string oldURI, string newURI, uint256 timestamp);
Parameters
| Name | Type | Description |
|---|---|---|
tokenId | uint256 | The ID of the token being updated |
oldURI | string | The previous metadata URI |
newURI | string | The new metadata URI |
timestamp | uint256 | The block timestamp when update occurred |
LicenseMinted
Emitted when a license is minted for an IP asset
event LicenseMinted(uint256 indexed ipTokenId, uint256 indexed licenseId);
Parameters
| Name | Type | Description |
|---|---|---|
ipTokenId | uint256 | The IP asset token ID |
licenseId | uint256 | The newly created license ID |
LicenseRegistered
Emitted when a license is registered for an IP asset
This event provides complete license context for off-chain indexing without requiring array storage. Indexers can build complete license lists by filtering this event by ipTokenId. This replaces the need for on-chain ipToLicenses[] array storage (gas optimization).
event LicenseRegistered(
uint256 indexed ipTokenId, uint256 indexed licenseId, address indexed licensee, uint256 supply, bool isExclusive
);
Parameters
| Name | Type | Description |
|---|---|---|
ipTokenId | uint256 | The IP asset token ID this license is for |
licenseId | uint256 | The ID of the newly registered license |
licensee | address | The address receiving the license |
supply | uint256 | Number of license tokens minted (ERC-1155 supply) |
isExclusive | bool | Whether this is an exclusive license |
RevenueSplitConfigured
Emitted when revenue split is configured for an IP asset
event RevenueSplitConfigured(uint256 indexed tokenId, address[] recipients, uint256[] shares);
Parameters
| Name | Type | Description |
|---|---|---|
tokenId | uint256 | The IP asset token ID |
recipients | address[] | Array of recipient addresses |
shares | uint256[] | Array of share percentages (must sum to 10000 basis points) |
RoyaltyRateSet
Emitted when royalty rate is set for an IP asset
event RoyaltyRateSet(uint256 indexed tokenId, uint256 basisPoints);
Parameters
| Name | Type | Description |
|---|---|---|
tokenId | uint256 | The IP asset token ID |
basisPoints | uint256 | Royalty rate in basis points (e.g., 1000 = 10%) |
DisputeStatusChanged
Emitted when an IP asset's dispute status changes
event DisputeStatusChanged(uint256 indexed tokenId, bool hasDispute);
Parameters
| Name | Type | Description |
|---|---|---|
tokenId | uint256 | The IP asset token ID |
hasDispute | bool | Whether the asset now has an active dispute |
LicenseTokenContractSet
Emitted when the LicenseToken contract address is updated
event LicenseTokenContractSet(address indexed newContract);
Parameters
| Name | Type | Description |
|---|---|---|
newContract | address | The new LicenseToken contract address |
ArbitratorContractSet
Emitted when the GovernanceArbitrator contract address is updated
event ArbitratorContractSet(address indexed newContract);
Parameters
| Name | Type | Description |
|---|---|---|
newContract | address | The new GovernanceArbitrator contract address |
RevenueDistributorSet
Emitted when the RevenueDistributor contract address is updated
event RevenueDistributorSet(address indexed newContract);
Parameters
| Name | Type | Description |
|---|---|---|
newContract | address | The new RevenueDistributor contract address |
PrivateMetadataUpdated
Emitted when private metadata is updated for an IP asset
The metadata content is not included in the event for privacy
event PrivateMetadataUpdated(uint256 indexed tokenId);
Parameters
| Name | Type | Description |
|---|---|---|
tokenId | uint256 | The ID of the IP asset |
NFTWrapped
Emitted when an NFT is wrapped into an IPAsset
event NFTWrapped(
uint256 indexed ipTokenId, address indexed nftContract, uint256 indexed nftTokenId, address wrapper
);
Parameters
| Name | Type | Description |
|---|---|---|
ipTokenId | uint256 | The newly created IPAsset ID |
nftContract | address | The wrapped NFT contract address |
nftTokenId | uint256 | The wrapped NFT token ID |
wrapper | address | The address that wrapped the NFT |
NFTUnwrapped
Emitted when an IPAsset is unwrapped to retrieve the original NFT
event NFTUnwrapped(
uint256 indexed ipTokenId, address indexed nftContract, uint256 indexed nftTokenId, address owner
);
Parameters
| Name | Type | Description |
|---|---|---|
ipTokenId | uint256 | The burned IPAsset ID |
nftContract | address | The returned NFT contract address |
nftTokenId | uint256 | The returned NFT token ID |
owner | address | The address that received the NFT |
Errors
InvalidAddress
Thrown when attempting to mint to zero address
error InvalidAddress();
InvalidContractAddress
Thrown when setting a contract address to zero address
error InvalidContractAddress(address contractAddress);
Parameters
| Name | Type | Description |
|---|---|---|
contractAddress | address | The invalid address that was attempted |
EmptyMetadata
Thrown when metadata URI is empty
error EmptyMetadata();
NotTokenOwner
Thrown when caller is not the token owner
error NotTokenOwner();
HasActiveLicenses
Thrown when attempting to burn a token with active licenses
error HasActiveLicenses(uint256 tokenId, uint256 count);
Parameters
| Name | Type | Description |
|---|---|---|
tokenId | uint256 | The IP asset token ID |
count | uint256 | Number of active licenses preventing the burn |
HasActiveDispute
Thrown when attempting to burn a token with an active dispute
error HasActiveDispute(uint256 tokenId);
Parameters
| Name | Type | Description |
|---|---|---|
tokenId | uint256 | The IP asset token ID |
LicenseCountUnderflow
Thrown when attempting to decrement license count below zero
error LicenseCountUnderflow(uint256 tokenId, uint256 current, uint256 attempted);
Parameters
| Name | Type | Description |
|---|---|---|
tokenId | uint256 | The IP asset token ID |
current | uint256 | Current license count |
attempted | uint256 | Amount attempting to decrement |
NotWrappedNFT
Thrown when attempting to unwrap a non-wrapped IPAsset
error NotWrappedNFT();
NFTAlreadyWrapped
Thrown when attempting to wrap an NFT that's already wrapped
error NFTAlreadyWrapped(uint256 existingIPAssetId);
Parameters
| Name | Type | Description |
|---|---|---|
existingIPAssetId | uint256 | The IPAsset that already wraps this NFT |
NFTNotOwned
Thrown when caller doesn't own the NFT being wrapped
error NFTNotOwned(address nftContract, uint256 nftTokenId, address caller);
Parameters
| Name | Type | Description |
|---|---|---|
nftContract | address | The NFT contract address |
nftTokenId | uint256 | The NFT token ID |
caller | address | The address attempting to wrap |
Structs
WrappedNFT
Represents a wrapped NFT within an IPAsset
struct WrappedNFT {
address nftContract;
uint256 nftTokenId;
}
Properties
| Name | Type | Description |
|---|---|---|
nftContract | address | The address of the wrapped ERC721 contract |
nftTokenId | uint256 | The token ID of the wrapped NFT |
ILicenseToken
Title: ILicenseToken
Interface for License Token contract (ERC-1155 semi-fungible tokens)
Manages licenses for IP assets with expiry and revocation
Payment tracking is handled by Marketplace contract
Functions
initialize
Initializes the LicenseToken contract (proxy pattern)
Sets up ERC1155, AccessControl, and contract references
Grants DEFAULT_ADMIN_ROLE, ARBITRATOR_ROLE, and IP_ASSET_ROLE
Can only be called once due to initializer modifier
function initialize(string memory baseURI, address admin, address ipAsset, address arbitrator) external;
Parameters
| Name | Type | Description |
|---|---|---|
baseURI | string | Base URI for token metadata |
admin | address | Address to receive all initial admin roles |
ipAsset | address | Address of the IPAsset contract (granted IP_ASSET_ROLE) |
arbitrator | address | Address of the GovernanceArbitrator contract (granted ARBITRATOR_ROLE) |
mintLicense
Mints a new license token
Only callable by IP_ASSET_ROLE through IPAsset contract
Validates IP asset exists via hasActiveDispute() call
Exclusive licenses must have supply = 1 and only one can exist per IP asset
If maxMissedPayments = 0, defaults to DEFAULT_MAX_MISSED_PAYMENTS (3)
If penaltyRateBPS = 0, defaults to DEFAULT_PENALTY_RATE (500)
penaltyRateBPS must be <= MAX_PENALTY_RATE (5000)
Updates IP asset active license count
function mintLicense(
address to,
uint256 ipAssetId,
uint256 supply,
string memory publicMetadataURI,
string memory privateMetadataURI,
uint256 expiryTime,
string memory terms,
bool isExclusive,
uint256 paymentInterval,
uint8 maxMissedPayments,
uint16 penaltyRateBPS
) external returns (uint256 licenseId);
Parameters
| Name | Type | Description |
|---|---|---|
to | address | Address to receive the license |
ipAssetId | uint256 | The IP asset to license |
supply | uint256 | Number of license tokens to mint (must be 1 for exclusive licenses) |
publicMetadataURI | string | Publicly accessible metadata |
privateMetadataURI | string | Private metadata (access controlled) |
expiryTime | uint256 | Unix timestamp when license expires (0 = perpetual) |
terms | string | Human-readable license terms |
isExclusive | bool | Whether this is an exclusive license |
paymentInterval | uint256 | Payment interval in seconds (0 = ONE_TIME, >0 = RECURRENT) |
maxMissedPayments | uint8 | Maximum missed payments before auto-revocation (0 = use DEFAULT_MAX_MISSED_PAYMENTS) |
penaltyRateBPS | uint16 | Penalty rate in basis points per month (0 = use DEFAULT_PENALTY_RATE, max = MAX_PENALTY_RATE) |
Returns
| Name | Type | Description |
|---|---|---|
licenseId | uint256 | The ID of the newly minted license |
markExpired
Marks a license as expired
Can be called by anyone once expiry time has passed
Perpetual licenses (expiryTime = 0) cannot be expired
Updates IP asset active license count
function markExpired(uint256 licenseId) external;
Parameters
| Name | Type | Description |
|---|---|---|
licenseId | uint256 | The license to mark as expired |
batchMarkExpired
Marks multiple licenses as expired in a single transaction
Continues on error - does not revert entire batch if individual license fails
function batchMarkExpired(uint256[] memory licenseIds) external;
Parameters
| Name | Type | Description |
|---|---|---|
licenseIds | uint256[] | Array of license IDs to mark as expired |
revokeLicense
Revokes a license
Only callable by ARBITRATOR_ROLE (dispute resolution)
Clears exclusive license flag if applicable
Updates IP asset active license count
function revokeLicense(uint256 licenseId, string memory reason) external;
Parameters
| Name | Type | Description |
|---|---|---|
licenseId | uint256 | The license to revoke |
reason | string | Human-readable revocation reason |
revokeForMissedPayments
Revokes a license for missed payments
Anyone can call this function, but it will only succeed if missedCount >= maxMissedPayments
Payment tracking is handled by Marketplace contract
Spam prevention: built-in validation requires missedCount to meet threshold
Clears exclusive license flag if applicable
Updates IP asset active license count
function revokeForMissedPayments(uint256 licenseId, uint256 missedCount) external;
Parameters
| Name | Type | Description |
|---|---|---|
licenseId | uint256 | The license to revoke |
missedCount | uint256 | Number of missed payments (must meet maxMissedPayments threshold) |
getPublicMetadata
Gets the public metadata URI for a license
function getPublicMetadata(uint256 licenseId) external view returns (string memory uri);
Parameters
| Name | Type | Description |
|---|---|---|
licenseId | uint256 | The license ID |
Returns
| Name | Type | Description |
|---|---|---|
uri | string | The public metadata URI |
getPrivateMetadata
Gets the private metadata URI for a license
Access controlled - only license holder, granted accounts, and admin
function getPrivateMetadata(uint256 licenseId) external view returns (string memory uri);
Parameters
| Name | Type | Description |
|---|---|---|
licenseId | uint256 | The license ID |
Returns
| Name | Type | Description |
|---|---|---|
uri | string | The private metadata URI |
grantPrivateAccess
Grants access to private metadata for an account
Only license holder can grant access
function grantPrivateAccess(uint256 licenseId, address account) external;
Parameters
| Name | Type | Description |
|---|---|---|
licenseId | uint256 | The license ID |
account | address | The account to grant access to |
revokePrivateAccess
Revokes private metadata access from an account
Only license holder can revoke access
function revokePrivateAccess(uint256 licenseId, address account) external;
Parameters
| Name | Type | Description |
|---|---|---|
licenseId | uint256 | The license ID |
account | address | The account to revoke access from |
hasPrivateAccess
Checks if an account has been granted private metadata access
function hasPrivateAccess(uint256 licenseId, address account) external view returns (bool hasAccess);
Parameters
| Name | Type | Description |
|---|---|---|
licenseId | uint256 | The license ID |
account | address | The account to check |
Returns
| Name | Type | Description |
|---|---|---|
hasAccess | bool | Whether the account has been granted access |
isRevoked
Checks if a license is revoked
function isRevoked(uint256 licenseId) external view returns (bool revoked);
Parameters
| Name | Type | Description |
|---|---|---|
licenseId | uint256 | The license ID |
Returns
| Name | Type | Description |
|---|---|---|
revoked | bool | Whether the license is revoked |
isExpired
Checks if a license is expired
function isExpired(uint256 licenseId) external view returns (bool expired);
Parameters
| Name | Type | Description |
|---|---|---|
licenseId | uint256 | The license ID |
Returns
| Name | Type | Description |
|---|---|---|
expired | bool | Whether the license is expired |
setArbitratorContract
Updates the GovernanceArbitrator contract address
Only callable by DEFAULT_ADMIN_ROLE
Revokes ARBITRATOR_ROLE from old address and grants to new address
function setArbitratorContract(address arbitrator) external;
Parameters
| Name | Type | Description |
|---|---|---|
arbitrator | address | New arbitrator contract address (cannot be zero address) |
setIPAssetContract
Updates the IPAsset contract address
Only callable by DEFAULT_ADMIN_ROLE
Revokes IP_ASSET_ROLE from old address and grants to new address
function setIPAssetContract(address ipAsset) external;
Parameters
| Name | Type | Description |
|---|---|---|
ipAsset | address | New IP asset contract address (cannot be zero address) |
grantRole
Grants a role to an account
Only callable by role admin
function grantRole(bytes32 role, address account) external;
Parameters
| Name | Type | Description |
|---|---|---|
role | bytes32 | The role identifier |
account | address | The account to grant the role to |
supportsInterface
Checks if contract supports a given interface
function supportsInterface(bytes4 interfaceId) external view returns (bool supported);
Parameters
| Name | Type | Description |
|---|---|---|
interfaceId | bytes4 | The interface identifier (ERC-165) |
Returns
| Name | Type | Description |
|---|---|---|
supported | bool | Whether the interface is supported |
getPaymentInterval
Gets the payment interval for a license
function getPaymentInterval(uint256 licenseId) external view returns (uint256 interval);
Parameters
| Name | Type | Description |
|---|---|---|
licenseId | uint256 | The license ID |
Returns
| Name | Type | Description |
|---|---|---|
interval | uint256 | Payment interval in seconds (0 = ONE_TIME, >0 = RECURRENT) |
isRecurring
Checks if a license has recurring payments
function isRecurring(uint256 licenseId) external view returns (bool recurring);
Parameters
| Name | Type | Description |
|---|---|---|
licenseId | uint256 | The license ID |
Returns
| Name | Type | Description |
|---|---|---|
recurring | bool | True if payment interval > 0 |
isOneTime
Checks if a license is one-time payment
function isOneTime(uint256 licenseId) external view returns (bool oneTime);
Parameters
| Name | Type | Description |
|---|---|---|
licenseId | uint256 | The license ID |
Returns
| Name | Type | Description |
|---|---|---|
oneTime | bool | True if payment interval == 0 |
getLicenseInfo
Gets comprehensive license information
function getLicenseInfo(uint256 licenseId)
external
view
returns (
uint256 ipAssetId,
uint256 supply,
uint256 expiryTime,
string memory terms,
uint256 paymentInterval,
bool isExclusive,
bool revokedStatus,
bool expiredStatus
);
Parameters
| Name | Type | Description |
|---|---|---|
licenseId | uint256 | The license ID |
Returns
| Name | Type | Description |
|---|---|---|
ipAssetId | uint256 | The IP asset this license is for |
supply | uint256 | Number of license tokens minted |
expiryTime | uint256 | Unix timestamp when license expires |
terms | string | Human-readable license terms |
paymentInterval | uint256 | Payment interval in seconds |
isExclusive | bool | Whether this is an exclusive license |
revokedStatus | bool | Whether the license has been revoked |
expiredStatus | bool | Whether the license has expired |
isActiveLicense
Checks if a license is currently active
A license is active if it is neither revoked nor expired
function isActiveLicense(uint256 licenseId) external view returns (bool active);
Parameters
| Name | Type | Description |
|---|---|---|
licenseId | uint256 | The license ID |
Returns
| Name | Type | Description |
|---|---|---|
active | bool | True if license is not revoked and not expired |
getMaxMissedPayments
Gets the maximum number of missed payments allowed for a license
function getMaxMissedPayments(uint256 licenseId) external view returns (uint8 maxMissed);
Parameters
| Name | Type | Description |
|---|---|---|
licenseId | uint256 | The license ID |
Returns
| Name | Type | Description |
|---|---|---|
maxMissed | uint8 | Maximum number of missed payments before auto-revocation |
setPenaltyRate
Sets the penalty rate for a specific license
function setPenaltyRate(uint256 licenseId, uint16 penaltyRateBPS) external;
Parameters
| Name | Type | Description |
|---|---|---|
licenseId | uint256 | The license ID |
penaltyRateBPS | uint16 | Penalty rate in basis points (100 bps = 1% per month) |
getPenaltyRate
Gets the penalty rate for a license
function getPenaltyRate(uint256 licenseId) external view returns (uint16 penaltyRate);
Parameters
| Name | Type | Description |
|---|---|---|
licenseId | uint256 | The license ID |
Returns
| Name | Type | Description |
|---|---|---|
penaltyRate | uint16 | Penalty rate in basis points (100 bps = 1% per month) |
Events
LicenseCreated
Emitted when a new license is created
event LicenseCreated(
uint256 indexed licenseId,
uint256 indexed ipAssetId,
address indexed licensee,
bool isExclusive,
uint256 paymentInterval
);
Parameters
| Name | Type | Description |
|---|---|---|
licenseId | uint256 | The ID of the newly created license |
ipAssetId | uint256 | The IP asset this license is for |
licensee | address | The address receiving the license |
isExclusive | bool | Whether this is an exclusive license |
paymentInterval | uint256 | Payment interval in seconds (0 = ONE_TIME, >0 = RECURRENT) |
LicenseExpired
Emitted when a license expires
event LicenseExpired(uint256 indexed licenseId);
Parameters
| Name | Type | Description |
|---|---|---|
licenseId | uint256 | The license that expired |
LicenseRevoked
Emitted when a license is revoked
event LicenseRevoked(uint256 indexed licenseId, string reason);
Parameters
| Name | Type | Description |
|---|---|---|
licenseId | uint256 | The license that was revoked |
reason | string | Human-readable revocation reason |
AutoRevoked
Emitted when a license is automatically revoked for missed payments
event AutoRevoked(uint256 indexed licenseId, uint256 missedPayments);
Parameters
| Name | Type | Description |
|---|---|---|
licenseId | uint256 | The license that was auto-revoked |
missedPayments | uint256 | Number of missed payments that triggered revocation |
PenaltyRateUpdated
Emitted when a license's penalty rate is updated
event PenaltyRateUpdated(uint256 indexed licenseId, uint16 penaltyRateBPS);
Parameters
| Name | Type | Description |
|---|---|---|
licenseId | uint256 | The license ID |
penaltyRateBPS | uint16 | The new penalty rate in basis points |
PrivateAccessGranted
Emitted when private metadata access is granted
event PrivateAccessGranted(uint256 indexed licenseId, address indexed account);
Parameters
| Name | Type | Description |
|---|---|---|
licenseId | uint256 | The license ID |
account | address | The account granted access |
PrivateAccessRevoked
Emitted when private metadata access is revoked
event PrivateAccessRevoked(uint256 indexed licenseId, address indexed account);
Parameters
| Name | Type | Description |
|---|---|---|
licenseId | uint256 | The license ID |
account | address | The account whose access was revoked |
ArbitratorContractUpdated
Emitted when the arbitrator contract is updated
event ArbitratorContractUpdated(address indexed oldArbitrator, address indexed newArbitrator);
Parameters
| Name | Type | Description |
|---|---|---|
oldArbitrator | address | The previous arbitrator contract address |
newArbitrator | address | The new arbitrator contract address |
IPAssetContractUpdated
Emitted when the IP asset contract is updated
event IPAssetContractUpdated(address indexed oldIPAsset, address indexed newIPAsset);
Parameters
| Name | Type | Description |
|---|---|---|
oldIPAsset | address | The previous IP asset contract address |
newIPAsset | address | The new IP asset contract address |
Errors
InvalidIPAsset
Thrown when attempting to create license for invalid IP asset
error InvalidIPAsset();
InvalidSupply
Thrown when license supply is invalid (e.g., zero)
error InvalidSupply();
ExclusiveLicenseMustHaveSupplyOne
Thrown when exclusive license does not have supply of exactly 1
error ExclusiveLicenseMustHaveSupplyOne();
ExclusiveLicenseAlreadyExists
Thrown when attempting to create multiple exclusive licenses for same IP
error ExclusiveLicenseAlreadyExists();
LicenseIsPerpetual
Thrown when attempting to expire a perpetual license
error LicenseIsPerpetual();
LicenseNotYetExpired
Thrown when attempting to mark a license as expired before expiry time
error LicenseNotYetExpired();
AlreadyMarkedExpired
Thrown when attempting to mark an already expired license as expired
error AlreadyMarkedExpired();
AlreadyRevoked
Thrown when attempting to revoke an already revoked license
error AlreadyRevoked();
NotAuthorizedForPrivateMetadata
Thrown when unauthorized access to private metadata is attempted
error NotAuthorizedForPrivateMetadata();
NotLicenseOwner
Thrown when non-license owner attempts owner-only operation
error NotLicenseOwner();
NotIPOwner
Thrown when non-IP owner attempts IP-owner-only operation
error NotIPOwner();
InsufficientMissedPayments
Thrown when insufficient missed payments for auto-revocation
error InsufficientMissedPayments();
CannotTransferExpiredLicense
Thrown when attempting to transfer an expired license
error CannotTransferExpiredLicense();
CannotTransferRevokedLicense
Thrown when attempting to transfer a revoked license
error CannotTransferRevokedLicense();
InvalidArbitratorAddress
Thrown when attempting to set arbitrator to zero address
error InvalidArbitratorAddress();
InvalidIPAssetAddress
Thrown when attempting to set IP asset contract to zero address
error InvalidIPAssetAddress();
InvalidMaxMissedPayments
Thrown when maxMissedPayments is zero or exceeds allowed maximum
error InvalidMaxMissedPayments();
InvalidPenaltyRate
Thrown when penalty rate exceeds maximum allowed rate
error InvalidPenaltyRate();
Structs
License
License configuration and state
struct License {
uint256 ipAssetId;
uint256 supply;
uint256 expiryTime;
string terms;
bool isExclusive;
bool isRevoked;
string publicMetadataURI;
string privateMetadataURI;
uint256 paymentInterval;
uint8 maxMissedPayments;
uint16 penaltyRateBPS;
}
Properties
| Name | Type | Description |
|---|---|---|
ipAssetId | uint256 | The IP asset this license is for |
supply | uint256 | Number of license tokens minted (ERC-1155 supply) |
expiryTime | uint256 | Unix timestamp when license expires (0 = perpetual, never expires) |
terms | string | Human-readable license terms |
isExclusive | bool | Whether this is an exclusive license |
isRevoked | bool | Whether the license has been revoked |
publicMetadataURI | string | Publicly accessible metadata URI |
privateMetadataURI | string | Private metadata URI (access controlled) |
paymentInterval | uint256 | Payment interval in seconds (0 = ONE_TIME, >0 = RECURRENT) |
maxMissedPayments | uint8 | Maximum number of missed payments before auto-revocation (1-255, 0 defaults to 3) |
penaltyRateBPS | uint16 | Penalty rate in basis points (100 bps = 1% per month, 0 defaults to 500, max 5000 = 50%) |
IMarketplace
Title: IMarketplace
Interface for NFT marketplace with listings and offers
Supports both ERC-721 and ERC-1155 tokens with platform fees and royalties
Functions
initialize
Initializes the Marketplace contract (proxy pattern)
Sets up admin roles. Platform fees are managed by RevenueDistributor.
function initialize(address admin, address revenueDistributor) external;
Parameters
| Name | Type | Description |
|---|---|---|
admin | address | Address to receive admin role |
revenueDistributor | address | Address of RevenueDistributor contract |
createListing
Creates a new NFT listing
Seller must approve marketplace contract before listing
function createListing(address nftContract, uint256 tokenId, uint256 price, bool isERC721)
external
returns (bytes32 listingId);
Parameters
| Name | Type | Description |
|---|---|---|
nftContract | address | Address of NFT contract |
tokenId | uint256 | Token ID to list |
price | uint256 | Listing price in wei |
isERC721 | bool | Whether the NFT is ERC-721 (true) or ERC-1155 (false) |
Returns
| Name | Type | Description |
|---|---|---|
listingId | bytes32 | Unique identifier for the listing |
cancelListing
Cancels an active listing
Only seller can cancel their own listing
function cancelListing(bytes32 listingId) external;
Parameters
| Name | Type | Description |
|---|---|---|
listingId | bytes32 | The listing to cancel |
buyListing
Buys an NFT from a listing
Transfers NFT, distributes payment with fees/royalties
function buyListing(bytes32 listingId) external payable;
Parameters
| Name | Type | Description |
|---|---|---|
listingId | bytes32 | The listing to purchase |
createOffer
Creates an offer for an NFT
Offer price is held in escrow
function createOffer(address nftContract, uint256 tokenId, uint256 expiryTime)
external
payable
returns (bytes32 offerId);
Parameters
| Name | Type | Description |
|---|---|---|
nftContract | address | Address of NFT contract |
tokenId | uint256 | Token ID to make offer for |
expiryTime | uint256 | Unix timestamp when offer expires |
Returns
| Name | Type | Description |
|---|---|---|
offerId | bytes32 | Unique identifier for the offer |
acceptOffer
Accepts an offer for an NFT
Only NFT owner can accept. Transfers NFT and distributes payment.
function acceptOffer(bytes32 offerId) external;
Parameters
| Name | Type | Description |
|---|---|---|
offerId | bytes32 | The offer to accept |
cancelOffer
Cancels an offer and refunds escrowed funds
Only offer creator can cancel
function cancelOffer(bytes32 offerId) external;
Parameters
| Name | Type | Description |
|---|---|---|
offerId | bytes32 | The offer to cancel |
pause
Pauses all marketplace operations
Only callable by PAUSER_ROLE
function pause() external;
unpause
Unpauses all marketplace operations
Only callable by PAUSER_ROLE
function unpause() external;
setPenaltyRate
Sets the penalty rate for late recurring payments
Only callable by admin. Penalty is calculated pro-rata per second.
function setPenaltyRate(uint256 basisPoints) external;
Parameters
| Name | Type | Description |
|---|---|---|
basisPoints | uint256 | Penalty rate in basis points per month (e.g., 500 = 5% per month) |
getMissedPayments
Calculates the number of missed payments for a recurring license
Returns 0 for ONE_TIME licenses
function getMissedPayments(address licenseContract, uint256 licenseId)
external
view
returns (uint256 missedPayments);
Parameters
| Name | Type | Description |
|---|---|---|
licenseContract | address | Address of the license token contract |
licenseId | uint256 | The license ID to check |
Returns
| Name | Type | Description |
|---|---|---|
missedPayments | uint256 | Number of missed payment periods |
makeRecurringPayment
Makes a recurring payment for a subscription license
Calculates penalty for late payments, auto-revokes after 3 missed payments
function makeRecurringPayment(address licenseContract, uint256 licenseId) external payable;
Parameters
| Name | Type | Description |
|---|---|---|
licenseContract | address | Address of the license token contract |
licenseId | uint256 | The license ID to pay for |
getRecurringPaymentAmount
Gets the base amount for a recurring payment
function getRecurringPaymentAmount(uint256 licenseId) external view returns (uint256 baseAmount);
Parameters
| Name | Type | Description |
|---|---|---|
licenseId | uint256 | The license ID |
Returns
| Name | Type | Description |
|---|---|---|
baseAmount | uint256 | The base payment amount (without penalty) |
calculatePenalty
Calculates the current penalty for late payment
Returns 0 if payment is not overdue or for ONE_TIME licenses
No penalty if within PENALTY_GRACE_PERIOD (3 days) of dueDate
Penalties only start accruing after dueDate + PENALTY_GRACE_PERIOD
function calculatePenalty(address licenseContract, uint256 licenseId) external view returns (uint256 penalty);
Parameters
| Name | Type | Description |
|---|---|---|
licenseContract | address | Address of the license token contract |
licenseId | uint256 | The license ID |
Returns
| Name | Type | Description |
|---|---|---|
penalty | uint256 | Penalty amount in wei (0 if within grace period) |
getTotalPaymentDue
Gets the total amount due for next recurring payment (base + penalty)
Useful for frontends to know exact amount before creating transaction
function getTotalPaymentDue(address licenseContract, uint256 licenseId)
external
view
returns (uint256 baseAmount, uint256 penalty, uint256 total);
Parameters
| Name | Type | Description |
|---|---|---|
licenseContract | address | Address of the license token contract |
licenseId | uint256 | The license ID |
Returns
| Name | Type | Description |
|---|---|---|
baseAmount | uint256 | The base payment amount |
penalty | uint256 | The penalty amount if overdue |
total | uint256 | The total amount due (baseAmount + penalty) |
Events
ListingCreated
Emitted when a new listing is created
event ListingCreated(
bytes32 indexed listingId, address indexed seller, address nftContract, uint256 tokenId, uint256 price
);
Parameters
| Name | Type | Description |
|---|---|---|
listingId | bytes32 | Unique identifier for the listing |
seller | address | Address of the seller |
nftContract | address | NFT contract address |
tokenId | uint256 | Token ID being listed |
price | uint256 | Listing price |
ListingCancelled
Emitted when a listing is cancelled
event ListingCancelled(bytes32 indexed listingId);
Parameters
| Name | Type | Description |
|---|---|---|
listingId | bytes32 | The listing that was cancelled |
OfferCreated
Emitted when an offer is created
event OfferCreated(
bytes32 indexed offerId, address indexed buyer, address nftContract, uint256 tokenId, uint256 price
);
Parameters
| Name | Type | Description |
|---|---|---|
offerId | bytes32 | Unique identifier for the offer |
buyer | address | Address making the offer |
nftContract | address | NFT contract address |
tokenId | uint256 | Token ID for the offer |
price | uint256 | Offer price |
OfferAccepted
Emitted when an offer is accepted
event OfferAccepted(bytes32 indexed offerId, address indexed seller);
Parameters
| Name | Type | Description |
|---|---|---|
offerId | bytes32 | The offer that was accepted |
seller | address | Address of the seller who accepted |
OfferCancelled
Emitted when an offer is cancelled
event OfferCancelled(bytes32 indexed offerId);
Parameters
| Name | Type | Description |
|---|---|---|
offerId | bytes32 | The offer that was cancelled |
Sale
Emitted when a sale is completed
event Sale(bytes32 indexed saleId, address indexed buyer, address indexed seller, uint256 price);
Parameters
| Name | Type | Description |
|---|---|---|
saleId | bytes32 | Unique sale identifier |
buyer | address | Address of the buyer |
seller | address | Address of the seller |
price | uint256 | Total sale price |
RecurringPaymentMade
Emitted when a recurring payment is made
event RecurringPaymentMade(
uint256 indexed licenseId, address indexed payer, uint256 baseAmount, uint256 penalty, uint256 timestamp
);
Parameters
| Name | Type | Description |
|---|---|---|
licenseId | uint256 | The license ID for the recurring payment |
payer | address | Address making the payment |
baseAmount | uint256 | Base payment amount (without penalty) |
penalty | uint256 | Penalty amount for late payment |
timestamp | uint256 | Time of payment |
PenaltyRateUpdated
Emitted when penalty rate is updated
event PenaltyRateUpdated(uint256 newRate);
Parameters
| Name | Type | Description |
|---|---|---|
newRate | uint256 | New penalty rate in basis points per day |
Errors
InvalidPrice
Thrown when price is zero or invalid
error InvalidPrice();
NotTokenOwner
Thrown when caller is not the token owner
error NotTokenOwner();
NotSeller
Thrown when caller is not the seller
error NotSeller();
ListingNotActive
Thrown when listing is not active
error ListingNotActive();
InsufficientPayment
Thrown when payment amount is insufficient
error InsufficientPayment();
NotOfferBuyer
Thrown when caller is not the offer buyer
error NotOfferBuyer();
OfferNotActive
Thrown when offer is not active
error OfferNotActive();
OfferExpired
Thrown when offer has expired
error OfferExpired();
NotRecurringLicense
Thrown when operation requires recurring license but license is one-time
error NotRecurringLicense();
LicenseNotActive
Thrown when license is not active
error LicenseNotActive();
InsufficientMissedPaymentsForRevocation
Thrown when attempting revocation without sufficient missed payments
error InsufficientMissedPaymentsForRevocation();
LicenseRevokedForMissedPayments
Thrown when license has been revoked for missed payments
error LicenseRevokedForMissedPayments();
InvalidPenaltyRate
Thrown when penalty rate exceeds maximum allowed
error InvalidPenaltyRate();
TransferFailed
Thrown when native token transfer fails
error TransferFailed();
PaymentNotDueYet
Thrown when attempting to make a payment before it is due
error PaymentNotDueYet();
Structs
Listing
Marketplace listing configuration
struct Listing {
address seller;
address nftContract;
uint256 tokenId;
uint256 price;
bool isActive;
bool isERC721;
}
Properties
| Name | Type | Description |
|---|---|---|
seller | address | Address of the seller |
nftContract | address | Address of the NFT contract (ERC-721 or ERC-1155) |
tokenId | uint256 | Token ID being listed |
price | uint256 | Listing price in wei |
isActive | bool | Whether the listing is currently active |
isERC721 | bool | Whether the NFT is ERC-721 (true) or ERC-1155 (false) |
Offer
Offer configuration for NFT purchase
struct Offer {
address buyer;
address nftContract;
uint256 tokenId;
uint256 price;
bool isActive;
uint256 expiryTime;
}
Properties
| Name | Type | Description |
|---|---|---|
buyer | address | Address making the offer |
nftContract | address | Address of the NFT contract |
tokenId | uint256 | Token ID for the offer |
price | uint256 | Offer price in wei (held in escrow) |
isActive | bool | Whether the offer is currently active |
expiryTime | uint256 | Unix timestamp when offer expires |
RecurringPayment
Recurring payment tracking for subscription licenses
struct RecurringPayment {
uint256 lastPaymentTime;
address currentOwner;
uint256 baseAmount;
}
Properties
| Name | Type | Description |
|---|---|---|
lastPaymentTime | uint256 | Timestamp of the last payment made |
currentOwner | address | Current owner of the license (tracks transfers) |
baseAmount | uint256 | Base payment amount for recurring payments |
IRevenueDistributor
Title: IRevenueDistributor
Interface for simple revenue distribution to configured recipients
Non-upgradeable contract implementing EIP-2981 royalty standard
Pure distribution logic - payment timing and penalties handled by calling contracts (e.g., Marketplace)
Functions
configureSplit
Configures revenue split for an IP asset
Only callable by IP asset owner or CONFIGURATOR_ROLE
function configureSplit(uint256 ipAssetId, address[] memory recipients, uint256[] memory shares) external;
Parameters
| Name | Type | Description |
|---|---|---|
ipAssetId | uint256 | The IP asset ID |
recipients | address[] | Array of recipient addresses |
shares | uint256[] | Array of share amounts in basis points (must sum to 10000) |
distributePayment
Distributes a payment according to configured splits
Deducts platform fee then splits remainder among recipients
function distributePayment(uint256 ipAssetId, uint256 amount, address seller, bool isPrimarySale) external payable;
Parameters
| Name | Type | Description |
|---|---|---|
ipAssetId | uint256 | The IP asset ID |
amount | uint256 | Payment amount to distribute |
seller | address | Address of the seller (receives remainder for secondary sales) |
isPrimarySale | bool | True for primary sales (100% to split), false for secondary (royalty to split, remainder to seller) |
withdraw
Withdraws accumulated funds
All recipients (including platform treasury) use this function to withdraw
function withdraw() external;
getBalance
Gets the principal balance for a recipient
function getBalance(address recipient) external view returns (uint256 balance);
Parameters
| Name | Type | Description |
|---|---|---|
recipient | address | Address to query |
Returns
| Name | Type | Description |
|---|---|---|
balance | uint256 | Principal amount available for withdrawal |
setDefaultRoyalty
Sets the default royalty rate
Only callable by admin
function setDefaultRoyalty(uint256 basisPoints) external;
Parameters
| Name | Type | Description |
|---|---|---|
basisPoints | uint256 | Royalty rate in basis points |
setAssetRoyalty
Configure royalty rate for a specific IP asset
Only callable by CONFIGURATOR_ROLE
function setAssetRoyalty(uint256 ipAssetId, uint256 basisPoints) external;
Parameters
| Name | Type | Description |
|---|---|---|
ipAssetId | uint256 | The IP asset ID |
basisPoints | uint256 | Royalty rate in basis points (e.g., 1000 = 10%) |
getAssetRoyalty
Get royalty rate for an IP asset (custom or default)
function getAssetRoyalty(uint256 ipAssetId) external view returns (uint256);
Parameters
| Name | Type | Description |
|---|---|---|
ipAssetId | uint256 | The IP asset ID |
Returns
| Name | Type | Description |
|---|---|---|
<none> | uint256 | Royalty rate in basis points |
grantConfiguratorRole
Grants CONFIGURATOR_ROLE to the IPAsset contract
Only callable by admin. Should be called after IPAsset deployment.
function grantConfiguratorRole(address ipAssetContract) external;
Parameters
| Name | Type | Description |
|---|---|---|
ipAssetContract | address | Address of the IPAsset contract |
ipSplits
Gets the configured split for an IP asset
function ipSplits(uint256 ipAssetId) external view returns (address[] memory recipients, uint256[] memory shares);
Parameters
| Name | Type | Description |
|---|---|---|
ipAssetId | uint256 | The IP asset ID |
Returns
| Name | Type | Description |
|---|---|---|
recipients | address[] | Array of recipient addresses |
shares | uint256[] | Array of share amounts |
isSplitConfigured
Checks if a split is configured for an IP asset
function isSplitConfigured(uint256 ipAssetId) external view returns (bool configured);
Parameters
| Name | Type | Description |
|---|---|---|
ipAssetId | uint256 | The IP asset ID |
Returns
| Name | Type | Description |
|---|---|---|
configured | bool | True if split exists, false otherwise |
Events
PaymentDistributed
Emitted when a payment is distributed
event PaymentDistributed(uint256 indexed ipAssetId, uint256 amount, address indexed seller, bool isPrimarySale);
Parameters
| Name | Type | Description |
|---|---|---|
ipAssetId | uint256 | The IP asset the payment is for |
amount | uint256 | Total payment amount |
seller | address | Address of the seller |
isPrimarySale | bool | Whether this is a primary sale (seller is in split recipients) |
SplitConfigured
Emitted when a revenue split is configured
event SplitConfigured(uint256 indexed ipAssetId, address[] recipients, uint256[] shares);
Parameters
| Name | Type | Description |
|---|---|---|
ipAssetId | uint256 | The IP asset ID |
recipients | address[] | Array of recipient addresses |
shares | uint256[] | Array of share amounts |
Withdrawal
Emitted when a recipient withdraws funds
event Withdrawal(address indexed recipient, uint256 principal);
Parameters
| Name | Type | Description |
|---|---|---|
recipient | address | Address withdrawing |
principal | uint256 | Principal amount withdrawn |
RoyaltyUpdated
Emitted when default royalty rate is updated
event RoyaltyUpdated(uint256 newRoyaltyBasisPoints);
Parameters
| Name | Type | Description |
|---|---|---|
newRoyaltyBasisPoints | uint256 | New royalty rate in basis points |
AssetRoyaltyUpdated
Emitted when a per-asset royalty rate is updated
event AssetRoyaltyUpdated(uint256 indexed ipAssetId, uint256 basisPoints);
Parameters
| Name | Type | Description |
|---|---|---|
ipAssetId | uint256 | The IP asset ID |
basisPoints | uint256 | New royalty rate in basis points |
Errors
ArrayLengthMismatch
Thrown when array lengths don't match
error ArrayLengthMismatch();
NoRecipientsProvided
Thrown when no recipients are provided
error NoRecipientsProvided();
InvalidRecipient
Thrown when a recipient address is zero
error InvalidRecipient();
InvalidSharesSum
Thrown when shares don't sum to 10000 basis points
error InvalidSharesSum();
IncorrectPaymentAmount
Thrown when msg.value doesn't match amount parameter
error IncorrectPaymentAmount();
InvalidIPAsset
Thrown when IP asset does not exist
error InvalidIPAsset();
NoBalanceToWithdraw
Thrown when attempting to withdraw with zero balance
error NoBalanceToWithdraw();
TransferFailed
Thrown when ETH transfer fails during withdrawal
error TransferFailed();
InvalidTreasuryAddress
Thrown when treasury address is zero
error InvalidTreasuryAddress();
InvalidPlatformFee
Thrown when platform fee exceeds 100%
error InvalidPlatformFee();
InvalidRoyalty
Thrown when royalty rate exceeds 100%
error InvalidRoyalty();
InvalidIPAssetAddress
Thrown when IPAsset contract address is zero
error InvalidIPAssetAddress();
InvalidBasisPoints
Thrown when basis points exceeds 10000 (100%)
error InvalidBasisPoints();
InvalidRoyaltyRate
Thrown when royalty rate exceeds 100%
error InvalidRoyaltyRate();
Structs
Split
Revenue split configuration for an IP asset
struct Split {
address[] recipients;
uint256[] shares;
}
Properties
| Name | Type | Description |
|---|---|---|
recipients | address[] | Array of addresses to receive revenue shares |
shares | uint256[] | Array of share amounts in basis points (must sum to 10000) |