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