CashCatDividendTracker
0x2f30e16d904764cf0194d6d7cc9164bd54a8cb7c
Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
5,300 bytes
ABI entries
42
20 read · 10 write
License
none
Contract information
- Address
- 0x2f30e16d904764cf0194d6d7cc9164bd54a8cb7c
- Chain
- Robinhood Chain (4663)
- Compiler
- v0.8.26+commit.8a97fa7a
- Optimization
- Enabled
- Creator
- 0x9C11E760e4…F20B233550
- Creation tx
- 0xd0e549a7ce…e6a2e41a26
Token
Not a token
This contract does not expose ERC-20 metadata.
Read contract (20)
MAX_CLAIM_WAIT() → uint256
MIN_CLAIM_WAIT() → uint256
accumulativeDividendOf(address) → uint256
balanceOf(address) → uint256
claimWait() → uint256
excludedFromDividends(address) → bool
getAccount(address) → uint256, uint256, uint256, uint256, bool
getNumberOfTokenHolders() → uint256
lastClaimTimes(address) → uint256
lastProcessedIndex() → uint256
magnifiedDividendPerShare() → uint256
minimumTokenBalanceForDividends() → uint256
owner() → address
rewardToken() → address
token() → address
totalDividendsDistributed() → uint256
totalDividendsWithdrawn() → uint256
totalSupply() → uint256
withdrawableDividendOf(address) → uint256
withdrawnDividendOf(address) → uint256
Events (7)
ClaimClaimWaitUpdatedDividendWithdrawnDividendsDistributedExcludeFromDividendsMinimumBalanceUpdatedOwnershipTransferred
ABI
[
{
"inputs": [
{
"internalType": "address",
"name": "rewardToken_",
"type": "address"
},
{
"internalType": "address",
"name": "token_",
"type": "address"
},
{
"internalType": "uint256",
"name": "minimumTokenBalanceForDividends_",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "constructor"
},
{
"inputs": [
{
"internalType": "address",
"name": "owner",
"type": "address"
}
],
"name": "OwnableInvalidOwner",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "OwnableUnauthorizedAccount",
"type": "error"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "value",
"type": "uint256"
}
],
"name": "SafeCastOverflowedUintToInt",
"type": "error"
},
{
"inputs": [
{
"internalType": "address",
"name": "token",
"type": "address"
}
],
"name": "SafeERC20FailedOperation",
"type": "error"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "account",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
},
{
"indexed": true,
"internalType": "bool",
"name": "automatic",
"type": "bool"
}
],
"name": "Claim",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "newValue",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "oldValue",
"type": "uint256"
}
],
"name": "ClaimWaitUpdated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "account",
"type": "address"
},
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "DividendWithdrawn",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "amount",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "magnifiedPerShare",
"type": "uint256"
}
],
"name": "DividendsDistributed",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "account",
"type": "address"
},
{
"indexed": false,
"internalType": "bool",
"name": "excluded",
"type": "bool"
}
],
"name": "ExcludeFromDividends",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "uint256",
"name": "newValue",
"type": "uint256"
},
{
"indexed": false,
"internalType": "uint256",
"name": "oldValue",
"type": "uint256"
}
],
"name": "MinimumBalanceUpdated",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"internalType": "address",
"name": "previousOwner",
"type": "address"
},
{
"indexed": true,
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "OwnershipTransferred",
"type": "event"
},
{
"inputs": [],
"name": "MAX_CLAIM_WAIT",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "MIN_CLAIM_WAIT",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "accumulativeDividendOf",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "claimWait",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "amount",
"type": "uint256"
}
],
"name": "distributeDividends",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
},
{
"internalType": "bool",
"name": "excluded",
"type": "bool"
}
],
"name": "excludeFromDividends",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "excludedFromDividends",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "getAccount",
"outputs": [
{
"internalType": "uint256",
"name": "trackedBalance",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "withdrawableDividends",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "totalDividends",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "lastClaimTime",
"type": "uint256"
},
{
"internalType": "bool",
"name": "excluded",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "getNumberOfTokenHolders",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"name": "lastClaimTimes",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "lastProcessedIndex",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "magnifiedDividendPerShare",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "minimumTokenBalanceForDividends",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "owner",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "gas",
"type": "uint256"
}
],
"name": "process",
"outputs": [
{
"internalType": "uint256",
"name": "iterations",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "claims",
"type": "uint256"
},
{
"internalType": "uint256",
"name": "lastIndex",
"type": "uint256"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
},
{
"internalType": "bool",
"name": "automatic",
"type": "bool"
}
],
"name": "processAccount",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "processAccountFromQueue",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "renounceOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "rewardToken",
"outputs": [
{
"internalType": "contract IERC20",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
},
{
"internalType": "uint256",
"name": "newBalance",
"type": "uint256"
}
],
"name": "setBalance",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [],
"name": "token",
"outputs": [
{
"internalType": "address",
"name": "",
"type": "address"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "totalDividendsDistributed",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "totalDividendsWithdrawn",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "totalSupply",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "newOwner",
"type": "address"
}
],
"name": "transferOwnership",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "newClaimWait",
"type": "uint256"
}
],
"name": "updateClaimWait",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "uint256",
"name": "newMinimum",
"type": "uint256"
}
],
"name": "updateMinimumTokenBalanceForDividends",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "withdrawableDividendOf",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "address",
"name": "account",
"type": "address"
}
],
"name": "withdrawnDividendOf",
"outputs": [
{
"internalType": "uint256",
"name": "",
"type": "uint256"
}
],
"stateMutability": "view",
"type": "function"
}
]Source code
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol";
import {IterableMapping} from "./libraries/IterableMapping.sol";
/// @title CashCatDividendTracker
/// @notice Pull-based (O(1)) dividend accounting that distributes an external ERC20
/// reward token (CASHCAT) to holders of the main token, pro-rata to their
/// tracked share balance. Uses the battle-tested `magnifiedDividendPerShare`
/// pattern (ERC-1726 style) adapted to pay an arbitrary ERC20 instead of ETH.
///
/// @dev Ownership: this contract is deployed BY and OWNED BY the main BabyCashCat token,
/// so `onlyOwner` == the main token, and its human owner drives configuration through
/// pass-through admin functions on the token. `onlyToken` guards the hot-path methods
/// (setBalance / distribute / process) that only the main token may call.
contract CashCatDividendTracker is Ownable {
using SafeERC20 for IERC20;
using SafeCast for uint256;
using IterableMapping for IterableMapping.Map;
// --- reward + share bookkeeping ---
IERC20 public immutable rewardToken; // CASHCAT
address public immutable token; // main BabyCashCat token
uint256 internal constant MAGNITUDE = 2 ** 128;
uint256 public magnifiedDividendPerShare;
uint256 public totalDividendsDistributed;
uint256 public totalDividendsWithdrawn;
mapping(address => int256) internal magnifiedDividendCorrections;
mapping(address => uint256) internal withdrawnDividends;
/// @notice Tracked (dividend-eligible) share balance mirror of the main token.
mapping(address => uint256) public balanceOf;
uint256 public totalSupply;
// --- holder set + distribution queue ---
IterableMapping.Map private tokenHoldersMap;
mapping(address => bool) public excludedFromDividends;
mapping(address => uint256) public lastClaimTimes;
uint256 public claimWait = 3600; // min seconds between auto-claims per holder
uint256 public minimumTokenBalanceForDividends; // dust floor to be dividend-eligible
uint256 public lastProcessedIndex;
uint256 public constant MIN_CLAIM_WAIT = 3600; // 1h
uint256 public constant MAX_CLAIM_WAIT = 86400; // 24h
// --- events ---
event ExcludeFromDividends(address indexed account, bool excluded);
event ClaimWaitUpdated(uint256 newValue, uint256 oldValue);
event MinimumBalanceUpdated(uint256 newValue, uint256 oldValue);
event DividendsDistributed(uint256 amount, uint256 magnifiedPerShare);
event DividendWithdrawn(address indexed account, uint256 amount);
event Claim(address indexed account, uint256 amount, bool indexed automatic);
modifier onlyToken() {
require(msg.sender == token, "DividendTracker: caller is not the token");
_;
}
constructor(address rewardToken_, address token_, uint256 minimumTokenBalanceForDividends_)
Ownable(token_)
{
require(rewardToken_ != address(0) && token_ != address(0), "zero address");
rewardToken = IERC20(rewardToken_);
token = token_;
minimumTokenBalanceForDividends = minimumTokenBalanceForDividends_;
}
// ---------------------------------------------------------------------
// Distribution (called by the token after it has transferred CASHCAT in)
// ---------------------------------------------------------------------
/// @notice Credit `amount` of already-received CASHCAT to all current shareholders.
function distributeDividends(uint256 amount) external onlyToken {
require(totalSupply > 0, "no shares");
if (amount > 0) {
magnifiedDividendPerShare += (amount * MAGNITUDE) / totalSupply;
totalDividendsDistributed += amount;
emit DividendsDistributed(amount, magnifiedDividendPerShare);
}
}
// ---------------------------------------------------------------------
// Share balance sync (called by the token on every taxable transfer)
// ---------------------------------------------------------------------
function setBalance(address account, uint256 newBalance) external onlyToken {
if (excludedFromDividends[account]) {
return;
}
if (newBalance >= minimumTokenBalanceForDividends) {
_setBalance(account, newBalance);
tokenHoldersMap.set(account, newBalance);
} else {
_setBalance(account, 0);
tokenHoldersMap.remove(account);
}
}
function _setBalance(address account, uint256 newBalance) private {
uint256 current = balanceOf[account];
if (newBalance > current) {
_mintShares(account, newBalance - current);
} else if (newBalance < current) {
_burnShares(account, current - newBalance);
}
}
function _mintShares(address account, uint256 value) private {
balanceOf[account] += value;
totalSupply += value;
magnifiedDividendCorrections[account] -= (magnifiedDividendPerShare * value).toInt256();
}
function _burnShares(address account, uint256 value) private {
balanceOf[account] -= value;
totalSupply -= value;
magnifiedDividendCorrections[account] += (magnifiedDividendPerShare * value).toInt256();
}
// ---------------------------------------------------------------------
// Views
// ---------------------------------------------------------------------
function accumulativeDividendOf(address account) public view returns (uint256) {
int256 a = (magnifiedDividendPerShare * balanceOf[account]).toInt256()
+ magnifiedDividendCorrections[account];
if (a < 0) a = 0;
return uint256(a) / MAGNITUDE;
}
function withdrawableDividendOf(address account) public view returns (uint256) {
return accumulativeDividendOf(account) - withdrawnDividends[account];
}
function withdrawnDividendOf(address account) external view returns (uint256) {
return withdrawnDividends[account];
}
function getNumberOfTokenHolders() external view returns (uint256) {
return tokenHoldersMap.size();
}
/// @notice UI helper: all dividend info for one account.
function getAccount(address account)
external
view
returns (
uint256 trackedBalance,
uint256 withdrawableDividends,
uint256 totalDividends,
uint256 lastClaimTime,
bool excluded
)
{
return (
balanceOf[account],
withdrawableDividendOf(account),
accumulativeDividendOf(account),
lastClaimTimes[account],
excludedFromDividends[account]
);
}
// ---------------------------------------------------------------------
// Withdrawals / processing
// ---------------------------------------------------------------------
function _withdrawDividendOfUser(address account) private returns (uint256) {
uint256 _withdrawable = withdrawableDividendOf(account);
if (_withdrawable > 0) {
withdrawnDividends[account] += _withdrawable;
totalDividendsWithdrawn += _withdrawable;
emit DividendWithdrawn(account, _withdrawable);
rewardToken.safeTransfer(account, _withdrawable);
}
return _withdrawable;
}
function _processAccount(address account, bool automatic) private returns (bool) {
uint256 amount = _withdrawDividendOfUser(account);
if (amount > 0) {
lastClaimTimes[account] = block.timestamp;
emit Claim(account, amount, automatic);
return true;
}
return false;
}
/// @notice Force-process a single account (used by the token's `claim()` pass-through).
function processAccount(address account, bool automatic) external onlyToken returns (bool) {
return _processAccount(account, automatic);
}
/// @dev Self-called from process() so a single reverting payout is isolated in try/catch.
function processAccountFromQueue(address account) external returns (bool) {
require(msg.sender == address(this), "only self");
return _processAccount(account, true);
}
/// @notice Gas-bounded walk of the holder set, auto-claiming eligible holders.
function process(uint256 gas) external onlyToken returns (uint256 iterations, uint256 claims, uint256 lastIndex) {
uint256 numberOfTokenHolders = tokenHoldersMap.size();
if (numberOfTokenHolders == 0) {
return (0, 0, lastProcessedIndex);
}
uint256 _lastProcessedIndex = lastProcessedIndex;
uint256 gasUsed = 0;
uint256 gasLeft = gasleft();
while (gasUsed < gas && iterations < numberOfTokenHolders) {
_lastProcessedIndex++;
if (_lastProcessedIndex >= numberOfTokenHolders) {
_lastProcessedIndex = 0;
}
address account = tokenHoldersMap.getKeyAtIndex(_lastProcessedIndex);
if (_canAutoClaim(lastClaimTimes[account])) {
// Isolate each payout: one un-payable holder can never freeze the whole queue.
try this.processAccountFromQueue(account) returns (bool claimed) {
if (claimed) claims++;
} catch {}
}
iterations++;
uint256 newGasLeft = gasleft();
if (gasLeft > newGasLeft) {
gasUsed += (gasLeft - newGasLeft);
}
gasLeft = newGasLeft;
}
lastProcessedIndex = _lastProcessedIndex;
return (iterations, claims, lastProcessedIndex);
}
function _canAutoClaim(uint256 lastClaimTime) private view returns (bool) {
if (lastClaimTime > block.timestamp) {
return false;
}
return block.timestamp - lastClaimTime >= claimWait;
}
// ---------------------------------------------------------------------
// Admin (owner == main token; driven by the token's human owner)
// ---------------------------------------------------------------------
function excludeFromDividends(address account, bool excluded) external onlyOwner {
if (excludedFromDividends[account] == excluded) return;
excludedFromDividends[account] = excluded;
if (excluded) {
_setBalance(account, 0);
tokenHoldersMap.remove(account);
} else {
// Re-include: immediately resync shares from the live token balance so the account
// isn't silently zeroed until its next transfer.
uint256 bal = IERC20(token).balanceOf(account);
if (bal >= minimumTokenBalanceForDividends) {
_setBalance(account, bal);
tokenHoldersMap.set(account, bal);
}
}
emit ExcludeFromDividends(account, excluded);
}
function updateClaimWait(uint256 newClaimWait) external onlyOwner {
require(newClaimWait >= MIN_CLAIM_WAIT && newClaimWait <= MAX_CLAIM_WAIT, "claimWait out of range");
emit ClaimWaitUpdated(newClaimWait, claimWait);
claimWait = newClaimWait;
}
function updateMinimumTokenBalanceForDividends(uint256 newMinimum) external onlyOwner {
emit MinimumBalanceUpdated(newMinimum, minimumTokenBalanceForDividends);
minimumTokenBalanceForDividends = newMinimum;
}
}
Chain explorer3101msChain node80ms