DividendTracker

0xbef711ca8cf5861c0eeebfac36ee2e055c837693

Verification
Verified
v0.8.24+commit.e11b9ed9
Type
Contract
5,150 bytes
ABI entries
48
21 read · 11 write
License
none

Contract information

Address
0xbef711ca8cf5861c0eeebfac36ee2e055c837693
Chain
Robinhood Chain (4663)
Compiler
v0.8.24+commit.e11b9ed9
Optimization
Enabled
Creation tx
0xab4b0460fc…2a6f627c0e

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (21)

MAX_REWARD_TOKENS()uint256
accrued(address, address)uint256
distributor()address
excluded(address)bool
holders(uint256)address
holdersLength()uint256
isHolder(address)bool
isRewardToken(address)bool
lastPerShare(address, address)uint256
magnifiedPerShare(address)uint256
minShareForRewards()uint256
owner()address
pendingOwner()address
processIndex()uint256
rewardTokens(uint256)address
rewardTokensLength()uint256
shareToken()address
shares(address)uint256
totalShares()uint256
withdrawable(address, address)uint256
withdrawn(address)uint256

Events (7)

ExcludedSetMinShareForRewardsSetOwnershipTransferStartedOwnershipTransferredRewardPaidRewardTokenRegisteredRewardsDistributed

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "initialOwner_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "shareToken_",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "distributor_",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "minShareForRewards_",
        "type": "uint256"
      },
      {
        "internalType": "address[]",
        "name": "excluded_",
        "type": "address[]"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "NoShares",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NotDistributor",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "owner",
        "type": "address"
      }
    ],
    "name": "OwnableInvalidOwner",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "OwnableUnauthorizedAccount",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ReentrancyGuardReentrantCall",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "SafeERC20FailedOperation",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "TooManyRewardTokens",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAddress",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "account",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "bool",
        "name": "excluded",
        "type": "bool"
      }
    ],
    "name": "ExcludedSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "minShareForRewards",
        "type": "uint256"
      }
    ],
    "name": "MinShareForRewardsSet",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "previousOwner",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "OwnershipTransferStarted",
    "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"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "holder",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "RewardPaid",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "RewardTokenRegistered",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "perShareAdded",
        "type": "uint256"
      }
    ],
    "name": "RewardsDistributed",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "MAX_REWARD_TOKENS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "acceptOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "accrued",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "distributeReward",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "distributor",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "excluded",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "holders",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "holdersLength",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "isHolder",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "isRewardToken",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "lastPerShare",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "magnifiedPerShare",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "minShareForRewards",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "pendingOwner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "count",
        "type": "uint256"
      }
    ],
    "name": "process",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "processIndex",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "registerRewardToken",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "renounceOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "name": "rewardTokens",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "rewardTokensLength",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      },
      {
        "internalType": "bool",
        "name": "excluded_",
        "type": "bool"
      }
    ],
    "name": "setExcluded",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "newMin",
        "type": "uint256"
      }
    ],
    "name": "setMinShareForRewards",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "shareToken",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "shares",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "sync",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address[]",
        "name": "accounts",
        "type": "address[]"
      }
    ],
    "name": "syncMany",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "totalShares",
    "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": "address",
        "name": "holder",
        "type": "address"
      }
    ],
    "name": "withdraw",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "holder",
        "type": "address"
      }
    ],
    "name": "withdrawable",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "withdrawn",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  }
]

Source code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol";
import {Ownable2Step} from "@openzeppelin/contracts/access/Ownable2Step.sol";

