Softlaw Marketplace Contracts

Smart contracts for the Softlaw marketplace, built with Foundry for deployment on Polkadot's Asset Hub (Passet Hub TestNet).

Development Environment

This project includes a pre-configured DevContainer with all tools installed:

Requirements:

  • Docker
  • VS Code with Remote-Containers extension

Setup:

  1. Open project in VS Code
  2. Click "Reopen in Container" when prompted
  3. Add your PRIVATE_KEY to .env file
  4. Start developing - Foundry and all dependencies are pre-installed

The DevContainer automatically:

  • Installs Foundry and Solidity tools
  • Loads your .env file
  • 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:

ContractTypeDescription
IPAssetERC721IP asset ownership and licensing
LicenseTokenERC1155License ownership and management
GovernanceArbitratorGovernanceDispute resolution and arbitration
MarketplaceTradingIP asset and license marketplace
RevenueDistributorLogicRevenue 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

  1. IPAsset is the entry point - users mint IP assets as ERC-721 NFTs
  2. License creation is delegated - IPAsset.mintLicense() calls LicenseToken.mintLicense()
  3. Payment flow is automatic - when a license sells, payment is immediately distributed
  4. 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):

  1. Platform fee is calculated on total amount and deducted first (e.g., 2.5%)
  2. If revenue split configured, remaining amount split by shares (must sum to 100%)
  3. If no split configured, all remaining amount goes to IP asset owner

For Secondary Sales (subsequent sales):

  1. Royalty fee is calculated on total amount (default or per-asset custom rate)
  2. Royalty amount is distributed according to revenue split configuration
  3. 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:

  1. The NFT transfers to the IPAsset contract (custody)
  2. IPAsset mints a new token representing the wrapped NFT
  3. The IPAsset token grants all licensing rights
  4. 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

Git Source

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

NameTypeDescription
adminaddressAddress to receive admin role
licenseTokenaddressAddress of LicenseToken contract
ipAssetaddressAddress of IPAsset contract
revenueDistributoraddressAddress 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

NameTypeDescription
licenseIduint256The license being disputed
reasonstringHuman-readable dispute reason
proofURIstringURI pointing to evidence/documentation

Returns

NameTypeDescription
disputeIduint256Unique identifier for the dispute

resolveDispute

Resolves a dispute

Only callable by ARBITRATOR_ROLE

function resolveDispute(uint256 disputeId, bool approved, string memory resolutionReason) external;

Parameters

NameTypeDescription
disputeIduint256The dispute to resolve
approvedboolWhether to approve (true) or reject (false) the dispute
resolutionReasonstringExplanation of the resolution

executeRevocation

Executes license revocation for an approved dispute

Calls LicenseToken.revokeLicense() and updates dispute status

function executeRevocation(uint256 disputeId) external;

Parameters

NameTypeDescription
disputeIduint256The approved dispute to execute

getDispute

Gets full dispute information

function getDispute(uint256 disputeId) external view returns (Dispute memory dispute);

Parameters

NameTypeDescription
disputeIduint256The dispute ID

Returns

NameTypeDescription
disputeDisputeThe complete dispute struct

getDisputesForLicense

Gets all dispute IDs for a specific license

function getDisputesForLicense(uint256 licenseId) external view returns (uint256[] memory disputeIds);

Parameters

NameTypeDescription
licenseIduint256The license ID

Returns

NameTypeDescription
disputeIdsuint256[]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

NameTypeDescription
disputeIduint256The dispute ID

Returns

NameTypeDescription
overdueboolWhether the dispute is overdue

getTimeRemaining

Gets time remaining for dispute resolution

function getTimeRemaining(uint256 disputeId) external view returns (uint256 timeRemaining);

Parameters

NameTypeDescription
disputeIduint256The dispute ID

Returns

NameTypeDescription
timeRemaininguint256Seconds remaining (0 if overdue)

getDisputeCount

Gets the total number of disputes submitted

function getDisputeCount() external view returns (uint256 count);

Returns

NameTypeDescription
countuint256Total 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

