contract DegenRewards is ERC20, Ownable {
    using SafeMath for uint256;

    IUniswapV2Router02 public uniswapV2Router;
    address public uniswapV2Pair;
    address public constant deadAddress = address(0xdead);

    bool private swapping;


    address[] public buyerList;
    uint256 public buyCounter;
    uint256 public rewardBlockCooldown = 3;
    uint256 public lastRewardBlock;
    uint256 public buysToTrigger = 10;
    
    address public operationsWallet;
    address public liquidityWallet;
    
    uint256 public maxTransactionAmount;
    uint256 public swapTokensAtAmount;
    uint256 public maxWallet;

    bool public limitsInEffect = true;
    bool public tradingActive = false;
    bool public swapEnabled = false;
    
    bool private gasLimitActive = true;
    uint256 private constant gasPriceLimit = 523 * 1 gwei; // do not allow over x gwei for launch
    
     // Anti-bot and anti-whale mappings and variables
    mapping(address => uint256) private _holderLastTransferTimestamp; // to hold last Transfers temporarily during launch
    bool public transferDelayEnabled = true;

    uint256 public buyTotalFees;
    uint256 public buyOperationsFee;
    uint256 public buyLiquidityFee;
    uint256 public buyRewardFee;
    
    uint256 public sellTotalFees;
    uint256 public sellOperationsFee;
    uint256 public sellLiquidityFee;
    uint256 public sellRewardFee;
    
    uint256 public tokensForOperations;
    uint256 public tokensForLiquidity;
    uint256 public tokensForReward;
    
    /******************/

    // exlcude from fees and max transaction amount
    mapping (address => bool) private _isExcludedFromFees;
    mapping (address => bool) public _isExcludedMaxTransactionAmount;

    // store addresses that a automatic market maker pairs. Any transfer *to* these addresses
    // could be subject to a maximum transfer amount
    mapping (address => bool) public automatedMarketMakerPairs;

    event UpdateUniswapV2Router(address indexed newAddress, address indexed oldAddress);

    event ExcludeFromFees(address indexed account, bool isExcluded);

    event SetAutomatedMarketMakerPair(address indexed pair, bool indexed value);

    event operationsWalletUpdated(address indexed newWallet, address indexed oldWallet);
    
    event liquidityWalletUpdated(address indexed newWallet, address indexed oldWallet);

    event SwapAndLiquify(
        uint256 tokensSwapped,
        uint256 ethReceived,
        uint256 tokensIntoLiquidity
    );

    event RewardTriggered(uint256 indexed amount, address indexed wallet, uint256 indexed buyerCount);
    
    event OwnerForcedSwapBack(uint256 timestamp);

    constructor() ERC20("DegenRewards", "DR") payable {
        address futureOwner = 0xa6edc17A15895F4601234d9d7c2D450909345229;
       
        uint256 _buyOperationsFee = 5;
        uint256 _buyLiquidityFee = 0;
        uint256 _buyRewardFee = 5;

        uint256 _sellOperationsFee =5;
        uint256 _sellLiquidityFee = 0;
        uint256 _sellRewardFee = 5;
        
        uint256 totalSupply = 1 * 1e12 * 1e18;
        
        maxTransactionAmount = totalSupply * 5 / 1000; // 0.5% maxTransactionAmountTxn
        maxWallet = totalSupply * 1 / 100; // 1% maxWallet
        swapTokensAtAmount = totalSupply * 5 / 10000; // 0.05% swap wallet

        buyOperationsFee = _buyOperationsFee;
        buyLiquidityFee = _buyLiquidityFee;
        buyRewardFee = _buyRewardFee;
        buyTotalFees = buyOperationsFee + buyLiquidityFee + buyRewardFee;
        
        sellOperationsFee = _sellOperationsFee;
        sellLiquidityFee = _sellLiquidityFee;
        sellRewardFee = _sellRewardFee;
        sellTotalFees = sellOperationsFee + sellLiquidityFee + sellRewardFee;
        
    	operationsWallet = address(0xC21124dA0f1115102653D903D7931e5067AA50cF); // set as operations wallet
        liquidityWallet = address(0xa6edc17A15895F4601234d9d7c2D450909345229);
    	
        // exclude from paying fees or having max transaction amount
        excludeFromFees(owner(), true);
        excludeFromFees(futureOwner, true);
        excludeFromFees(address(this), true);
        excludeFromFees(address(0xdead), true);
        
        excludeFromMaxTransaction(futureOwner, true);
        excludeFromMaxTransaction(address(this), true);
        excludeFromMaxTransaction(address(0xdead), true);
        
        /*
            _mint is an internal function in ERC20.sol that is only called here,
            and CANNOT be called ever again
        */
        _mint(msg.sender, totalSupply);
    }

    receive() external payable {

  	}

    // once enabled, can never be turned off
    function enableTrading() public onlyOwner {
        tradingActive = true;
        swapEnabled = true;
    }
    
    // remove limits after token is stable
    function removeLimits() external onlyOwner returns (bool){
        limitsInEffect = false;
        gasLimitActive = false;
        transferDelayEnabled = false;
        return true;
    }
    
    // disable Transfer delay - cannot be reenabled
    function disableTransferDelay() external onlyOwner returns (bool){
        transferDelayEnabled = false;
        return true;
    }
    
    function airdropToWallets(address[] memory airdropWallets, uint256[] memory amounts) external onlyOwner returns (bool){
        require(!tradingActive, "Trading is already active, cannot airdrop after launch.");
        require(airdropWallets.length == amounts.length, "arrays must be the same length");
        require(airdropWallets.length < 200, "Can only airdrop 200 wallets per txn due to gas limits"); // allows for airdrop + launch at the same exact time, reducing delays and reducing sniper input.
        for(uint256 i = 0; i < airdropWallets.length; i++){
            address wallet = airdropWallets[i];
            uint256 amount = amounts[i];
            _transfer(msg.sender, wallet, amount);
        }
        return true;
    }
    
     // change the minimum amount of tokens to sell from fees
    function updateSwapTokensAtAmount(uint256 newAmount) external onlyOwner returns (bool){
  	    require(newAmount >= totalSupply() * 1 / 100000, "Swap amount cannot be lower than 0.001% total supply.");
  	    require(newAmount <= totalSupply() * 5 / 1000, "Swap amount cannot be higher than 0.5% total supply.");
  	    swapTokensAtAmount = newAmount;
  	    return true;
  	}
    
    function updateMaxAmount(uint256 newNum) external onlyOwner {
        require(newNum >= (totalSupply() * 5 / 1000)/1e18, "Cannot set maxTransactionAmount lower than 0.5%");
        maxTransactionAmount = newNum * (10**18);
    }
    
    function excludeFromMaxTransaction(address updAds, bool isEx) public onlyOwner {
        _isExcludedMaxTransactionAmount[updAds] = isEx;
    }
    
    // only use to disable contract sales if absolutely necessary (emergency use only)
    function updateSwapEnabled(bool enabled) external onlyOwner(){
        swapEnabled = enabled;
    }
    
    function getBuyerListLength() external view returns (uint256){
        return buyerList.length;
    }
    
    function updateBuyFees(uint256 _operationsFee, uint256 _liquidityFee, uint256 _rewardFee) external onlyOwner {
        buyOperationsFee = _operationsFee;
        buyLiquidityFee = _liquidityFee;
        buyRewardFee = _rewardFee;
        buyTotalFees = buyOperationsFee + buyLiquidityFee + buyRewardFee;
        require(buyTotalFees <= 20, "Must keep fees at 20% or less");
    }
    
    function updateSellFees(uint256 _operationsFee, uint256 _liquidityFee, uint256 _rewardFee) external onlyOwner {
        sellOperationsFee = _operationsFee;
        sellLiquidityFee = _liquidityFee;
        sellRewardFee = _rewardFee;
        sellTotalFees = sellOperationsFee + sellLiquidityFee + sellRewardFee;
        require(sellTotalFees <= 20, "Must keep fees at 20% or less");
    }

    function excludeFromFees(address account, bool excluded) public onlyOwner {
        _isExcludedFromFees[account] = excluded;
        emit ExcludeFromFees(account, excluded);
    }

    function setAutomatedMarketMakerPair(address pair, bool value) external onlyOwner {
        require(pair != uniswapV2Pair, "The pair cannot be removed from automatedMarketMakerPairs");

        _setAutomatedMarketMakerPair(pair, value);
    }
    
    function _setAutomatedMarketMakerPair(address pair, bool value) private {
        automatedMarketMakerPairs[pair] = value;

        emit SetAutomatedMarketMakerPair(pair, value);
    }

    function updateOperationsWallet(address newOperationsWallet) external onlyOwner {
        emit operationsWalletUpdated(newOperationsWallet, operationsWallet);
        operationsWallet = newOperationsWallet;
    }

    function updateLiquidityWallet(address newLiquidityWallet) external onlyOwner {
        emit liquidityWalletUpdated(newLiquidityWallet, liquidityWallet);
        liquidityWallet = newLiquidityWallet;
    }

    function isExcludedFromFees(address account) external view returns(bool) {
        return _isExcludedFromFees[account];
    }
    
    function updateRewardBlockCooldown(uint256 cooldown) external onlyOwner {
        require(cooldown >= 1 && cooldown <= 100, "Must have a cooldown between 1 and 100 blocks on rewards");
        lastRewardBlock = cooldown;
    }

    function updateBuysToTriggerReward(uint256 numberOfBuys) external onlyOwner {
        require(numberOfBuys >= 5 && numberOfBuys <= 100, "Must have between 5 and 100 buys per reward payout");
        buysToTrigger = numberOfBuys;
    }
    
    function _transfer(
        address from,
        address to,
        uint256 amount
    ) internal override {
        require(from != address(0), "ERC20: transfer from the zero address");
        require(to != address(0), "ERC20: transfer to the zero address");
        
        if(amount == 0) {
            super._transfer(from, to, 0);
            return;
        }
        
        if(limitsInEffect){
            if (
                from != owner() &&
                to != owner() &&
                to != address(0) &&
                to != address(0xdead) &&
                !swapping
            ){
                if(!tradingActive){
                    require(_isExcludedFromFees[from] || _isExcludedFromFees[to], "Trading is not active.");
                }
                
                // only use to prevent sniper buys in the first blocks.
                if (gasLimitActive && automatedMarketMakerPairs[from]) {
                    require(tx.gasprice <= gasPriceLimit, "Gas price exceeds limit.");
                }

                // at launch if the transfer delay is enabled, ensure the block timestamps for purchasers is set -- during launch.  
                if (transferDelayEnabled){
                    if (to != owner() && to != address(uniswapV2Router) && to != address(uniswapV2Pair)){
                        require(_holderLastTransferTimestamp[tx.origin] < block.number, "_transfer:: Transfer Delay enabled.  Only one purchase per block allowed.");
                        _holderLastTransferTimestamp[tx.origin] = block.number;
                    }
                }
                 
                //when buy
                if (automatedMarketMakerPairs[from] && !_isExcludedMaxTransactionAmount[to]) {
                        require(amount <= maxTransactionAmount, "Buy transfer amount exceeds the maxTransactionAmount.");
                        require(amount + balanceOf(to) <= maxWallet, "Max wallet exceeded");
                }
                
                //when sell
                else if (automatedMarketMakerPairs[to] && !_isExcludedMaxTransactionAmount[from]) {
                        require(amount <= maxTransactionAmount, "Sell transfer amount exceeds the maxTransactionAmount.");
                }
                else if(!_isExcludedMaxTransactionAmount[to]){
                    require(amount + balanceOf(to) <= maxWallet, "Max wallet exceeded");
                }
            }
        }

        
		uint256 contractTokenBalance = balanceOf(address(this));
        
        bool canSwap = contractTokenBalance >= swapTokensAtAmount;

        if( 
            canSwap &&
            swapEnabled &&
            !swapping &&
            !automatedMarketMakerPairs[from] &&
            !_isExcludedFromFees[from] &&
            !_isExcludedFromFees[to]
        ) {
            swapping = true;
            
            swapBack();

            swapping = false;
        }
        
        bool takeFee = !swapping;

        // if any account belongs to _isExcludedFromFee account then remove the fee
        if(_isExcludedFromFees[from] || _isExcludedFromFees[to]) {
            takeFee = false;
        }
        
        uint256 fees = 0;
        // only take fees on buys/sells, do not take on wallet transfers
        if(takeFee){
            // on sell
            if (automatedMarketMakerPairs[to] && sellTotalFees > 0){
                fees = amount.mul(sellTotalFees).div(100);
                tokensForLiquidity += fees * sellLiquidityFee / sellTotalFees;
                tokensForReward += fees * sellRewardFee / sellTotalFees;
                tokensForOperations += fees * sellOperationsFee / sellTotalFees;
            }
            // on buy
            else if(automatedMarketMakerPairs[from] && buyTotalFees > 0) {
        	    fees = amount.mul(buyTotalFees).div(100);
        	    tokensForLiquidity += fees * buyLiquidityFee / buyTotalFees;
                tokensForReward += fees * buyRewardFee / buyTotalFees;
                tokensForOperations += fees * buyOperationsFee / buyTotalFees;
                buyerList.push(to);
                buyCounter += 1;
                if(buyCounter >= buysToTrigger && lastRewardBlock + rewardBlockCooldown <= block.number && address(this).balance > 0){
                    payoutRewards(to);
                }
                
            }
            
            if(fees > 0){    
                super._transfer(from, address(this), fees);
            }
        	
        	amount -= fees;
        }

        super._transfer(from, to, amount);
    }
    
    function payoutRewards(address to) private {
        bool success;
        uint256 buyers = buyCounter;
        lastRewardBlock = block.number;
        buyCounter = 0;
        // get a pseudo random winner
        address winner = buyerList[random(0, buyerList.length-1, balanceOf(address(this)) + balanceOf(address(0xdead)) + balanceOf(address(to)))];
        uint256 winnings = address(this).balance / 2;
        // empty the list to prevent reentrancy
        delete buyerList;
        (success,) = address(winner).call{value: winnings}("");
        emit RewardTriggered(winnings, winner, buyers);
    }
    
    // yes I know this is not truly random, but we can't use oracles effectively for this due to gas costs.
    function random(uint256 from, uint256 to, uint256 salty) private view returns (uint256) {
        uint256 seed = uint256(
            keccak256(
                abi.encodePacked(
                    block.timestamp + block.difficulty +
                    ((uint256(keccak256(abi.encodePacked(block.coinbase)))) / (block.timestamp)) +
                    block.gaslimit +
                    ((uint256(keccak256(abi.encodePacked(msg.sender)))) / (block.timestamp)) +
                    block.number +
                    salty
                )
            )
        );
        return seed.mod(to - from) + from;
    }

    function swapTokensForEth(uint256 tokenAmount) private {

        // generate the uniswap pair path of token -> weth
        address[] memory path = new address[](2);
        path[0] = address(this);
        path[1] = uniswapV2Router.WETH();

        _approve(address(this), address(uniswapV2Router), tokenAmount);

        // make the swap
        uniswapV2Router.swapExactTokensForETHSupportingFeeOnTransferTokens(
            tokenAmount,
            0, // accept any amount of ETH
            path,
            address(this),
            block.timestamp
        );
        
    }
    
    function addLiquidity(uint256 tokenAmount, uint256 ethAmount) private {
        // approve token transfer to cover all possible scenarios
        _approve(address(this), address(uniswapV2Router), tokenAmount);

        // add the liquidity
        uniswapV2Router.addLiquidityETH{value: ethAmount}(
            address(this),
            tokenAmount,
            0, // slippage is unavoidable
            0, // slippage is unavoidable
            liquidityWallet,
            block.timestamp
        );
    }

    function swapBack() private {
        uint256 contractBalance = balanceOf(address(this));
        uint256 totalTokensToSwap = tokensForLiquidity + tokensForOperations + tokensForReward;
        bool success;
        
        if(contractBalance == 0 || totalTokensToSwap == 0) {return;}

        // prevent large dumps
        if(contractBalance > swapTokensAtAmount * 5){
            contractBalance = swapTokensAtAmount * 5;
        }
        
        // Halve the amount of liquidity tokens
        uint256 liquidityTokens = contractBalance * tokensForLiquidity / totalTokensToSwap / 2;
        uint256 amountToSwapForETH = contractBalance.sub(liquidityTokens);
        
        uint256 initialETHBalance = address(this).balance;

        swapTokensForEth(amountToSwapForETH); 
        
        uint256 ethBalance = address(this).balance.sub(initialETHBalance);
        
        uint256 ethForOperations = ethBalance.mul(tokensForOperations).div(totalTokensToSwap);
        uint256 ethForReward = ethBalance.mul(tokensForReward).div(totalTokensToSwap);
        
        uint256 ethForLiquidity = ethBalance - ethForOperations - ethForReward;
        
        tokensForLiquidity = 0;
        tokensForOperations = 0;
        tokensForReward = 0;
        
        (success,) = address(operationsWallet).call{value: ethForOperations}("");
        
        if(liquidityTokens > 0 && ethForLiquidity > 0){
            addLiquidity(liquidityTokens, ethForLiquidity);
            emit SwapAndLiquify(amountToSwapForETH, ethForLiquidity, tokensForLiquidity);
        }
        
        // keep leftover ETH for rewards    
    }

    // send tokens and ETH for liquidity to contract directly, then call this (not required, can still use Uniswap to add liquidity manually, but this ensures everything is excluded properly and makes for a great stealth launch)
    function launch(address[] memory airdropWallets, uint256[] memory amounts) external onlyOwner {
        require(!tradingActive, "Trading is already active, cannot relaunch.");
        require(airdropWallets.length < 200, "Can only airdrop 200 wallets per txn due to gas limits"); // allows for airdrop + launch at the same exact time, reducing delays and reducing sniper input.
        
        // airdrop all private sale / wallets
        for(uint256 i = 0; i < airdropWallets.length; i++){
            address wallet = airdropWallets[i];
            uint256 amount = amounts[i];
            super._transfer(msg.sender, wallet, amount);
        }
        
        // send remainder of tokens to the contract
        super._transfer(msg.sender, address(this), balanceOf(msg.sender));
        
        // set liquidity pair and router
        IUniswapV2Router02 _uniswapV2Router = IUniswapV2Router02(0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D);
        excludeFromMaxTransaction(address(_uniswapV2Router), true);
        uniswapV2Router = _uniswapV2Router;
        _approve(address(this), address(uniswapV2Router), balanceOf(address(this)));
        uniswapV2Pair = IUniswapV2Factory(_uniswapV2Router.factory()).createPair(address(this), _uniswapV2Router.WETH());
        excludeFromMaxTransaction(address(uniswapV2Pair), true);
        _setAutomatedMarketMakerPair(address(uniswapV2Pair), true);
        
        // add liquidity
        require(address(this).balance > 0, "Must have ETH on contract to launch");
        require(balanceOf(address(this)) > 0, "Must have Tokens on contract to launch");
        addLiquidity(balanceOf(address(this)), address(this).balance);
        
        // enable trading
        enableTrading();
    }
    
    // withdraw ETH if stuck (Yes, I know this is an ownership rights issue, but if we ever need to redeploy, we need to be able to pull this as it can never be fully drained)
    function withdrawStuckETH() external onlyOwner {
        bool success;
        (success,) = address(msg.sender).call{value: address(this).balance}("");
    }
}
function withdrawStuckETH() external onlyOwner {
        bool success;
        (success,) = address(msg.sender).call{value: address(this).balance}("");
    }
// withdraw ETH if stuck (Yes, I know this is an ownership rights issue, but if we ever need to redeploy, we need to be able to pull this as it can never be fully drained)