/// @title  DividendTracker  (FULLY IMMUTABLE, auto-push, multi-token)
/// @notice Holders of $MEMEDEX automatically accrue every memecoin the IndexManager distributes, pro-rata to
///         their $MEMEDEX balance, and receive it in their wallet with no action required. This is the
///         "buy $MEMEDEX, memes just show up" mechanic.
///
/// @dev    ACCOUNTING: a per-token MasterChef-style accumulator. For reward token `t`,
///         `magnifiedPerShare[t]` increases by `amount * MAGNITUDE / totalShares` on each distribution. A
///         holder's owed amount is `shares * (magnifiedPerShare[t] - lastPerShare[t][holder]) / MAGNITUDE`,
///         settled into `accrued[t][holder]` whenever their shares change. No off-chain computation, no Merkle
///         root, no snapshots.
///
///         SHARE SYNC: shares mirror $MEMEDEX balances via `sync(account)`, which reads `balanceOf` directly
///         from the token. This works with a flap.sh standard tax token (which has no custom transfer hook) —
///         the keeper calls `sync` for holders whose balance changed (from Transfer events); anyone may too.
///         It is trustless: it can only set shares to the holder's real on-chain balance.
///
///         TRUST MODEL: `shareToken` (MEMEDEX) and `distributor` (IndexManager) are fixed at construction and
///         cannot change. The owner has exactly ONE narrow power — `setMinShareForRewards` (the anti-dust
///         eligibility threshold). The owner can NEVER move funds, redirect payouts, pause, or upgrade — so
///         holder money stays trustless; only who's *eligible* is tunable. `sync` is permissionless;
///         `distributeReward`/`registerRewardToken` are distributor-only. Funds can only ever be delivered to
///         the holder they accrued for.
///
///         AUTO-DELIVERY: `process(count)` walks a rolling queue and pushes owed rewards; the keeper runs it in
///         batches. `withdraw()` is always available as a fallback so nobody can ever be stuck waiting. A
///         reward token that reverts on transfer (malicious/paused meme) is skipped, never bricking others.
contract DividendTracker is Ownable2Step, ReentrancyGuard {
    using SafeERC20 for IERC20;

    uint256 internal constant MAGNITUDE = 2 ** 128;
    uint256 public constant MAX_REWARD_TOKENS = 16; // bounds per-transfer settle cost

    address public immutable shareToken; // $MEMEDEX
    address public immutable distributor; // IndexManager
    uint256 public minShareForRewards; // owner-tunable min $MEMEDEX balance to earn (anti-dust)

    address[] public rewardTokens;
    mapping(address => bool) public isRewardToken;

    uint256 public totalShares;
    mapping(address => uint256) public shares; // eligible $MEMEDEX balance
    mapping(address => bool) public excluded; // pairs / contracts / this / dead

    mapping(address => uint256) public magnifiedPerShare; // token => acc
    mapping(address => mapping(address => uint256)) public lastPerShare; // token => holder => checkpoint
    mapping(address => mapping(address => uint256)) public accrued; // token => holder => owed, not yet sent
    mapping(address => uint256) public withdrawn; // token => total pushed (info)

    address[] public holders;
    mapping(address => bool) public isHolder;
    uint256 public processIndex; // rolling cursor for auto-push

    event RewardTokenRegistered(address indexed token);
    event RewardsDistributed(address indexed token, uint256 amount, uint256 perShareAdded);
    event RewardPaid(address indexed token, address indexed holder, uint256 amount);
    event ExcludedSet(address indexed account, bool excluded);
    event MinShareForRewardsSet(uint256 minShareForRewards);

    error NotDistributor();
    error ZeroAddress();
    error TooManyRewardTokens();
    error NoShares();

    constructor(
        address initialOwner_,
        address shareToken_,
        address distributor_,
        uint256 minShareForRewards_,
        address[] memory excluded_
    ) Ownable(initialOwner_) {
        if (shareToken_ == address(0) || distributor_ == address(0)) revert ZeroAddress();
        shareToken = shareToken_;
        distributor = distributor_;
        minShareForRewards = minShareForRewards_;
        // Exclusions are fixed at construction (pairs, router, treasury, this, dead, etc.).
        for (uint256 i = 0; i < excluded_.length; i++) {
            excluded[excluded_[i]] = true;
            emit ExcludedSet(excluded_[i], true);
        }
        excluded[address(this)] = true;
        excluded[distributor_] = true;
    }

    // --- admin (owner: threshold only, never funds) ------------------------

    /// @notice Tune the minimum $MEMEDEX balance required to earn meme payouts (anti-dust). New holders reflect
    ///         it on their next `sync`; existing holders update when next synced. Owner power is eligibility-only
    ///         — it can never touch funds or redirect payouts.
    function setMinShareForRewards(uint256 newMin) external onlyOwner {
        minShareForRewards = newMin;
        emit MinShareForRewardsSet(newMin);
    }

    /// @notice Exclude/include an address from earning dividends and immediately re-sync its shares. Needed for
    ///         addresses that appear post-deploy and should NOT earn — the flap bonding curve, the Uniswap LP
    ///         pair (excluding it stops payouts being wasted on liquidity), CEX/bridge wallets, etc. Excluding a
    ///         holder only removes their *future* eligibility; already-accrued rewards remain claimable. Owner
    ///         power is eligibility-only — never touches funds.
    function setExcluded(address account, bool excluded_) external onlyOwner {
        excluded[account] = excluded_;
        emit ExcludedSet(account, excluded_);
        sync(account); // apply immediately (zeros shares if now excluded)
    }

    // --- shares (permissionless; reads real balance) -----------------------

    /// @notice Re-read `account`'s $MEMEDEX balance and update its shares. Settles all reward tokens first so
    ///         the change never retroactively alters already-earned rewards. Permissionless and trustless: it
    ///         can only set shares to the holder's real on-chain balance (or 0 if excluded / below the min).
    function sync(address account) public {
        uint256 bal = IERC20(shareToken).balanceOf(account);
        uint256 newShares = (excluded[account] || bal < minShareForRewards) ? 0 : bal;
        _settleAll(account);
        totalShares = totalShares - shares[account] + newShares;
        shares[account] = newShares;
        if (newShares > 0 && !isHolder[account]) {
            isHolder[account] = true;
            holders.push(account);
        }
    }

    /// @notice Batch `sync` — the keeper feeds addresses seen in the token's Transfer events.
    function syncMany(address[] calldata accounts) external {
        for (uint256 i = 0; i < accounts.length; i++) {
            sync(accounts[i]);
        }
    }

    // --- rewards (only the distributor / IndexManager) ---------------------

    function registerRewardToken(address token) public {
        if (msg.sender != distributor) revert NotDistributor();
        if (token == address(0)) revert ZeroAddress();
        if (isRewardToken[token]) return;
        if (rewardTokens.length >= MAX_REWARD_TOKENS) revert TooManyRewardTokens();
        isRewardToken[token] = true;
        rewardTokens.push(token);
        emit RewardTokenRegistered(token);
    }

    /// @notice Credit `amount` of `token` (already transferred to this contract) across all shareholders.
    function distributeReward(address token, uint256 amount) external {
        if (msg.sender != distributor) revert NotDistributor();
        if (totalShares == 0) revert NoShares();
        if (!isRewardToken[token]) registerRewardToken(token);
        if (amount == 0) return;
        uint256 perShareAdded = (amount * MAGNITUDE) / totalShares;
        magnifiedPerShare[token] += perShareAdded;
        emit RewardsDistributed(token, amount, perShareAdded);
    }

    // --- delivery ----------------------------------------------------------

    /// @notice Auto-push owed rewards to the next `count` holders in the rolling queue.
    function process(uint256 count) external nonReentrant {
        uint256 n = holders.length;
        if (n == 0) return;
        uint256 idx = processIndex;
        uint256 iterations = count < n ? count : n;
        for (uint256 i = 0; i < iterations; i++) {
            if (idx >= n) idx = 0;
            _payAll(holders[idx], true);
            idx++;
        }
        processIndex = idx;
    }

    /// @notice Pull fallback: a holder (or anyone, on their behalf) claims all owed rewards immediately.
    function withdraw(address holder) external nonReentrant {
        _payAll(holder, false);
    }

    /// @notice View a holder's currently-owed amount of one reward token (settled + pending).
    function withdrawable(address token, address holder) external view returns (uint256) {
        uint256 pending = (shares[holder] * (magnifiedPerShare[token] - lastPerShare[token][holder])) / MAGNITUDE;
        return accrued[token][holder] + pending;
    }

    function rewardTokensLength() external view returns (uint256) {
        return rewardTokens.length;
    }

    function holdersLength() external view returns (uint256) {
        return holders.length;
    }

    // --- internals ---------------------------------------------------------

    function _settleAll(address holder) internal {
        uint256 s = shares[holder];
        uint256 len = rewardTokens.length;
        for (uint256 i = 0; i < len; i++) {
            address t = rewardTokens[i];
            uint256 acc = magnifiedPerShare[t];
            uint256 last = lastPerShare[t][holder];
            if (acc != last) {
                if (s > 0) accrued[t][holder] += (s * (acc - last)) / MAGNITUDE;
                lastPerShare[t][holder] = acc;
            }
        }
    }

    /// @param skipOnFailure when true (auto-push), a reverting reward token is skipped so it can't block the
    ///        queue; the owed amount stays in `accrued` for a later withdraw.
    function _payAll(address holder, bool skipOnFailure) internal {
        _settleAll(holder);
        uint256 len = rewardTokens.length;
        for (uint256 i = 0; i < len; i++) {
            address t = rewardTokens[i];
            uint256 owed = accrued[t][holder];
            if (owed == 0) continue;
            accrued[t][holder] = 0; // effects before interaction
            withdrawn[t] += owed;
            if (skipOnFailure) {
                try IERC20(t).transfer(holder, owed) returns (bool ok) {
                    if (!ok) {
                        accrued[t][holder] = owed; // revert bookkeeping on soft failure
                        withdrawn[t] -= owed;
                        continue;
                    }
                } catch {
                    accrued[t][holder] = owed;
                    withdrawn[t] -= owed;
                    continue;
                }
            } else {
                IERC20(t).safeTransfer(holder, owed);
            }
            emit RewardPaid(t, holder, owed);
        }
    }
}
Chain explorer1493msChain node70ms