NameTypeDescription
disputeIduint256Unique dispute identifier
licenseIduint256The license being disputed
submitteraddressAddress submitting the dispute
reasonstringDispute reason

DisputeResolved

Emitted when a dispute is resolved

event DisputeResolved(uint256 indexed disputeId, bool approved, address indexed resolver, string reason);

Parameters

NameTypeDescription
disputeIduint256The dispute that was resolved
approvedboolWhether dispute was approved (true) or rejected (false)
resolveraddressAddress that resolved the dispute
reasonstringResolution reasoning

LicenseRevoked

Emitted when a license is revoked due to dispute

event LicenseRevoked(uint256 indexed licenseId, uint256 indexed disputeId);

Parameters

NameTypeDescription
licenseIduint256The license that was revoked
disputeIduint256The 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

NameTypeDescription
licenseIduint256The license being disputed
submitteraddressAddress that submitted the dispute (BR-005.1: any party)
ipOwneraddressIP asset owner (cached from license data)
reasonstringHuman-readable dispute reason (BR-005.3: required)
proofURIstringOptional URI to supporting evidence (BR-005.1: optional)
statusDisputeStatusCurrent dispute status
submittedAtuint256Timestamp when dispute was submitted
resolvedAtuint256Timestamp when dispute was resolved (0 if pending)
resolveraddressAddress of arbitrator who resolved the dispute
resolutionReasonstringHuman-readable resolution explanation

Enums

DisputeStatus

Possible states of a dispute

enum DisputeStatus {
    Pending,
    Approved,
    Rejected,
    Executed
}

Variants

NameDescription
PendingDispute submitted, awaiting resolution
ApprovedDispute resolved in favor of submitter
RejectedDispute resolved against submitter
ExecutedApproved dispute has been executed (license revoked)

IIPAsset

Git Source

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

NameTypeDescription
namestringThe name for the ERC721 token
symbolstringThe symbol for the ERC721 token
adminaddressAddress to receive all initial admin roles (DEFAULT_ADMIN, PAUSER, UPGRADER)
licenseTokenaddressAddress of the LicenseToken contract
arbitratoraddressAddress 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

NameTypeDescription
toaddressAddress to receive the newly minted IP asset
metadataURIstringIPFS or HTTP URI pointing to IP metadata

Returns

NameTypeDescription
tokenIduint256The 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

NameTypeDescription
ipTokenIduint256The IP asset to create a license for
licenseeaddressAddress to receive the license
supplyuint256Number of license tokens to mint (ERC-1155 supply)
publicMetadataURIstringPublicly visible license metadata URI
privateMetadataURIstringPrivate license terms URI (access controlled)
expiryTimeuint256Unix timestamp when license expires
termsstringHuman-readable license terms
isExclusiveboolWhether this is an exclusive license
paymentIntervaluint256Payment interval in seconds (0 = one-time, >0 = recurring)

Returns

NameTypeDescription
licenseIduint256The 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

NameTypeDescription
tokenIduint256The IP asset token ID
newURIstringThe 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

NameTypeDescription
tokenIduint256The IP asset token ID
recipientsaddress[]Array of addresses to receive revenue shares
sharesuint256[]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

NameTypeDescription
tokenIduint256The IP asset token ID
basisPointsuint256Royalty 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

NameTypeDescription
tokenIduint256The 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

NameTypeDescription
tokenIduint256The IP asset token ID
hasDisputeboolWhether there is an active dispute

setLicenseTokenContract

Updates the LicenseToken contract address

Only callable by admin

function setLicenseTokenContract(address licenseToken) external;

Parameters

NameTypeDescription
licenseTokenaddressNew LicenseToken contract address

setArbitratorContract

Updates the GovernanceArbitrator contract address

Only callable by admin

function setArbitratorContract(address arbitrator) external;

Parameters

NameTypeDescription
arbitratoraddressNew GovernanceArbitrator contract address

setRevenueDistributorContract

Updates the RevenueDistributor contract address

Only callable by admin

function setRevenueDistributorContract(address distributor) external;

