contract DonutUniswapRewards is LPTokenWrapper, Ownable {
    using SafeERC20 for IERC20;
    IERC20 public donut = IERC20(0xC0F9bD5Fa5698B6505F643900FFA515Ea5dF54A9);

    uint256 public rewardRate;
    uint64 public periodFinish;
    uint64 public lastUpdateTime;
    uint128 public rewardPerTokenStored;
    struct UserRewards {
        uint128 userRewardPerTokenPaid;
        uint128 rewards;
    }
    mapping(address => UserRewards) public userRewards;

    event RewardAdded(uint256 reward);
    event RewardPaid(address indexed user, uint256 reward);

    modifier updateReward(address account) {
        uint128 _rewardPerTokenStored = rewardPerToken();
        lastUpdateTime = lastTimeRewardApplicable();
        rewardPerTokenStored = _rewardPerTokenStored;
        userRewards[account].rewards = earned(account);
        userRewards[account].userRewardPerTokenPaid = _rewardPerTokenStored;
        _;
    }

    function lastTimeRewardApplicable() public view returns (uint64) {
        uint64 blockTimestamp = uint64(block.timestamp);
        return blockTimestamp < periodFinish ? blockTimestamp : periodFinish;
    }

    function rewardPerToken() public view returns (uint128) {
        uint256 totalStakedSupply = totalSupply;
        if (totalStakedSupply == 0) {
            return rewardPerTokenStored;
        }
        uint256 rewardDuration = lastTimeRewardApplicable()-lastUpdateTime;
        return uint128(rewardPerTokenStored + rewardDuration*rewardRate*1e18/totalStakedSupply);
    }

    function earned(address account) public view returns (uint128) {
        return uint128(balanceOf(account)*(rewardPerToken()-userRewards[account].userRewardPerTokenPaid)/1e18 + userRewards[account].rewards);
    }

    // stake visibility is public as overriding LPTokenWrapper's stake() function
    function stake(uint128 amount) public override updateReward(msg.sender) {
        require(amount > 0, "Cannot stake 0");
        super.stake(amount);
    }

    function withdraw() public override updateReward(msg.sender) {
        super.withdraw();
    }

    function exit() external {
        withdraw();
        getReward();
    }

    function getReward() public updateReward(msg.sender) {
        uint256 reward = earned(msg.sender);
        if (reward > 0) {
            userRewards[msg.sender].rewards = 0;
            require(donut.transfer(msg.sender, reward), "DONUT transfer failed");
            emit RewardPaid(msg.sender, reward);
        }
    }

    function setRewardParams(uint128 reward, uint64 duration) external onlyOwner {
        rewardPerTokenStored = rewardPerToken();
        uint64 blockTimestamp = uint64(block.timestamp);
        if (blockTimestamp >= periodFinish) {
            rewardRate = reward/duration;
        } else {
            uint256 remaining = periodFinish-blockTimestamp;
            uint256 leftover = remaining*rewardRate;
            rewardRate = (reward+leftover)/duration;
        }
        lastUpdateTime = blockTimestamp;
        periodFinish = blockTimestamp+duration;
        emit RewardAdded(reward);
    }
    
    /* to be used if users vote to stop the incentive program,
    also to withdraw possible airdrops and mistakenly sent uni tokens
    can't touch staked uniswap LP tokens */
    function recoverTokens(IERC20 token) external onlyOwner {
        if(token == uniswapDonutEth) {
            //unstaked balance - tokens sent directly to the contract address rather than staked
            //totalSupply always <= uniswapDonutEth.balanceOf(address(this)), no overflow possible
            uint256 unstakedSupply = uniswapDonutEth.balanceOf(address(this))-totalSupply;
            require(unstakedSupply > 0 && uniswapDonutEth.transfer(msg.sender, unstakedSupply));
        }
        else {
            uint256 tokenBalance = token.balanceOf(address(this));
            require(tokenBalance > 0);
            token.safeTransfer(msg.sender, tokenBalance);
        }
    }
}
function stake(uint128 amount) public override updateReward(msg.sender) {
    require(amount > 0, "Cannot stake 0");
    super.stake(amount);
}

// stake visibility is public as overriding LPTokenWrapper's stake() function