BangDistributor

0x6edd4dbb53cb2718e6c669e43ba2f0811f548136

Verification
Verified
v0.8.26+commit.8a97fa7a
Type
Contract
7,105 bytes
ABI entries
55
21 read · 9 write
License
none

Contract information

Address
0x6edd4dbb53cb2718e6c669e43ba2f0811f548136
Chain
Robinhood Chain (4663)
Compiler
v0.8.26+commit.8a97fa7a
Optimization
Enabled
Creation tx
0x167d5d466c…6bbf63d03b

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (21)

MAX_ASSETS()uint256
MAX_CLAIM_WAIT()uint256
accountInfo(address)uint256, uint256[], uint256[]
accumulativeRewardOf(uint256, address)uint256
assetAt(uint256)address, uint256, uint256
assetCount()uint256
claimWait()uint256
claimedRewards(uint256, address)uint256
excludedFromRewards(address)bool
holderAt(uint256)address
holderCount()uint256
holdersSlice(uint256, uint256)address[], uint256[]
lastClaimTimes(address)uint256
lastProcessedIndex()uint256
minimumTokenBalanceForRewards()uint256
shares(address)uint256
token()address
totalShares()uint256
totalsAll()address[], uint256[], uint256[]
treasury()address
withdrawableRewardOf(uint256, address)uint256

Events (9)

AssetRegisteredClaimClaimFailedCriteriaUpdatedExcludedFromRewardsProcessedRewardDistributedShareUpdatedTreasurySet

ABI

[
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "minimumTokenBalanceForRewards_",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "claimWait_",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "target",
        "type": "address"
      }
    ],
    "name": "AddressEmptyCode",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "AddressInsufficientBalance",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ClaimWaitTooHigh",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "FailedInnerCall",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "NoShares",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "OnlySelf",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "OnlyToken",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "OnlyTreasury",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ReentrancyGuardReentrantCall",
    "type": "error"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      }
    ],
    "name": "SafeERC20FailedOperation",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "TooManyAssets",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "TreasuryAlreadySet",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "UnknownAsset",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAddress",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ZeroAmount",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "assetId",
        "type": "uint256"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "rewardToken",
        "type": "address"
      }
    ],
    "name": "AssetRegistered",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "account",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "assetId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "bool",
        "name": "automatic",
        "type": "bool"
      }
    ],
    "name": "Claim",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "account",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "assetId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "ClaimFailed",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "minimumTokenBalanceForRewards",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "claimWait",
        "type": "uint256"
      }
    ],
    "name": "CriteriaUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "account",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "bool",
        "name": "excluded",
        "type": "bool"
      }
    ],
    "name": "ExcludedFromRewards",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "iterations",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "claims",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "lastProcessedIndex",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "gas",
        "type": "uint256"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "processor",
        "type": "address"
      }
    ],
    "name": "Processed",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "assetId",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "RewardDistributed",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "account",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "previousShare",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "newShare",
        "type": "uint256"
      }
    ],
    "name": "ShareUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "treasury",
        "type": "address"
      }
    ],
    "name": "TreasurySet",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "MAX_ASSETS",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "MAX_CLAIM_WAIT",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "accountInfo",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "share",
        "type": "uint256"
      },
      {
        "internalType": "uint256[]",
        "name": "withdrawable",
        "type": "uint256[]"
      },
      {
        "internalType": "uint256[]",
        "name": "claimed",
        "type": "uint256[]"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "assetId",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "accumulativeRewardOf",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "assetId",
        "type": "uint256"
      }
    ],
    "name": "assetAt",
    "outputs": [
      {
        "internalType": "address",
        "name": "rewardToken",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "totalDistributed",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "totalClaimed",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "assetCount",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "claim",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "paidAssets",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "claimWait",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "claimedRewards",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "assetId",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "distribute",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "excludedFromRewards",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "index",
        "type": "uint256"
      }
    ],
    "name": "holderAt",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "holderCount",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "start",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "count",
        "type": "uint256"
      }
    ],
    "name": "holdersSlice",
    "outputs": [
      {
        "internalType": "address[]",
        "name": "accounts",
        "type": "address[]"
      },
      {
        "internalType": "uint256[]",
        "name": "shareAmounts",
        "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": "minimumTokenBalanceForRewards",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "gasLimit",
        "type": "uint256"
      }
    ],
    "name": "process",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "iterations",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "claims",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "newLastProcessedIndex",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "rewardToken_",
        "type": "address"
      }
    ],
    "name": "registerAsset",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "assetId",
        "type": "uint256"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "minimumTokenBalanceForRewards_",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "claimWait_",
        "type": "uint256"
      }
    ],
    "name": "setCriteria",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      },
      {
        "internalType": "bool",
        "name": "excluded",
        "type": "bool"
      }
    ],
    "name": "setExcludedFromRewards",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "newShare",
        "type": "uint256"
      }
    ],
    "name": "setShare",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "treasury_",
        "type": "address"
      }
    ],
    "name": "setTreasury",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "shares",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "token",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "totalShares",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "totalsAll",
    "outputs": [
      {
        "internalType": "address[]",
        "name": "tokens",
        "type": "address[]"
      },
      {
        "internalType": "uint256[]",
        "name": "distributed",
        "type": "uint256[]"
      },
      {
        "internalType": "uint256[]",
        "name": "claimed",
        "type": "uint256[]"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "assetId",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "transferReward",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "treasury",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "assetId",
        "type": "uint256"
      },
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      }
    ],
    "name": "withdrawableRewardOf",
    "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 {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