Parameters

NameTypeDescription
distributoraddressNew 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

NameTypeDescription
tokenIduint256The IP asset token ID
deltaint256Change 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

NameTypeDescription
tokenIduint256The IP asset token ID

Returns

NameTypeDescription
hasDisputeboolWhether 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

NameTypeDescription
tokenIduint256The ID of the IP asset
metadatastringThe 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

NameTypeDescription
tokenIduint256The ID of the IP asset

Returns

NameTypeDescription
<none>stringmetadata 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

NameTypeDescription
nftContractaddressThe address of the ERC721 contract
nftTokenIduint256The token ID of the NFT to wrap
metadataURIstringThe metadata URI for the new IPAsset

Returns

NameTypeDescription
ipTokenIduint256The 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

NameTypeDescription
tokenIduint256The IPAsset token ID to unwrap

isWrapped

Checks if an IPAsset is wrapping an external NFT

function isWrapped(uint256 tokenId) external view returns (bool);

Parameters

NameTypeDescription
tokenIduint256The IPAsset token ID

Returns

NameTypeDescription
<none>boolTrue 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

NameTypeDescription
tokenIduint256The IPAsset token ID

Returns

NameTypeDescription
nftContractaddressThe wrapped NFT contract address (zero address if not wrapped)
nftTokenIduint256The 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

NameTypeDescription
tokenIduint256The ID of the newly minted token
owneraddressThe address that owns the new IP asset
metadataURIstringThe 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

NameTypeDescription
tokenIduint256The ID of the token being updated
oldURIstringThe previous metadata URI
newURIstringThe new metadata URI
timestampuint256The 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

NameTypeDescription
ipTokenIduint256The IP asset token ID
licenseIduint256The 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

NameTypeDescription
ipTokenIduint256The IP asset token ID this license is for
licenseIduint256The ID of the newly registered license
licenseeaddressThe address receiving the license
supplyuint256Number of license tokens minted (ERC-1155 supply)
isExclusiveboolWhether 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

NameTypeDescription
tokenIduint256The IP asset token ID
recipientsaddress[]Array of recipient addresses
sharesuint256[]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

NameTypeDescription
tokenIduint256The IP asset token ID
basisPointsuint256Royalty 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

NameTypeDescription
tokenIduint256The IP asset token ID
hasDisputeboolWhether the asset now has an active dispute

LicenseTokenContractSet

Emitted when the LicenseToken contract address is updated

event LicenseTokenContractSet(address indexed newContract);

Parameters

NameTypeDescription
newContractaddressThe new LicenseToken contract address

ArbitratorContractSet

Emitted when the GovernanceArbitrator contract address is updated

event ArbitratorContractSet(address indexed newContract);

Parameters

NameTypeDescription
newContractaddressThe new GovernanceArbitrator contract address

RevenueDistributorSet

Emitted when the RevenueDistributor contract address is updated

event RevenueDistributorSet(address indexed newContract);

Parameters

NameTypeDescription
newContractaddressThe 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

NameTypeDescription
tokenIduint256The 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

NameTypeDescription
ipTokenIduint256The newly created IPAsset ID
nftContractaddressThe wrapped NFT contract address
nftTokenIduint256The wrapped NFT token ID
wrapperaddressThe 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

NameTypeDescription
ipTokenIduint256The burned IPAsset ID
nftContractaddressThe returned NFT contract address
nftTokenIduint256The returned NFT token ID
owneraddressThe 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

NameTypeDescription
contractAddressaddressThe 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

NameTypeDescription
tokenIduint256The IP asset token ID
countuint256Number of active licenses preventing the burn

HasActiveDispute

Thrown when attempting to burn a token with an active dispute

error HasActiveDispute(uint256 tokenId);

Parameters

NameTypeDescription
tokenIduint256The IP asset token ID

LicenseCountUnderflow

Thrown when attempting to decrement license count below zero

error LicenseCountUnderflow(uint256 tokenId, uint256 current, uint256 attempted);

Parameters

