Distributor

0xb726088be96c36f4e058853f62a226cf39b431b8

Verification
Unverified
v0.8.26+commit.8a97fa7a
Type
Contract
4,414 bytes
ABI entries
19
14 read · 4 write
License
none

Contract information

Address
0xb726088be96c36f4e058853f62a226cf39b431b8
Chain
Robinhood Chain (4663)
Compiler
v0.8.26+commit.8a97fa7a
Optimization
Enabled
Creation tx
0xfa68dfe9ea…836f699a23

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (14)

WETH()address
dividendsPerShare()uint256
dividendsPerShareAccuracyFactor()uint256
getUnpaidEarnings(address)uint256
maxSlippageBps()uint256
minDistribution()uint256
minPeriod()uint256
rewardToken()address
shareholderClaims(address)uint256
shares(address)uint256, uint256, uint256
totalDistributed()uint256
totalDividends()uint256
totalShares()uint256
unclaimed(address)uint256

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "_router",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "_rewardToken",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "WETH",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "claimDividend",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "deposit",
    "outputs": [],
    "stateMutability": "payable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "dividendsPerShare",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "dividendsPerShareAccuracyFactor",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "shareholder",
        "type": "address"
      }
    ],
    "name": "getUnpaidEarnings",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "maxSlippageBps",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "minDistribution",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "minPeriod",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "uint256",
        "name": "gas",
        "type": "uint256"
      }
    ],
    "name": "process",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "rewardToken",
    "outputs": [
      {
        "internalType": "contract IERC20Full",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "shareholder",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "setShare",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "shareholderClaims",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "shares",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "totalExcluded",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "totalRealised",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "totalDistributed",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "totalDividends",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "totalShares",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "unclaimed",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  }
]

Source code

/**
SPDX-License-Identifier: Unlicensed
*/

/**
 * Reward-bearing ERC-20.
 *
 * Transfers carry NO fee of any kind: `amount` sent is `amount` received,
 * always. Holders accrue dividends in a reward token, funded externally
 * via the distributor's public `deposit()` (e.g. by a liquidity-fee
 * harvester). There are no owner or admin functions, no fee setters, no
 * transfer limits, no blacklist, and no way to withdraw contract funds.
 * Nothing here can be changed after deployment.
 */

pragma solidity ^0.8.24;

library SafeMath {
    function add(uint256 a, uint256 b) internal pure returns (uint256) { return a + b; }
    function sub(uint256 a, uint256 b) internal pure returns (uint256) { return a - b; }
    function mul(uint256 a, uint256 b) internal pure returns (uint256) { return a * b; }
    function div(uint256 a, uint256 b) internal pure returns (uint256) { return a / b; }

    function sub(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {
        unchecked {
            require(b <= a, errorMessage);
            return a - b;
        }
    }
}

interface IERC20Full {
    function totalSupply() external view returns (uint256);
    function decimals() external view returns (uint8);
    function symbol() external view returns (string memory);
    function name() external view returns (string memory);
    function getOwner() external view returns (address);
    function balanceOf(address account) external view returns (uint256);
    function transfer(address recipient, uint256 amount) external returns (bool);
    function allowance(address _owner, address spender) external view returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
    function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
    event Transfer(address indexed from, address indexed to, uint256 value);
    event Approval(address indexed owner, address indexed spender, uint256 value);
}

interface IDEXRouter {
    function WETH() external pure returns (address);
    function factory() external view returns (address);
    function swapExactETHForTokensSupportingFeeOnTransferTokens(
        uint amountOutMin,
        address[] calldata path,
        address to,
        uint deadline
    ) external payable;
}

interface IDEXFactoryView {
    function getPair(address tokenA, address tokenB) external view returns (address);
}

interface IDEXPair {
    function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast);
    function token0() external view returns (address);
}

interface IDividendDistributor {
    function setShare(address shareholder, uint256 amount) external;
    function deposit() external payable;
    function process(uint256 gas) external;
}

