// 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"; /// @title DegenZRewardDistributor — holds tax tokens; holders claim share of converted reward token. /// @dev v1: anyone can process a batch swap of accumulated tax → rewardToken via V2 router; holders claim pro-rata. contract DegenZRewardDistributor is Ownable, ReentrancyGuard { using SafeERC20 for IERC20; IERC20 public immutable taxToken; IERC20 public immutable rewardToken; address public immutable v2Router; address public immutable weth; uint256 public totalRewardDistributed; mapping(address => uint256) public claimedReward; mapping(address => uint256) public rewardDebt; uint256 public accRewardPerShare; // 1e18 precision uint256 private constant PRECISION = 1e18; event RewardProcessed(uint256 taxIn, uint256 rewardOut); event Claimed(address indexed user, uint256 amount); constructor(address taxToken_, address rewardToken_, address v2Router_, address weth_, address owner_) Ownable(owner_) { require( taxToken_ != address(0) && rewardToken_ != address(0) && v2Router_ != address(0) && weth_ != address(0), "zero addr" ); taxToken = IERC20(taxToken_); rewardToken = IERC20(rewardToken_); v2Router = v2Router_; weth = weth_; } /// @notice Convert accumulated tax tokens into rewardToken and credit holders by balance. /// @dev Simplified: boosts accRewardPerShare using current totalSupply snapshot of taxToken. function processRewards(uint256 minRewardOut, uint256 deadline) external nonReentrant { uint256 taxBal = taxToken.balanceOf(address(this)); require(taxBal > 0, "no tax"); uint256 rewardBefore = rewardToken.balanceOf(address(this)); if (address(rewardToken) == address(taxToken)) { // Already in reward token — just credit. _credit(taxBal); emit RewardProcessed(taxBal, taxBal); return; } taxToken.forceApprove(v2Router, taxBal); address[] memory path; if (address(rewardToken) == weth) { path = new address[](2); path[0] = address(taxToken); path[1] = weth; } else { path = new address[](3); path[0] = address(taxToken); path[1] = weth; path[2] = address(rewardToken); } IUniswapV2RouterLike(v2Router).swapExactTokensForTokensSupportingFeeOnTransferTokens( taxBal, minRewardOut, path, address(this), deadline ); uint256 rewardOut = rewardToken.balanceOf(address(this)) - rewardBefore; require(rewardOut >= minRewardOut, "slippage"); _credit(rewardOut); emit RewardProcessed(taxBal, rewardOut); } function pendingReward(address user) public view returns (uint256) { uint256 bal = taxToken.balanceOf(user); uint256 accumulated = (bal * accRewardPerShare) / PRECISION; if (accumulated <= rewardDebt[user]) return 0; return accumulated - rewardDebt[user]; } function claim() external nonReentrant { uint256 amount = pendingReward(msg.sender); require(amount > 0, "nothing"); rewardDebt[msg.sender] = (taxToken.balanceOf(msg.sender) * accRewardPerShare) / PRECISION; claimedReward[msg.sender] += amount; totalRewardDistributed += amount; rewardToken.safeTransfer(msg.sender, amount); emit Claimed(msg.sender, amount); } /// @notice Sync debt after balance changes (optional helper for UIs). function updateDebt(address user) external { rewardDebt[user] = (taxToken.balanceOf(user) * accRewardPerShare) / PRECISION; } function _credit(uint256 rewardAmount) internal { uint256 supply = taxToken.totalSupply(); require(supply > 0, "supply=0"); accRewardPerShare += (rewardAmount * PRECISION) / supply; } } interface IUniswapV2RouterLike { function swapExactTokensForTokensSupportingFeeOnTransferTokens( uint256 amountIn, uint256 amountOutMin, address[] calldata path, address to, uint256 deadline ) external; }