NameTypeDescription
tokenIduint256The IP asset token ID
currentuint256Current license count
attempteduint256Amount 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

NameTypeDescription
existingIPAssetIduint256The 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

NameTypeDescription
nftContractaddressThe NFT contract address
nftTokenIduint256The NFT token ID
calleraddressThe address attempting to wrap

Structs

WrappedNFT

Represents a wrapped NFT within an IPAsset

struct WrappedNFT {
    address nftContract;
    uint256 nftTokenId;
}

Properties

NameTypeDescription
nftContractaddressThe address of the wrapped ERC721 contract
nftTokenIduint256The token ID of the wrapped NFT

ILicenseToken

Git Source

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

NameTypeDescription
baseURIstringBase URI for token metadata
adminaddressAddress to receive all initial admin roles
ipAssetaddressAddress of the IPAsset contract (granted IP_ASSET_ROLE)
arbitratoraddressAddress 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

NameTypeDescription
toaddressAddress to receive the license
ipAssetIduint256The IP asset to license
supplyuint256Number of license tokens to mint (must be 1 for exclusive licenses)
publicMetadataURIstringPublicly accessible metadata
privateMetadataURIstringPrivate metadata (access controlled)
expiryTimeuint256Unix timestamp when license expires (0 = perpetual)
termsstringHuman-readable license terms
isExclusiveboolWhether this is an exclusive license
paymentIntervaluint256Payment interval in seconds (0 = ONE_TIME, >0 = RECURRENT)
maxMissedPaymentsuint8Maximum missed payments before auto-revocation (0 = use DEFAULT_MAX_MISSED_PAYMENTS)
penaltyRateBPSuint16Penalty rate in basis points per month (0 = use DEFAULT_PENALTY_RATE, max = MAX_PENALTY_RATE)

Returns

NameTypeDescription
licenseIduint256The 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

NameTypeDescription
licenseIduint256The 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

NameTypeDescription
licenseIdsuint256[]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

NameTypeDescription
licenseIduint256The license to revoke
reasonstringHuman-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

NameTypeDescription
licenseIduint256The license to revoke
missedCountuint256Number 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

NameTypeDescription
licenseIduint256The license ID

Returns

NameTypeDescription
uristringThe 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

NameTypeDescription
licenseIduint256The license ID

Returns

NameTypeDescription
uristringThe 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

NameTypeDescription
licenseIduint256The license ID
accountaddressThe 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

NameTypeDescription
licenseIduint256The license ID
accountaddressThe 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

NameTypeDescription
licenseIduint256The license ID
accountaddressThe account to check

Returns

NameTypeDescription
hasAccessboolWhether the account has been granted access

isRevoked

Checks if a license is revoked

function isRevoked(uint256 licenseId) external view returns (bool revoked);

Parameters

NameTypeDescription
licenseIduint256The license ID

Returns

NameTypeDescription
revokedboolWhether the license is revoked

isExpired

Checks if a license is expired

function isExpired(uint256 licenseId) external view returns (bool expired);

Parameters

NameTypeDescription
licenseIduint256The license ID

Returns

NameTypeDescription
expiredboolWhether 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

NameTypeDescription
arbitratoraddressNew 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

NameTypeDescription
ipAssetaddressNew 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

NameTypeDescription
rolebytes32The role identifier
accountaddressThe account to grant the role to

supportsInterface

Checks if contract supports a given interface

function supportsInterface(bytes4 interfaceId) external view returns (bool supported);

Parameters

NameTypeDescription
interfaceIdbytes4The interface identifier (ERC-165)

Returns

NameTypeDescription
supportedboolWhether the interface is supported

getPaymentInterval

Gets the payment interval for a license

function getPaymentInterval(uint256 licenseId) external view returns (uint256 interval);

Parameters

NameTypeDescription
licenseIduint256The license ID

Returns

NameTypeDescription
intervaluint256Payment 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

NameTypeDescription
licenseIduint256The license ID

Returns

NameTypeDescription
recurringboolTrue if payment interval > 0

isOneTime

Checks if a license is one-time payment