contract Distributor is IDividendDistributor {
    using SafeMath for uint256;

    address _token;

    struct Share {
        uint256 amount;
        uint256 totalExcluded; // excluded dividend
        uint256 totalRealised;
    }

    IERC20Full public rewardToken;
    address public WETH;
    IDEXRouter router;

    address[] shareholders;
    mapping (address => uint256) shareholderIndexes;
    mapping (address => uint256) public shareholderClaims;

    mapping (address => Share) public shares;

    // Dividends accrued to a holder and settled into a claimable balance when
    // their token balance changed. Banking here (instead of transferring
    // during a balance change) keeps setShare free of external calls, so a
    // reward-token failure can never leave ERC20 balances updated while
    // dividend shares go stale. Paid out only in the claim/process path.
    mapping (address => uint256) public unclaimed;

    uint256 public totalShares;
    uint256 public totalDividends;
    uint256 public totalDistributed; // to be shown in UI
    uint256 public dividendsPerShare;
    uint256 public dividendsPerShareAccuracyFactor = 10 ** 36;

    uint256 public minPeriod = 1 hours;
    // Denominated in reward-token units; set in the constructor to 1 whole
    // reward token (10 ** rewardToken.decimals()).
    uint256 public minDistribution;

    uint256 currentIndex;

    modifier onlyToken() {
        require(msg.sender == _token); _;
    }

    constructor (address _router, address _rewardToken) {
        require(_router != address(0), "router is zero address");
        require(_rewardToken != address(0), "reward token is zero address");
        router = IDEXRouter(_router);
        rewardToken = IERC20Full(_rewardToken);
        WETH = router.WETH();
        minDistribution = 10 ** IERC20Full(_rewardToken).decimals();
        _token = msg.sender;
    }

    function setShare(address shareholder, uint256 amount) external override onlyToken {
        // Settle accrued dividends into the claimable balance WITHOUT any
        // external call, so this function is deterministic and cannot revert
        // on a reward-token failure (which would otherwise desync shares from
        // the ERC20 balance in the token's try/catch caller).
        if(shares[shareholder].amount > 0){
            uint256 accrued = _accrued(shareholder);
            if(accrued > 0){ unclaimed[shareholder] = unclaimed[shareholder].add(accrued); }
        }

        if(amount > 0 && shares[shareholder].amount == 0){
            addShareholder(shareholder);
        }else if(amount == 0 && shares[shareholder].amount > 0){
            removeShareholder(shareholder);
        }

        totalShares = totalShares.sub(shares[shareholder].amount).add(amount);
        shares[shareholder].amount = amount;
        shares[shareholder].totalExcluded = getCumulativeDividends(shares[shareholder].amount);
    }

    // Max slippage tolerated on the ETH->reward swap, in basis points of the
    // reserve-implied fair output. Bounds sandwich MEV: a swap that returns
    // less than this reverts (and the caller rolls the deposit to a later
    // attempt) rather than filling at an attacker-moved price.
    uint256 public constant maxSlippageBps = 1000; // 10%

    // Anyone may fund dividends: sent ETH is swapped to the reward token and
    // credited pro-rata to all current shareholders. Reverts when there are
    // no shareholders so funds are never stranded.
    function deposit() external payable override {
        require(totalShares > 0, "no shareholders");

        uint256 balanceBefore = rewardToken.balanceOf(address(this));

        address[] memory path = new address[](2);
        path[0] = WETH;
        path[1] = address(rewardToken);

        router.swapExactETHForTokensSupportingFeeOnTransferTokens{value: msg.value}(
            _minRewardOut(msg.value),
            path,
            address(this),
            block.timestamp
        );

        uint256 amount = rewardToken.balanceOf(address(this)).sub(balanceBefore);

        totalDividends = totalDividends.add(amount);
        dividendsPerShare = dividendsPerShare.add(dividendsPerShareAccuracyFactor.mul(amount).div(totalShares));
    }

    // Fair-output floor for `ethIn` worth of WETH->reward, derived from the
    // canonical V2 pair's current reserves (constant-product with the 0.3%
    // fee), discounted by maxSlippageBps. Returns 0 if the pair is missing or
    // empty so the swap can still proceed (the harvest caller wraps deposit in
    // try/catch, so a bad fill reverts and rolls forward rather than locking).
    function _minRewardOut(uint256 ethIn) internal view returns (uint256) {
        address pair = IDEXFactoryView(router.factory()).getPair(WETH, address(rewardToken));
        if (pair == address(0)) return 0;
        (uint112 r0, uint112 r1, ) = IDEXPair(pair).getReserves();
        (uint256 reserveIn, uint256 reserveOut) =
            IDEXPair(pair).token0() == WETH ? (uint256(r0), uint256(r1)) : (uint256(r1), uint256(r0));
        if (reserveIn == 0 || reserveOut == 0) return 0;
        uint256 amountInWithFee = ethIn.mul(997);
        uint256 expectedOut = amountInWithFee.mul(reserveOut) / (reserveIn.mul(1000).add(amountInWithFee));
        return expectedOut.mul(10000 - maxSlippageBps) / 10000;
    }

    function process(uint256 gas) external override onlyToken {
        uint256 shareholderCount = shareholders.length;

        if(shareholderCount == 0) { return; }

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

        uint256 iterations = 0;

        while(gasUsed < gas && iterations < shareholderCount) {
            if(currentIndex >= shareholderCount){
                currentIndex = 0;
            }

            if(shouldDistribute(shareholders[currentIndex])){
                _distribute(shareholders[currentIndex]);
            }

            gasUsed = gasUsed.add(gasLeft.sub(gasleft()));
            gasLeft = gasleft();
            currentIndex++;
            iterations++;
        }
    }

    function shouldDistribute(address shareholder) internal view returns (bool) {
        return shareholderClaims[shareholder] + minPeriod < block.timestamp
        && getUnpaidEarnings(shareholder) > minDistribution;
    }

    function _distribute(address shareholder) internal {
        uint256 amount = getUnpaidEarnings(shareholder); // banked + accrued
        if(amount == 0){ return; }

        // Effects before interaction: zero the claim before paying, so a
        // reentrant call during the transfer sees nothing owed.
        unclaimed[shareholder] = 0;
        shares[shareholder].totalExcluded = getCumulativeDividends(shares[shareholder].amount);

        if(_pay(shareholder, amount)){
            shareholderClaims[shareholder] = block.timestamp;
            shares[shareholder].totalRealised = shares[shareholder].totalRealised.add(amount);
            totalDistributed = totalDistributed.add(amount);
        } else {
            // Failed or frozen recipient: re-bank the full amount so it stays
            // claimable later. One bad holder is skipped, never bricking the
            // payout loop for everyone else.
            unclaimed[shareholder] = amount;
        }
    }

    // Non-reverting reward payout: a revert-style freeze (how real stablecoin
    // blacklists behave) is treated the same as a false return.
    function _pay(address shareholder, uint256 amount) internal returns (bool) {
        try rewardToken.transfer(shareholder, amount) returns (bool ok) {
            return ok;
        } catch {
            return false;
        }
    }

    function claimDividend() external {
        _distribute(msg.sender);
    }

    // Dividends still accruing on the current share balance (excludes banked).
    function _accrued(address shareholder) internal view returns (uint256) {
        if(shares[shareholder].amount == 0){ return 0; }
        uint256 total = getCumulativeDividends(shares[shareholder].amount);
        uint256 excluded = shares[shareholder].totalExcluded;
        if(total <= excluded){ return 0; }
        return total.sub(excluded);
    }

    function getUnpaidEarnings(address shareholder) public view returns (uint256) {
        return unclaimed[shareholder].add(_accrued(shareholder));
    }

    function getCumulativeDividends(uint256 share) internal view returns (uint256) {
        return share.mul(dividendsPerShare).div(dividendsPerShareAccuracyFactor);
    }

    function addShareholder(address shareholder) internal {
        shareholderIndexes[shareholder] = shareholders.length;
        shareholders.push(shareholder);
    }

    function removeShareholder(address shareholder) internal {
        shareholders[shareholderIndexes[shareholder]] = shareholders[shareholders.length-1];
        shareholderIndexes[shareholders[shareholders.length-1]] = shareholderIndexes[shareholder];
        shareholders.pop();
    }
}