contract BangDistributor is ReentrancyGuard {
    using SafeERC20 for IERC20;

    uint256 private constant MAGNITUDE = 2 ** 128;
    uint256 public constant MAX_ASSETS = 8;
    uint256 public constant MAX_CLAIM_WAIT = 7 days;

    address public immutable token;
    address public treasury;

    struct Asset {
        IERC20 rewardToken;
        uint256 magnifiedRewardPerShare;
        uint256 totalDistributed;
        uint256 totalClaimed;
    }

    Asset[] private _assets;
    mapping(address => uint256) private _assetIdPlusOne;

    mapping(uint256 => mapping(address => int256)) private _corrections;
    mapping(uint256 => mapping(address => uint256)) public claimedRewards;

    uint256 public minimumTokenBalanceForRewards;
    uint256 public claimWait;
    uint256 public totalShares;
    uint256 public lastProcessedIndex;

    mapping(address => uint256) public shares;
    mapping(address => uint256) public lastClaimTimes;
    mapping(address => bool) public excludedFromRewards;

    address[] private holders;
    mapping(address => uint256) private holderIndexPlusOne;

    event TreasurySet(address indexed treasury);
    event AssetRegistered(uint256 indexed assetId, address indexed rewardToken);
    event RewardDistributed(uint256 indexed assetId, uint256 amount);
    event Claim(address indexed account, uint256 indexed assetId, uint256 amount, bool automatic);
    event ClaimFailed(address indexed account, uint256 indexed assetId, uint256 amount);
    event ShareUpdated(address indexed account, uint256 previousShare, uint256 newShare);
    event ExcludedFromRewards(address indexed account, bool excluded);
    event CriteriaUpdated(uint256 minimumTokenBalanceForRewards, uint256 claimWait);
    event Processed(uint256 iterations, uint256 claims, uint256 lastProcessedIndex, uint256 gas, address indexed processor);

    error OnlyToken();
    error OnlyTreasury();
    error OnlySelf();
    error ZeroAddress();
    error TreasuryAlreadySet();
    error TooManyAssets();
    error UnknownAsset();
    error ZeroAmount();
    error NoShares();
    error ClaimWaitTooHigh();

    modifier onlyToken() {
        if (msg.sender != token) revert OnlyToken();
        _;
    }

    modifier onlyTreasury() {
        if (msg.sender != treasury) revert OnlyTreasury();
        _;
    }

    constructor(uint256 minimumTokenBalanceForRewards_, uint256 claimWait_) {
        if (claimWait_ > MAX_CLAIM_WAIT) revert ClaimWaitTooHigh();
        token = msg.sender;
        minimumTokenBalanceForRewards = minimumTokenBalanceForRewards_;
        claimWait = claimWait_;
    }

    function setTreasury(address treasury_) external onlyToken {
        if (treasury_ == address(0)) revert ZeroAddress();
        if (treasury != address(0)) revert TreasuryAlreadySet();
        treasury = treasury_;
        emit TreasurySet(treasury_);
    }

    function registerAsset(address rewardToken_) external onlyTreasury returns (uint256 assetId) {
        if (rewardToken_ == address(0)) revert ZeroAddress();
        uint256 idPlusOne = _assetIdPlusOne[rewardToken_];
        if (idPlusOne != 0) return idPlusOne - 1;
        if (_assets.length >= MAX_ASSETS) revert TooManyAssets();
        _assets.push(
            Asset({rewardToken: IERC20(rewardToken_), magnifiedRewardPerShare: 0, totalDistributed: 0, totalClaimed: 0})
        );
        assetId = _assets.length - 1;
        _assetIdPlusOne[rewardToken_] = assetId + 1;
        emit AssetRegistered(assetId, rewardToken_);
    }

    function assetCount() external view returns (uint256) {
        return _assets.length;
    }

    function assetAt(uint256 assetId)
        external
        view
        returns (address rewardToken, uint256 totalDistributed, uint256 totalClaimed)
    {
        Asset storage a = _assets[assetId];
        return (address(a.rewardToken), a.totalDistributed, a.totalClaimed);
    }

    function totalsAll()
        external
        view
        returns (address[] memory tokens, uint256[] memory distributed, uint256[] memory claimed)
    {
        uint256 n = _assets.length;
        tokens = new address[](n);
        distributed = new uint256[](n);
        claimed = new uint256[](n);
        for (uint256 i = 0; i < n; i++) {
            tokens[i] = address(_assets[i].rewardToken);
            distributed[i] = _assets[i].totalDistributed;
            claimed[i] = _assets[i].totalClaimed;
        }
    }

    function accountInfo(address account)
        external
        view
        returns (uint256 share, uint256[] memory withdrawable, uint256[] memory claimed)
    {
        uint256 n = _assets.length;
        share = shares[account];
        withdrawable = new uint256[](n);
        claimed = new uint256[](n);
        for (uint256 i = 0; i < n; i++) {
            withdrawable[i] = withdrawableRewardOf(i, account);
            claimed[i] = claimedRewards[i][account];
        }
    }

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

    function holderAt(uint256 index) external view returns (address) {
        return holders[index];
    }

    function holdersSlice(uint256 start, uint256 count)
        external
        view
        returns (address[] memory accounts, uint256[] memory shareAmounts)
    {
        uint256 n = holders.length;
        if (start >= n) return (new address[](0), new uint256[](0));
        uint256 end = start + count;
        if (end > n) end = n;
        accounts = new address[](end - start);
        shareAmounts = new uint256[](end - start);
        for (uint256 i = start; i < end; i++) {
            accounts[i - start] = holders[i];
            shareAmounts[i - start] = shares[holders[i]];
        }
    }

    function withdrawableRewardOf(uint256 assetId, address account) public view returns (uint256) {
        uint256 accumulative = accumulativeRewardOf(assetId, account);
        uint256 claimed = claimedRewards[assetId][account];
        if (accumulative <= claimed) return 0;
        return accumulative - claimed;
    }

    function accumulativeRewardOf(uint256 assetId, address account) public view returns (uint256) {
        int256 corrected =
            int256(_assets[assetId].magnifiedRewardPerShare * shares[account]) + _corrections[assetId][account];
        if (corrected <= 0) return 0;
        return uint256(corrected) / MAGNITUDE;
    }

    function setCriteria(uint256 minimumTokenBalanceForRewards_, uint256 claimWait_) external onlyToken {
        if (claimWait_ > MAX_CLAIM_WAIT) revert ClaimWaitTooHigh();
        minimumTokenBalanceForRewards = minimumTokenBalanceForRewards_;
        claimWait = claimWait_;
        emit CriteriaUpdated(minimumTokenBalanceForRewards_, claimWait_);
    }

    function setExcludedFromRewards(address account, bool excluded) external onlyToken {
        excludedFromRewards[account] = excluded;
        if (excluded) {
            _setShare(account, 0);
        }
        emit ExcludedFromRewards(account, excluded);
    }

    function setShare(address account, uint256 newShare) external onlyToken {
        if (excludedFromRewards[account] || newShare < minimumTokenBalanceForRewards) {
            newShare = 0;
        }
        _setShare(account, newShare);
    }

    function distribute(uint256 assetId, uint256 amount) external onlyTreasury {
        if (assetId >= _assets.length) revert UnknownAsset();
        if (amount == 0) revert ZeroAmount();
        if (totalShares == 0) revert NoShares();
        Asset storage a = _assets[assetId];
        a.magnifiedRewardPerShare += (amount * MAGNITUDE) / totalShares;
        a.totalDistributed += amount;
        emit RewardDistributed(assetId, amount);
    }

    function claim(address account) external onlyToken nonReentrant returns (uint256 paidAssets) {
        return _processAccount(account, false);
    }

    function process(uint256 gasLimit)
        external
        onlyToken
        nonReentrant
        returns (uint256 iterations, uint256 claims, uint256 newLastProcessedIndex)
    {
        uint256 numberOfHolders = holders.length;
        if (numberOfHolders == 0) {
            return (0, 0, lastProcessedIndex);
        }

        uint256 gasUsed = 0;
        uint256 gasLeft = gasleft();
        uint256 localIndex = lastProcessedIndex;

        while (gasUsed < gasLimit && iterations < numberOfHolders) {
            localIndex++;
            if (localIndex >= numberOfHolders) {
                localIndex = 0;
            }

            address account = holders[localIndex];
            if (_canAutoClaim(account)) {
                if (_processAccount(account, true) > 0) {
                    claims++;
                }
            }

            iterations++;
            uint256 newGasLeft = gasleft();
            if (gasLeft > newGasLeft) {
                gasUsed += gasLeft - newGasLeft;
            }
            gasLeft = newGasLeft;
        }

        lastProcessedIndex = localIndex;
        emit Processed(iterations, claims, lastProcessedIndex, gasLimit, tx.origin);
        return (iterations, claims, lastProcessedIndex);
    }

    function transferReward(uint256 assetId, address account, uint256 amount) external {
        if (msg.sender != address(this)) revert OnlySelf();
        _assets[assetId].rewardToken.safeTransfer(account, amount);
    }

    function _processAccount(address account, bool automatic) internal returns (uint256 paidAssets) {
        uint256 n = _assets.length;
        bool paidAny = false;

        for (uint256 assetId = 0; assetId < n; assetId++) {
            uint256 amount = withdrawableRewardOf(assetId, account);
            if (amount == 0) continue;

            Asset storage a = _assets[assetId];

            claimedRewards[assetId][account] += amount;
            a.totalClaimed += amount;

            if (automatic) {
                try this.transferReward(assetId, account, amount) {
                    emit Claim(account, assetId, amount, true);
                    paidAssets++;
                    paidAny = true;
                } catch {
                    claimedRewards[assetId][account] -= amount;
                    a.totalClaimed -= amount;
                    emit ClaimFailed(account, assetId, amount);
                }
            } else {
                // C-1: manual claim is now isolated per-asset too — a frozen/reverting reward token skips
                // that asset instead of reverting the holder's entire claim of all 8 assets.
                try this.transferReward(assetId, account, amount) {
                    emit Claim(account, assetId, amount, false);
                    paidAssets++;
                    paidAny = true;
                } catch {
                    claimedRewards[assetId][account] -= amount;
                    a.totalClaimed -= amount;
                    emit ClaimFailed(account, assetId, amount);
                }
            }
        }

        if (paidAny) {
            lastClaimTimes[account] = block.timestamp;
        }
    }

    function _canAutoClaim(address account) internal view returns (bool) {
        if (claimWait == 0) return true;
        uint256 lastClaimTime = lastClaimTimes[account];
        if (lastClaimTime == 0) return true;
        return block.timestamp >= lastClaimTime + claimWait;
    }

    function _setShare(address account, uint256 newShare) internal {
        uint256 currentShare = shares[account];
        if (currentShare == newShare) return;

        uint256 n = _assets.length;
        if (newShare > currentShare) {
            uint256 mintAmount = newShare - currentShare;
            totalShares += mintAmount;
            for (uint256 i = 0; i < n; i++) {
                uint256 mps = _assets[i].magnifiedRewardPerShare;
                if (mps != 0) _corrections[i][account] -= int256(mps * mintAmount);
            }
        } else {
            uint256 burnAmount = currentShare - newShare;
            totalShares -= burnAmount;
            for (uint256 i = 0; i < n; i++) {
                uint256 mps = _assets[i].magnifiedRewardPerShare;
                if (mps != 0) _corrections[i][account] += int256(mps * burnAmount);
            }
        }

        shares[account] = newShare;

        if (newShare == 0) {
            _removeHolder(account);
        } else {
            _addHolder(account);
        }

        emit ShareUpdated(account, currentShare, newShare);
    }

    function _addHolder(address account) internal {
        if (holderIndexPlusOne[account] != 0) return;
        holders.push(account);
        holderIndexPlusOne[account] = holders.length;
    }

    function _removeHolder(address account) internal {
        uint256 indexPlusOne = holderIndexPlusOne[account];
        if (indexPlusOne == 0) return;

        uint256 index = indexPlusOne - 1;
        uint256 lastIndex = holders.length - 1;
        if (index != lastIndex) {
            address lastHolder = holders[lastIndex];
            holders[index] = lastHolder;
            holderIndexPlusOne[lastHolder] = index + 1;
        }
        holders.pop();
        delete holderIndexPlusOne[account];

        if (holders.length == 0) {
            lastProcessedIndex = 0;
        } else if (lastProcessedIndex >= holders.length) {
            lastProcessedIndex = holders.length - 1;
        }
    }
}
Chain explorer1722msChain node90ms