function isOneTime(uint256 licenseId) external view returns (bool oneTime);

Parameters

NameTypeDescription
licenseIduint256The license ID

Returns

NameTypeDescription
oneTimeboolTrue 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

NameTypeDescription
licenseIduint256The license ID

Returns

NameTypeDescription
ipAssetIduint256The IP asset this license is for
supplyuint256Number of license tokens minted
expiryTimeuint256Unix timestamp when license expires
termsstringHuman-readable license terms
paymentIntervaluint256Payment interval in seconds
isExclusiveboolWhether this is an exclusive license
revokedStatusboolWhether the license has been revoked
expiredStatusboolWhether 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

NameTypeDescription
licenseIduint256The license ID

Returns

NameTypeDescription
activeboolTrue 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

NameTypeDescription
licenseIduint256The license ID

Returns

NameTypeDescription
maxMisseduint8Maximum number of missed payments before auto-revocation

setPenaltyRate

Sets the penalty rate for a specific license

function setPenaltyRate(uint256 licenseId, uint16 penaltyRateBPS) external;

Parameters

NameTypeDescription
licenseIduint256The license ID
penaltyRateBPSuint16Penalty 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

NameTypeDescription
licenseIduint256The license ID

Returns

NameTypeDescription
penaltyRateuint16Penalty 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

NameTypeDescription
licenseIduint256The ID of the newly created license
ipAssetIduint256The IP asset this license is for
licenseeaddressThe address receiving the license
isExclusiveboolWhether this is an exclusive license
paymentIntervaluint256Payment interval in seconds (0 = ONE_TIME, >0 = RECURRENT)

LicenseExpired

Emitted when a license expires

event LicenseExpired(uint256 indexed licenseId);

Parameters

NameTypeDescription
licenseIduint256The license that expired

LicenseRevoked

Emitted when a license is revoked

event LicenseRevoked(uint256 indexed licenseId, string reason);

Parameters

NameTypeDescription
licenseIduint256The license that was revoked
reasonstringHuman-readable revocation reason

AutoRevoked

Emitted when a license is automatically revoked for missed payments

event AutoRevoked(uint256 indexed licenseId, uint256 missedPayments);

Parameters

NameTypeDescription
licenseIduint256The license that was auto-revoked
missedPaymentsuint256Number of missed payments that triggered revocation

PenaltyRateUpdated

Emitted when a license's penalty rate is updated

event PenaltyRateUpdated(uint256 indexed licenseId, uint16 penaltyRateBPS);

Parameters

NameTypeDescription
licenseIduint256The license ID
penaltyRateBPSuint16The new penalty rate in basis points

PrivateAccessGranted

Emitted when private metadata access is granted

event PrivateAccessGranted(uint256 indexed licenseId, address indexed account);

Parameters

NameTypeDescription
licenseIduint256The license ID
accountaddressThe account granted access

PrivateAccessRevoked

Emitted when private metadata access is revoked

event PrivateAccessRevoked(uint256 indexed licenseId, address indexed account);

Parameters

NameTypeDescription
licenseIduint256The license ID
accountaddressThe account whose access was revoked

ArbitratorContractUpdated

Emitted when the arbitrator contract is updated

event ArbitratorContractUpdated(address indexed oldArbitrator, address indexed newArbitrator);

Parameters

NameTypeDescription
oldArbitratoraddressThe previous arbitrator contract address
newArbitratoraddressThe new arbitrator contract address

IPAssetContractUpdated

Emitted when the IP asset contract is updated

event IPAssetContractUpdated(address indexed oldIPAsset, address indexed newIPAsset);

Parameters

NameTypeDescription
oldIPAssetaddressThe previous IP asset contract address
newIPAssetaddressThe 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

