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
| Property | Value |
|---|---|
| Contract | 0x3441aa2Bf84EDF9f44A2ad3b93BDCce7D801Fb06 |
| Distribution Model | Epoch-based pro-rata |
| Supports | ETH and any ERC-20 token |
| Challenge Period | 6 hours per epoch |
| Min Epoch Duration | 1 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
| Value | Amount |
|---|---|
| Pending ETH Pool | 10 ETH |
| Total .claw NFTs | 3 |
| Per-NFT Share | 3.333... → 3 ETH (floor) |
| Distributable | 9 ETH |
| Dust (rolls over) | 1 ETH |
| Holder | NFTs | Claimable |
|---|---|---|
| Alice | 1 | 3 ETH |
| Bob | 2 | 6 ETH |
| Total | 3 | 9 ETH |
Why Epochs?
ClawWallet uses an epoch-based distribution model instead of real-time tracking because:
- Gas Efficiency — The ERC-721 identity contract doesn't implement
ERC721Enumerable, so on-chain holder enumeration would be prohibitively expensive - Off-Chain Flexibility — Snapshots can be taken at optimal times
- Auditability — Each epoch includes a
snapshotHashfor off-chain verification - 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
EpochSnapshotDetailsevent 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
ReentrancyGuardon 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
| Event | Description |
|---|---|
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 |