Skip to main content

ClawDividends

ClawDividends is an epoch-based fee distribution contract that shares protocol revenue with .claw domain holders. Agents deposit their earned fees, an operator snapshots holders, and each holder claims their pro-rata share.

Overview

PropertyValue
Contract0x3441aa2Bf84EDF9f44A2ad3b93BDCce7D801Fb06
Distribution ModelEpoch-based pro-rata
SupportsETH and any ERC-20 token
Challenge Period6 hours per epoch
Min Epoch Duration1 hour between epochs

How It Works

┌─────────────┐                           ┌──────────────────┐
│ Agent Fees │ ── depositETH/ERC20 ───▶ │ ClawDividends │
└─────────────┘ │ │
│ Pending Pool │
┌─────────────┐ │ │ │
│ Operator │ ── createEpoch ─────────▶ │ ┌────▼────┐ │
└─────────────┘ (holder snapshot) │ │ Epoch N │ │
│ │ Per-NFT │ │
┌─────────────┐ │ │ Share │ │
│ NFT Holder │ ◀── claim ────────────── │ └─────────┘ │
└─────────────┘ └──────────────────┘

Step 1: Fee Deposits

Agents (or anyone) deposit fees into the pending pool:

const dividends = new ethers.Contract(DIVIDENDS_ADDRESS, [
'function depositETH() external payable',
'function depositERC20(address token, uint256 amount) external',
], wallet);

// Deposit ETH fees
await dividends.depositETH({ value: ethers.parseEther('0.5') });

// Or deposit ERC-20 tokens
await dividends.depositERC20(tokenAddress, ethers.parseEther('1000'));

Fees accumulate in the pending pool until an epoch is created.

Step 2: Epoch Creation

An operator snapshots all .claw holders off-chain and submits the data on-chain:

function createEpoch(
address token, // ETH_SENTINEL for ETH, or ERC-20 address
address[] calldata holders, // Array of holder addresses
uint256[] calldata nftCounts, // NFT count per holder
uint256 totalNFTs, // Must match sum of nftCounts
bytes32 snapshotHash // Hash of snapshot data (for auditing)
) external onlyOperator;

The epoch:

  • Moves the pending pool for that token into the epoch
  • Calculates perNFTShare = totalPool / totalNFTs (floor division)
  • Stores holder snapshots on-chain
  • Sets a 6-hour challenge period before claims are allowed
  • Any remainder (dust) stays in the pending pool for the next epoch

Step 3: Challenge Period (C-02 Fix)

After epoch creation, there's a 6-hour window where the owner can cancel a suspicious epoch:

function cancelEpoch(uint256 epochId) external onlyOwner;

If cancelled:

  • The epoch's undistributed funds return to the pending pool
  • No claims are possible from cancelled epochs

Step 4: Claiming

After the challenge period, holders claim their dividends:

const dividends = new ethers.Contract(DIVIDENDS_ADDRESS, [
'function claim(uint256 epochId) external',
'function claimAll() external',
'function claimMultiple(uint256[] calldata epochIds) external',
'function getClaimable(uint256 epochId, address holder) external view returns (uint256)',
'function pendingDividends(address holder) external view returns (uint256 ethPending, address[] tokens, uint256[] amounts)',
], wallet);

// Check pending dividends
const { ethPending, tokens, amounts } = await dividends.pendingDividends(myAddress);
console.log('Pending ETH:', ethers.formatEther(ethPending));

// Claim a specific epoch
await dividends.claim(epochId);

// Or claim all unclaimed epochs
await dividends.claimAll();

// Or claim specific epochs
await dividends.claimMultiple([1, 2, 5, 8]);

Epoch Structure

struct Epoch {
uint256 id; // Epoch number (1-indexed)
uint256 createdAt; // Creation timestamp
uint256 finalizedAt; // Claimable after this time (challenge period)
uint256 totalNFTs; // Total .claw NFTs at snapshot
address token; // ETH or ERC-20 address
uint256 totalPool; // Distributable fee pool
uint256 perNFTShare; // Per-NFT dividend amount
uint256 totalClaimed; // Running claimed total
bytes32 snapshotHash; // Audit hash
bool finalized; // Is finalized
bool cancelled; // Was cancelled during challenge period
}

Pro-Rata Calculation

Each holder receives:

holderDividend = perNFTShare × nftCount

Where:

perNFTShare = totalPool / totalNFTs  (floor division)
dust = totalPool - (perNFTShare × totalNFTs) → rolls to next epoch

Example

ValueAmount
Pending ETH Pool10 ETH
Total .claw NFTs3
Per-NFT Share3.333... → 3 ETH (floor)
Distributable9 ETH
Dust (rolls over)1 ETH
HolderNFTsClaimable
Alice13 ETH
Bob26 ETH
Total39 ETH

Why Epochs?

ClawWallet uses an epoch-based distribution model instead of real-time tracking because:

  1. Gas Efficiency — The ERC-721 identity contract doesn't implement ERC721Enumerable, so on-chain holder enumeration would be prohibitively expensive
  2. Off-Chain Flexibility — Snapshots can be taken at optimal times
  3. Auditability — Each epoch includes a snapshotHash for off-chain verification
  4. Anti-Manipulation — Challenge period prevents fraudulent snapshots

Security Features

Challenge Period (C-02 Fix)

  • 6-hour window after epoch creation before claims are allowed
  • Owner can cancel suspicious epochs during this period
  • Full snapshot details emitted via EpochSnapshotDetails event for off-chain verification

Minimum Epoch Duration (C-02 Fix)

  • At least 1 hour between epoch creations
  • Prevents rapid-fire epoch manipulation
  • Configurable by owner (minimum 1 hour)

Other Protections

  • ReentrancyGuard on all claim/withdraw functions
  • Operator role separated from owner (defense in depth)
  • Emergency pause via owner
  • CEI pattern (Checks-Effects-Interactions) on claims
  • Unclaimed funds persist indefinitely — no lockout

View Functions

// Check pending dividends for a holder
const { ethPending, tokens, amounts } = await dividends.pendingDividends(holderAddress);

// Check claimable for a specific epoch
const claimable = await dividends.getClaimable(epochId, holderAddress);

// Get epoch details
const epoch = await dividends.getEpoch(epochId);

// Get holder snapshot for an epoch
const snapshot = await dividends.getHolderSnapshot(epochId, holderAddress);

// Get unclaimed epoch IDs
const unclaimedIds = await dividends.getUnclaimedEpochs(holderAddress);

// Current epoch counter
const currentEpoch = await dividends.currentEpoch();

// Lifetime stats
const totalDeposited = await dividends.totalDeposited(ETH_SENTINEL);
const totalClaimedETH = await dividends.totalClaimed(ETH_SENTINEL);

Events

EventDescription
FeeDeposited(agent, token, amount)Fees deposited into pending pool
EpochCreated(epochId, token, totalPool, totalNFTs)New epoch created
EpochSnapshotDetails(epochId, hash, holders, counts)Full snapshot data logged
EpochFinalized(epochId)Epoch finalized (claimable)
EpochCancelled(epochId, cancelledBy)Epoch cancelled during challenge
DividendClaimed(epochId, holder, amount)Holder claimed dividends
AgentRegistered(agent)Agent auto-registered on first deposit