NameTypeDescription
ipAssetIduint256The IP asset this license is for
supplyuint256Number of license tokens minted (ERC-1155 supply)
expiryTimeuint256Unix timestamp when license expires (0 = perpetual, never expires)
termsstringHuman-readable license terms
isExclusiveboolWhether this is an exclusive license
isRevokedboolWhether the license has been revoked
publicMetadataURIstringPublicly accessible metadata URI
privateMetadataURIstringPrivate metadata URI (access controlled)
paymentIntervaluint256Payment interval in seconds (0 = ONE_TIME, >0 = RECURRENT)
maxMissedPaymentsuint8Maximum number of missed payments before auto-revocation (1-255, 0 defaults to 3)
penaltyRateBPSuint16Penalty rate in basis points (100 bps = 1% per month, 0 defaults to 500, max 5000 = 50%)

IMarketplace

Git Source

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

NameTypeDescription
adminaddressAddress to receive admin role
revenueDistributoraddressAddress 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

NameTypeDescription
nftContractaddressAddress of NFT contract
tokenIduint256Token ID to list
priceuint256Listing price in wei
isERC721boolWhether the NFT is ERC-721 (true) or ERC-1155 (false)

Returns

NameTypeDescription
listingIdbytes32Unique identifier for the listing

cancelListing

Cancels an active listing

Only seller can cancel their own listing

function cancelListing(bytes32 listingId) external;

Parameters

NameTypeDescription
listingIdbytes32The listing to cancel

buyListing

Buys an NFT from a listing

Transfers NFT, distributes payment with fees/royalties

function buyListing(bytes32 listingId) external payable;

Parameters

NameTypeDescription
listingIdbytes32The 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

NameTypeDescription
nftContractaddressAddress of NFT contract
tokenIduint256Token ID to make offer for
expiryTimeuint256Unix timestamp when offer expires

Returns

NameTypeDescription
offerIdbytes32Unique 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

NameTypeDescription
offerIdbytes32The offer to accept

cancelOffer

Cancels an offer and refunds escrowed funds

Only offer creator can cancel

function cancelOffer(bytes32 offerId) external;

Parameters

NameTypeDescription
offerIdbytes32The 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

NameTypeDescription
basisPointsuint256Penalty 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

NameTypeDescription
licenseContractaddressAddress of the license token contract
licenseIduint256The license ID to check

Returns

NameTypeDescription
missedPaymentsuint256Number 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

NameTypeDescription
licenseContractaddressAddress of the license token contract
licenseIduint256The license ID to pay for

getRecurringPaymentAmount

Gets the base amount for a recurring payment

function getRecurringPaymentAmount(uint256 licenseId) external view returns (uint256 baseAmount);

Parameters

NameTypeDescription
licenseIduint256The license ID

Returns

NameTypeDescription
baseAmountuint256The 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

NameTypeDescription
licenseContractaddressAddress of the license token contract
licenseIduint256The license ID

Returns

NameTypeDescription
penaltyuint256Penalty 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

NameTypeDescription
licenseContractaddressAddress of the license token contract
licenseIduint256The license ID

Returns

NameTypeDescription
baseAmountuint256The base payment amount
penaltyuint256The penalty amount if overdue
totaluint256The 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

NameTypeDescription
listingIdbytes32Unique identifier for the listing
selleraddressAddress of the seller
nftContractaddressNFT contract address
tokenIduint256Token ID being listed
priceuint256Listing price

ListingCancelled

Emitted when a listing is cancelled

event ListingCancelled(bytes32 indexed listingId);

Parameters

NameTypeDescription
listingIdbytes32The 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

NameTypeDescription
offerIdbytes32Unique identifier for the offer
buyeraddressAddress making the offer
nftContractaddressNFT contract address
tokenIduint256Token ID for the offer
priceuint256Offer price

OfferAccepted

Emitted when an offer is accepted

event OfferAccepted(bytes32 indexed offerId, address indexed seller);

Parameters

NameTypeDescription
offerIdbytes32The offer that was accepted
selleraddressAddress of the seller who accepted

OfferCancelled

Emitted when an offer is cancelled

event OfferCancelled(bytes32 indexed offerId);

Parameters

NameTypeDescription
offerIdbytes32The 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

NameTypeDescription
saleIdbytes32Unique sale identifier
buyeraddressAddress of the buyer
selleraddressAddress of the seller
priceuint256Total 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

