Distributor

0x011bbcf5913da7fe77889824b9cb628e68cf9ca8

Verification
Verified
v0.8.20+commit.a1b79de6
Type
Contract
5,949 bytes
ABI entries
40
8 read · 9 write
License
none

Contract information

Address
0x011bbcf5913da7fe77889824b9cb628e68cf9ca8
Chain
Robinhood Chain (4663)
Compiler
v0.8.20+commit.a1b79de6
Optimization
Enabled
Creation tx
0x5124a8505c…3b326a5e84

Token

Not a token

This contract does not expose ERC-20 metadata.

Read contract (8)

claimed(address, address, address, bytes)uint256
getUnclaimedAmount(address, address, address, uint256, bytes)uint256
isHarvested(address, address, address, uint256, bytes)bool
merkleUpdater()address
owner()address
paused()bool
root()bytes32
wNative()address

Events (7)

ClaimedDistributorPausedDistributorUnpausedMerkleUpdaterChangedOwnershipTransferredRecoveredERC20RootUpdated

ABI

[
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "_owner",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "_updater",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "_wNative",
        "type": "address"
      }
    ],
    "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": "AlreadyClaimed",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "ContractIsPaused",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "FailedInnerCall",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "FailedToSendNative",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidLengths",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidMerkleUpdater",
    "type": "error"
  },
  {
    "inputs": [],
    "name": "InvalidProof",
    "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": "Unauthorized",
    "type": "error"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": true,
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "pool",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "accAmount",
        "type": "uint256"
      },
      {
        "indexed": false,
        "internalType": "bytes",
        "name": "identifier",
        "type": "bytes"
      }
    ],
    "name": "Claimed",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [],
    "name": "DistributorPaused",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [],
    "name": "DistributorUnpaused",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "address",
        "name": "old",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "address",
        "name": "merkleUpdater",
        "type": "address"
      }
    ],
    "name": "MerkleUpdaterChanged",
    "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": "to",
        "type": "address"
      },
      {
        "indexed": true,
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "indexed": false,
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "RecoveredERC20",
    "type": "event"
  },
  {
    "anonymous": false,
    "inputs": [
      {
        "indexed": false,
        "internalType": "bytes32",
        "name": "root",
        "type": "bytes32"
      }
    ],
    "name": "RootUpdated",
    "type": "event"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "pool",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "bytes",
        "name": "identifier",
        "type": "bytes"
      }
    ],
    "name": "claimed",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "pool",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      },
      {
        "internalType": "bytes",
        "name": "identifier",
        "type": "bytes"
      }
    ],
    "name": "getUnclaimedAmount",
    "outputs": [
      {
        "internalType": "uint256",
        "name": "",
        "type": "uint256"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "pool",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      },
      {
        "internalType": "bytes",
        "name": "identifier",
        "type": "bytes"
      },
      {
        "internalType": "bytes32[]",
        "name": "proof",
        "type": "bytes32[]"
      }
    ],
    "name": "harvest",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "pool",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      },
      {
        "internalType": "bytes",
        "name": "identifier",
        "type": "bytes"
      }
    ],
    "name": "isHarvested",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "merkleUpdater",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "user",
        "type": "address"
      },
      {
        "internalType": "address[]",
        "name": "pools",
        "type": "address[]"
      },
      {
        "internalType": "address[]",
        "name": "tokens",
        "type": "address[]"
      },
      {
        "internalType": "uint256[]",
        "name": "amounts",
        "type": "uint256[]"
      },
      {
        "internalType": "bytes[]",
        "name": "identifiers",
        "type": "bytes[]"
      },
      {
        "internalType": "bytes32[][]",
        "name": "proofs",
        "type": "bytes32[][]"
      }
    ],
    "name": "multiHarvest",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "owner",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "pause",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "paused",
    "outputs": [
      {
        "internalType": "bool",
        "name": "",
        "type": "bool"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "to",
        "type": "address"
      },
      {
        "internalType": "address",
        "name": "token",
        "type": "address"
      },
      {
        "internalType": "uint256",
        "name": "amount",
        "type": "uint256"
      }
    ],
    "name": "recoverERC20",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "renounceOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "root",
    "outputs": [
      {
        "internalType": "bytes32",
        "name": "",
        "type": "bytes32"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "newOwner",
        "type": "address"
      }
    ],
    "name": "transferOwnership",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "unpause",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "address",
        "name": "updater",
        "type": "address"
      }
    ],
    "name": "updateMerkleUpdater",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [
      {
        "internalType": "bytes32",
        "name": "newRoot",
        "type": "bytes32"
      }
    ],
    "name": "updateRoot",
    "outputs": [],
    "stateMutability": "nonpayable",
    "type": "function"
  },
  {
    "inputs": [],
    "name": "wNative",
    "outputs": [
      {
        "internalType": "address",
        "name": "",
        "type": "address"
      }
    ],
    "stateMutability": "view",
    "type": "function"
  },
  {
    "stateMutability": "payable",
    "type": "receive"
  }
]

Source code

// SPDX-License-Identifier: MIT

pragma solidity 0.8.20;

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

import {IDistributor} from "./IDistributor.sol";
import {IWeth} from "./IWeth.sol";

