HolderDistributor

0x2fa0a7654ffaa37251688e8db4fcac656911245e

Verification
Verified
v0.8.28+commit.7893614a
Type
Contract
3,486 bytes
ABI entries
19
8 read · 4 write
License
none

Contract information

Address
0x2fa0a7654ffaa37251688e8db4fcac656911245e
Chain
Robinhood Chain (4663)
Compiler
v0.8.28+commit.7893614a
Optimization
Enabled
Creation tx
0xb1a7f7a561…ca74051020

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (8)

WETH()address
claimed(address, address, address)uint256
epoch(address)uint256
merkleRoot(address)bytes32
owner()address
pool(address, address)uint256
poolBalance(address, address)uint256
totalClaimed(address, address)uint256

Events (4)

DepositedHolderPaidOwnerUpdatedRootUpdated

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "_owner",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "_weth",
        "type": "address"
      }
    ],
    "stateMutability": "nonpayable",
    "type": "constructor"
  },
  {
    "inputs": [],
    "name": "ReentrancyGuardReentrantCall",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "asset",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "Deposited",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "account",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "ethAmount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "tokenAmount",
        "type": "uint256"
      }
    ],
    "name": "HolderPaid",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "owner",
        "type": "address"
      }
    ],
    "name": "OwnerUpdated",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "uint256",
        "name": "epoch",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "bytes32",
        "name": "root",
        "type": "bytes32"
      }
    ],
    "name": "RootUpdated",
    "type": "event"
  },
  {
    "inputs": [],
    "name": "WETH",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "account",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "cumulativeEth",
        "type": "uint256"
      },
      {
        "internalType": "uint256",
        "name": "cumulativeToken",
        "type": "uint256"
      },
      {
        "internalType": "bytes32[]",
        "name": "proof",
        "type": "bytes32[]"
      }
    ],
    "name": "claim",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "claimed",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "asset",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "deposit",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "epoch",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "merkleRoot",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "pool",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "asset",
        "type": "address"
      }
    ],
    "name": "poolBalance",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "bytes32",
        "name": "root",
        "type": "bytes32"
      }
    ],
    "name": "postRoot",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "setOwner",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "name": "totalClaimed",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "stateMutability": "payable",
    "type": "receive"
  }
]

Source code

// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;

import "@openzeppelin/contracts/utils/cryptography/MerkleProof.sol";
import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";

interface IWETH9 {
    function withdraw(uint256) external;
    function balanceOf(address) external view returns (uint256);
}

interface IERC20Min {
    function transfer(address, uint256) external returns (bool);
    function transferFrom(address, address, uint256) external returns (bool);
    function balanceOf(address) external view returns (uint256);
}

/**
 * @title HolderDistributor
 * @notice Dual-asset (ETH-via-WETH + the launch token) cumulative Merkle distributor for the
 *         25% holder slice of xpad V3 swap fees. Modelled on PostGradRewards: per-token, per-asset
 *         solvency; cumulative leaves; updatable roots; NO owner withdraw. `claim` is
 *         account-parameterized and pays the leaf `account` (never msg.sender), so a keeper can
 *         auto-submit it for every holder (0-click) yet can never redirect funds; a holder can
 *         also self-claim if the keeper is offline. The WETH slice is unwrapped to native ETH.
 */
contract HolderDistributor is ReentrancyGuard {
    address public owner;              // root poster (keeper/treasury key)
    address public immutable WETH;

    mapping(address => mapping(address => uint256)) public pool;         // token => asset => deposited
    mapping(address => mapping(address => uint256)) public totalClaimed; // token => asset => claimed
    mapping(address => mapping(address => mapping(address => uint256))) public claimed; // token => account => asset => cumulative claimed
    mapping(address => bytes32) public merkleRoot; // token => root
    mapping(address => uint256) public epoch;      // token => epoch

    event Deposited(address indexed token, address indexed asset, uint256 amount);
    event RootUpdated(address indexed token, uint256 indexed epoch, bytes32 root);
    event HolderPaid(address indexed token, address indexed account, uint256 ethAmount, uint256 tokenAmount);
    event OwnerUpdated(address indexed owner);

    modifier onlyOwner() { require(msg.sender == owner, "HD: not owner"); _; }

    constructor(address _owner, address _weth) {
        require(_owner != address(0) && _weth != address(0), "HD: zero");
        owner = _owner;
        WETH = _weth;
        emit OwnerUpdated(_owner);
    }

    /// @dev receives native ETH only from WETH.withdraw during a claim.
    receive() external payable {}

    // ─── Funding: permissionless, pulls real funds, credits the real received delta ───
    function deposit(address token, address asset, uint256 amount) external nonReentrant {
        require(token != address(0) && asset != address(0), "HD: zero");
        require(amount > 0, "HD: zero amount");
        uint256 before = IERC20Min(asset).balanceOf(address(this));
        require(IERC20Min(asset).transferFrom(msg.sender, address(this), amount), "HD: pull failed");
        uint256 received = IERC20Min(asset).balanceOf(address(this)) - before;
        pool[token][asset] += received;
        emit Deposited(token, asset, received);
    }

    // ─── Admin ───
    function postRoot(address token, bytes32 root) external onlyOwner {
        require(root != bytes32(0), "HD: zero root");
        uint256 e = epoch[token] + 1;
        epoch[token] = e;
        merkleRoot[token] = root;
        emit RootUpdated(token, e, root);
    }

    function setOwner(address newOwner) external onlyOwner {
        require(newOwner != address(0), "HD: zero");
        owner = newOwner;
        emit OwnerUpdated(newOwner);
    }

    // ─── Claim: pays the leaf `account`, callable by anyone (keeper auto-submits) ───
    function claim(
        address token,
        address account,
        uint256 cumulativeEth,
        uint256 cumulativeToken,
        bytes32[] calldata proof
    ) external nonReentrant {
        bytes32 leaf = keccak256(bytes.concat(keccak256(abi.encode(account, cumulativeEth, cumulativeToken))));
        require(MerkleProof.verify(proof, merkleRoot[token], leaf), "HD: bad proof");

        uint256 ethDelta = _accrue(token, WETH, account, cumulativeEth);
        uint256 tokDelta = _accrue(token, token, account, cumulativeToken);
        require(ethDelta > 0 || tokDelta > 0, "HD: nothing to claim");

        if (ethDelta > 0) {
            IWETH9(WETH).withdraw(ethDelta);
            (bool ok, ) = payable(account).call{value: ethDelta}("");
            require(ok, "HD: eth send failed");
        }
        if (tokDelta > 0) {
            require(IERC20Min(token).transfer(account, tokDelta), "HD: token send failed");
        }
        emit HolderPaid(token, account, ethDelta, tokDelta);
    }

    /// @dev effects for one asset: bump cumulative-claimed and enforce per-token solvency.
    function _accrue(address token, address asset, address account, uint256 cumulative) private returns (uint256 delta) {
        uint256 already = claimed[token][account][asset];
        if (cumulative <= already) return 0;
        delta = cumulative - already;
        require(totalClaimed[token][asset] + delta <= pool[token][asset], "HD: pool exhausted");
        claimed[token][account][asset] = cumulative;
        totalClaimed[token][asset] += delta;
    }

    // ─── Views ───
    function poolBalance(address token, address asset) external view returns (uint256) {
        return pool[token][asset] - totalClaimed[token][asset];
    }
}
Chain explorer5660msChain node92ms