contract Token is IERC20Full {
    using SafeMath for uint256;

    address constant DEAD = 0x000000000000000000000000000000000000dEaD;
    address constant ZERO = 0x0000000000000000000000000000000000000000;

    string _name;
    string _symbol;
    uint8 constant _decimals = 9;

    uint256 constant _totalSupply = 1_000_000_000_000_000 * (10 ** _decimals);

    mapping (address => uint256) _balances;
    mapping (address => mapping (address => uint256)) _allowances;

    // Infrastructure addresses (pool singleton, position manager, routers,
    // fee harvester) hold tokens in custody, not as investors; they are
    // excluded from dividends and the wallet cap at construction and this
    // can never change.
    mapping (address => bool) public isDividendExempt;
    mapping (address => bool) public isCapExempt;

    // Anti-whale wallet cap: no wallet may accumulate more than 2.5% of
    // supply. Wallets already above the cap (migration airdrops) can only
    // reduce their balance until they are under it.
    //
    // The deployer (airdropSource) may deliver over-cap balances so the
    // migration airdrop can seed holders above 2.5%. This bypass is NOT
    // permanent: once completeAirdrop() is called it is disabled forever and
    // the deployer becomes an ordinary, cap-bound wallet. The launch pipeline
    // calls completeAirdrop() immediately after the airdrop finishes.
    uint256 public constant maxWalletBps = 250;
    address public immutable airdropSource;
    bool public airdropComplete;

    Distributor distributor;
    address public distributorAddress;

    uint256 constant distributorGas = 500000;

    constructor (
        string memory name_,
        string memory symbol_,
        address _dexRouter,
        address _rewardToken,
        address[] memory _dividendExempt
    ) {
        _name = name_;
        _symbol = symbol_;

        distributor = new Distributor(_dexRouter, _rewardToken);
        distributorAddress = address(distributor);

        isDividendExempt[address(this)] = true;
        isDividendExempt[DEAD] = true;
        isCapExempt[address(this)] = true;
        isCapExempt[DEAD] = true;
        for (uint256 i = 0; i < _dividendExempt.length; i++) {
            isDividendExempt[_dividendExempt[i]] = true;
            isCapExempt[_dividendExempt[i]] = true;
        }

        airdropSource = msg.sender;

        _balances[msg.sender] = _totalSupply;
        emit Transfer(address(0), msg.sender, _totalSupply);
    }

    function maxWalletAmount() public pure returns (uint256) {
        return _totalSupply * maxWalletBps / 10000;
    }

    // One-way switch: permanently ends the deployer's cap bypass. Idempotent,
    // callable only by the deployer. After this the deployer is cap-bound like
    // any other wallet and no address can ever deliver over-cap balances.
    function completeAirdrop() external {
        require(msg.sender == airdropSource, "not airdrop source");
        airdropComplete = true;
    }

    receive() external payable { }

    function totalSupply() external pure override returns (uint256) { return _totalSupply; }
    function decimals() external pure override returns (uint8) { return _decimals; }
    function symbol() external view override returns (string memory) { return _symbol; }
    function name() external view override returns (string memory) { return _name; }
    // No owner or admin functions exist anywhere in this contract.
    function getOwner() external pure override returns (address) { return address(0); }
    function balanceOf(address account) public view override returns (uint256) { return _balances[account]; }
    function allowance(address holder, address spender) external view override returns (uint256) { return _allowances[holder][spender]; }

    function approve(address spender, uint256 amount) public override returns (bool) {
        _allowances[msg.sender][spender] = amount;
        emit Approval(msg.sender, spender, amount);
        return true;
    }

    function approveMax(address spender) external returns (bool) {
        return approve(spender, type(uint256).max);
    }

    function transfer(address recipient, uint256 amount) external override returns (bool) {
        return _transferFrom(msg.sender, recipient, amount);
    }

    function transferFrom(address sender, address recipient, uint256 amount) external override returns (bool) {
        uint256 allowed = _allowances[sender][msg.sender];
        // standard infinite-approval sentinel; every finite allowance is consumed
        if (allowed != type(uint256).max) {
            _allowances[sender][msg.sender] = allowed.sub(amount, "Insufficient Allowance");
        }
        return _transferFrom(sender, recipient, amount);
    }

    function _transferFrom(address sender, address recipient, uint256 amount) internal returns (bool) {
        _balances[sender] = _balances[sender].sub(amount, "Insufficient Balance");

        bool airdropBypass = (sender == airdropSource && !airdropComplete);
        if (!isCapExempt[recipient] && !airdropBypass) {
            require(_balances[recipient].add(amount) <= maxWalletAmount(), "wallet cap exceeded");
        }
        _balances[recipient] = _balances[recipient].add(amount);

        if(!isDividendExempt[sender]){ try distributor.setShare(sender, _balances[sender]) {} catch {} }
        if(!isDividendExempt[recipient]){ try distributor.setShare(recipient, _balances[recipient]) {} catch {} }

        try distributor.process(distributorGas) {} catch {}

        emit Transfer(sender, recipient, amount);
        return true;
    }

    function getCirculatingSupply() public view returns (uint256) {
        return _totalSupply.sub(balanceOf(DEAD)).sub(balanceOf(ZERO));
    }
}
Chain explorer3370msChain node92ms