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
- Creator
- 0x0d45254857…2E1FA4aF74
- 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