NameTypeDescription
licenseIduint256The license ID for the recurring payment
payeraddressAddress making the payment
baseAmountuint256Base payment amount (without penalty)
penaltyuint256Penalty amount for late payment
timestampuint256Time of payment

PenaltyRateUpdated

Emitted when penalty rate is updated

event PenaltyRateUpdated(uint256 newRate);

Parameters

NameTypeDescription
newRateuint256New 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

NameTypeDescription
selleraddressAddress of the seller
nftContractaddressAddress of the NFT contract (ERC-721 or ERC-1155)
tokenIduint256Token ID being listed
priceuint256Listing price in wei
isActiveboolWhether the listing is currently active
isERC721boolWhether 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

NameTypeDescription
buyeraddressAddress making the offer
nftContractaddressAddress of the NFT contract
tokenIduint256Token ID for the offer
priceuint256Offer price in wei (held in escrow)
isActiveboolWhether the offer is currently active
expiryTimeuint256Unix timestamp when offer expires

RecurringPayment

Recurring payment tracking for subscription licenses

struct RecurringPayment {
    uint256 lastPaymentTime;
    address currentOwner;
    uint256 baseAmount;
}

Properties

NameTypeDescription
lastPaymentTimeuint256Timestamp of the last payment made
currentOwneraddressCurrent owner of the license (tracks transfers)
baseAmountuint256Base payment amount for recurring payments

IRevenueDistributor

Git Source

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

NameTypeDescription
ipAssetIduint256The IP asset ID
recipientsaddress[]Array of recipient addresses
sharesuint256[]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

NameTypeDescription
ipAssetIduint256The IP asset ID
amountuint256Payment amount to distribute
selleraddressAddress of the seller (receives remainder for secondary sales)
isPrimarySaleboolTrue 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

NameTypeDescription
recipientaddressAddress to query

Returns

NameTypeDescription
balanceuint256Principal amount available for withdrawal

setDefaultRoyalty

Sets the default royalty rate

Only callable by admin

function setDefaultRoyalty(uint256 basisPoints) external;

Parameters

NameTypeDescription
basisPointsuint256Royalty 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

NameTypeDescription
ipAssetIduint256The IP asset ID
basisPointsuint256Royalty 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

NameTypeDescription
ipAssetIduint256The IP asset ID

Returns

NameTypeDescription
<none>uint256Royalty 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

NameTypeDescription
ipAssetContractaddressAddress 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

NameTypeDescription
ipAssetIduint256The IP asset ID

Returns

NameTypeDescription
recipientsaddress[]Array of recipient addresses
sharesuint256[]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

NameTypeDescription
ipAssetIduint256The IP asset ID

Returns

NameTypeDescription
configuredboolTrue 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

NameTypeDescription
ipAssetIduint256The IP asset the payment is for
amountuint256Total payment amount
selleraddressAddress of the seller
isPrimarySaleboolWhether 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

NameTypeDescription
ipAssetIduint256The IP asset ID
recipientsaddress[]Array of recipient addresses
sharesuint256[]Array of share amounts

Withdrawal

Emitted when a recipient withdraws funds

event Withdrawal(address indexed recipient, uint256 principal);

Parameters

NameTypeDescription
recipientaddressAddress withdrawing
principaluint256Principal amount withdrawn

RoyaltyUpdated

Emitted when default royalty rate is updated

event RoyaltyUpdated(uint256 newRoyaltyBasisPoints);

Parameters

NameTypeDescription
newRoyaltyBasisPointsuint256New royalty rate in basis points

AssetRoyaltyUpdated

Emitted when a per-asset royalty rate is updated

event AssetRoyaltyUpdated(uint256 indexed ipAssetId, uint256 basisPoints);

Parameters

NameTypeDescription
ipAssetIduint256The IP asset ID
basisPointsuint256New 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

NameTypeDescription
recipientsaddress[]Array of addresses to receive revenue shares
sharesuint256[]Array of share amounts in basis points (must sum to 10000)