contract Distributor is IDistributor, Ownable, ReentrancyGuard {
    using SafeERC20 for IERC20;

    /// @inheritdoc IDistributor
    address public immutable wNative;

    /// @inheritdoc IDistributor
    bytes32 public root;

    /// @notice Mapping of whether a user has claimed their rewards
    mapping(
        address user
            => mapping(
                address pool
                    => mapping(address token => mapping(bytes identifier => uint256 amount))
            )
    ) public claimed;

    /// @inheritdoc IDistributor
    address public merkleUpdater;

    /// @notice Whether the contract is paused or not
    bool public paused;

    /// @param _owner The owner of the contract
    /// @param _updater Address that can update the merkle root of the contract
    /// @param _wNative The address of the wrapped native token for the deployed chain
    constructor(address _owner, address _updater, address _wNative) Ownable(_owner) {
        if (_updater == address(0)) revert InvalidMerkleUpdater();

        merkleUpdater = _updater;
        wNative = _wNative;
    }

    /// @inheritdoc IDistributor
    function harvest(
        address user,
        address pool,
        address token,
        uint256 amount,
        bytes calldata identifier,
        bytes32[] calldata proof
    ) external nonReentrant {
        _requireNotPaused();
        _harvest(user, pool, token, amount, identifier, proof);
    }

    /// @inheritdoc IDistributor
    function multiHarvest(
        address user,
        address[] calldata pools,
        address[] calldata tokens,
        uint256[] calldata amounts,
        bytes[] calldata identifiers,
        bytes32[][] calldata proofs
    ) external nonReentrant {
        _requireNotPaused();

        if (
            tokens.length == 0 || tokens.length != pools.length || tokens.length != amounts.length
                || tokens.length != identifiers.length || tokens.length != proofs.length
        ) revert InvalidLengths();

        for (uint256 i; i < tokens.length;) {
            _harvest(user, pools[i], tokens[i], amounts[i], identifiers[i], proofs[i]);

            unchecked {
                i++;
            }
        }
    }

    /// @inheritdoc IDistributor
    function isHarvested(
        address user,
        address pool,
        address token,
        uint256 amount,
        bytes calldata identifier
    ) public view returns (bool) {
        return claimed[user][pool][token][identifier] >= amount;
    }

    // @inheritdoc IDistributor
    function getUnclaimedAmount(
        address user,
        address pool,
        address token,
        uint256 amount,
        bytes calldata identifier
    ) public view returns (uint256) {
        uint256 claimedAmount = claimed[user][pool][token][identifier];
        return amount < claimedAmount ? 0 : amount - claimedAmount;
    }

    /// @inheritdoc IDistributor
    function updateRoot(bytes32 newRoot) external {
        _requireOnlyMerkleUpdater();

        root = newRoot;

        emit RootUpdated(newRoot);
    }

    /// @inheritdoc IDistributor
    function recoverERC20(address to, address token, uint256 amount) external {
        _requireOnlyOwner();

        IERC20(token).safeTransfer(to, amount);
        emit RecoveredERC20(to, token, amount);
    }

    /// @inheritdoc IDistributor
    function pause() external {
        _requireOnlyOwner();

        paused = true;

        emit DistributorPaused();
    }

    /// @inheritdoc IDistributor
    function unpause() external {
        _requireOnlyOwner();

        paused = false;

        emit DistributorUnpaused();
    }

    /// @inheritdoc IDistributor
    function updateMerkleUpdater(address updater) external {
        _requireOnlyOwner();

        if (updater == address(0)) revert InvalidMerkleUpdater();

        address old = merkleUpdater;
        merkleUpdater = updater;

        emit MerkleUpdaterChanged(old, merkleUpdater);
    }

    receive() external payable {}

    /// @dev Checks if contract is paused
    function _requireNotPaused() internal view {
        if (paused) revert ContractIsPaused();
    }

    /// @dev Checks if caller has operator rights
    function _requireOnlyOwner() internal view {
        if (msg.sender != owner()) revert Unauthorized();
    }

    /// @dev Checks if caller has updater rights
    function _requireOnlyMerkleUpdater() internal view {
        if (msg.sender != merkleUpdater) revert Unauthorized();
    }

    /// @dev Internal function to handle harvest of rewards
    function _harvest(
        address user,
        address pool,
        address token,
        uint256 amount,
        bytes calldata identifier,
        bytes32[] calldata proof
    ) internal {
        bytes32 leaf = keccak256(abi.encode(user, pool, token, amount, identifier));
        if (!MerkleProof.verify(proof, root, leaf)) revert InvalidProof();

        uint256 toSend = getUnclaimedAmount(user, pool, token, amount, identifier);
        if (toSend == 0) revert AlreadyClaimed();

        claimed[user][pool][token][identifier] = amount;

        if (token == wNative) {
            _unwrap(toSend);
            (bool ok,) = user.call{value: toSend}("");

            if (!ok) revert FailedToSendNative();
        } else {
            IERC20(token).safeTransfer(user, toSend);
        }

        emit Claimed(user, pool, token, toSend, amount, identifier);
    }

    /// @dev Internal function to withdraw wrapped native into native
    function _unwrap(uint256 _amount) internal {
        IWeth(wNative).withdraw(_amount);
    }
}
Chain explorer2540msChain node85ms