pragma solidity ^0.8.9;




contract Presale_w_refund is Ownable,ReentrancyGuard {
    
    /// unix time after which failSafe withdrawals are enabled
    uint public failSafeAfterTime;

    /// unix time before which you can claim for that round
    uint public claimBeforeTime;

    uint128 public claimPercentage;
    uint128 public claimRound = 0;

    uint public totalContributed;
    
    /// a minimum contribution to participate in the seed round
    uint256 public constant MIN_CONTRIBUTION = .1 ether;
    
    /// limit the maximum contribution for each wallet
    uint256 public constant MAX_CONTRIBUTION = .5 ether;

    /// the maximum amount of eth that this contract will accept for seed round
    uint256 public constant HARD_CAP = 31 ether;

    // Amount of time without any activity after which the fail-safe 100% withdrawal is enabled
    uint256 public constant FAIL_SAFE_PERIOD = 7 days;

    /// How long you can claim for after a claiming round is open
    uint256 public constant CLAIM_TIME_PERIOD = 8 hours;

    /// used to track the total number of contributors
    uint256 public numberOfContributors;

    uint public constant PRESALE = 50_000_000_000 * 1e18;

    bool public areContributionsAllowed;
    bool public isClaimingAllowed;

    mapping (address => uint) public addressToRoundClaimed;
    mapping (address => uint) public addresssToEthContributed;
    mapping (address => uint) public addressToClaimedPercentage;
    mapping (uint256 => address) public indexToAddress;    

    address public immutable tokenAddress; 

    event ContributionReceived(address indexed contributor, uint amount);
    event TokenClaimed(address indexed claimer, uint amount);  

    constructor(address _erc20Address) { 
        tokenAddress = _erc20Address; 
        failSafeAfterTime = block.timestamp + FAIL_SAFE_PERIOD;
    }

    /// @notice Claim tokens: Send eth to lock in your place in the presale. Must be between min and max contribution
    function sendToSeed() public payable {
        if(!areContributionsAllowed) revert contributionsNotAllowed();
        if(msg.value < MIN_CONTRIBUTION) revert lessThanContributionMinimum();
        if(addresssToEthContributed[msg.sender] +msg.value > MAX_CONTRIBUTION) revert moreThanContributionMaximum();
        if(msg.value + totalContributed > HARD_CAP) revert maxContributionsExceeded(); 


        if (addresssToEthContributed[msg.sender] == 0) {
            numberOfContributors++;
            indexToAddress[numberOfContributors] = msg.sender;
        }

        addresssToEthContributed[msg.sender] = addresssToEthContributed[msg.sender] + msg.value;
        totalContributed += msg.value;
        emit ContributionReceived(msg.sender, msg.value);
    }
    
    receive() external payable {
        sendToSeed();
    }

/// @notice Claim tokens: Claim all tokens if failSafe time has passed. Else, if claiming is open, claim the agreed percentage.
    function claimToken() external notClaimedAllTokens(msg.sender) {
        if(addresssToEthContributed[msg.sender] < 1) revert noContributionsMade();

        uint userPortion;

        if (block.timestamp > failSafeAfterTime) {       
            userPortion= ((addresssToEthContributed[msg.sender]*(100-addressToClaimedPercentage[msg.sender]) * PRESALE) / (100 * totalContributed));
            addressToClaimedPercentage[msg.sender] = 100;
        }
        else {
            if(isClaimingAllowed == false) revert claimingNotEnabled();
            if(block.timestamp > claimBeforeTime) revert claimTimePassed();
            if(addressToRoundClaimed[msg.sender] == claimRound) revert alreadyClaimedRound();

            addressToRoundClaimed[msg.sender] = claimRound;

            if( (addressToClaimedPercentage[msg.sender] + claimPercentage) > 99) 
            {
            userPortion = ((addresssToEthContributed[msg.sender] * (100 - addressToClaimedPercentage[msg.sender]) * PRESALE) / (100*totalContributed));
            addressToClaimedPercentage[msg.sender] = 100;
            }
            else {
            userPortion = ((addresssToEthContributed[msg.sender] * claimPercentage * PRESALE) / (100*totalContributed));
            addressToClaimedPercentage[msg.sender] = addressToClaimedPercentage[msg.sender]+ claimPercentage;
            }
        }
   
        transferERC20(msg.sender, userPortion);
        emit TokenClaimed(msg.sender, userPortion);
    }

/// Use to get index of user to refund. To be used with refundContributors 
/// @return index matching the user address. To refund one user, use as both _startIndex and _endIndex in refund contributor
function findContributorIndex(address _contributor) external view returns (uint) {
            for (uint256 i = 1; i <= numberOfContributors;i++) {
            if(indexToAddress[i] == _contributor) return i;
        }
        return 0;
}

       /// if there are not enough contributions or we hit a bug, refund everyone their ETH. 
       /// can also refund individual user and subtract their contributions
    function refundContributors(uint256 _startIndex, uint256 _endIndex) external onlyOwner nonReentrant {
        for (uint256 i = _startIndex; i <= _endIndex;i++) {
            address contributorAddress = indexToAddress[i];
            uint addressContributed = addresssToEthContributed[contributorAddress];
            payable(contributorAddress).transfer(addressContributed);
             totalContributed -= addressContributed;
            addresssToEthContributed[contributorAddress] = 0;
           
        }
    }

/// set claim percentage between 1 and 100
/// @param _newClaimPercentage 1 represents 1%; 99 represents 99%
    function setClaimPercentage(uint128 _newClaimPercentage) internal {
        if(_newClaimPercentage > 100 || _newClaimPercentage < 1) {
            revert invalidClaimPercentage();
        }
        claimPercentage = _newClaimPercentage;
    }

    function transferERC20(address to, uint256 amount) internal {
        IERC20 token = IERC20(tokenAddress);
        uint256 erc20balance = token.balanceOf(address(this));
        require(amount <= erc20balance, "balance is low");
        token.transfer(to, amount);
    }   

    /// enable or disable contributions
    function toggleAllowContributions() external onlyOwner {
        areContributionsAllowed = !areContributionsAllowed;
    }

    /// go to the next round && open claiming for CLAIM_TIME_PERIOD
    /// @param _newClaimPercentage between 1 and 100
    function openClaiming(uint128 _newClaimPercentage) external onlyOwner {
        claimRound++;
        setClaimPercentage(_newClaimPercentage);
        isClaimingAllowed = true;
        failSafeAfterTime = block.timestamp + FAIL_SAFE_PERIOD;
        claimBeforeTime = block.timestamp + CLAIM_TIME_PERIOD; 
    }

    /// reset Fail Safe Timer
    function keepLocked() external onlyOwner {
        isClaimingAllowed = false;
        failSafeAfterTime = block.timestamp + FAIL_SAFE_PERIOD; 
    }

    /// allows the owner to withdraw the funds in this contract
    /// @param _withdrawTo the address to which the funds will be withdrawn
    function withdrawBalance(address payable _withdrawTo) external onlyOwner nonReentrant {
        (bool success, ) = _withdrawTo.call{value: address(this).balance}("");
        require(success, "Withdraw failed");
    }

    modifier notClaimedAllTokens(address _claimer) {
       if(addressToClaimedPercentage[_claimer] > 99) revert cannotClaimMoreThan100Percent();
              _;
    }

    error contributionsNotAllowed();
    error invalidClaimPercentage();
    error claimTimePassed();      
    error cannotClaimMoreThan100Percent();
    error noContributionsMade();
    error claimingNotEnabled();
    error lessThanContributionMinimum();
    error moreThanContributionMaximum();
    error maxContributionsExceeded();
    error alreadyClaimedRound();

}
[{"inputs":[{"internalType":"address","name":"_erc20Address","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"alreadyClaimedRound","type":"error"},{"inputs":[],"name":"cannotClaimMoreThan100Percent","type":"error"},{"inputs":[],"name":"claimTimePassed","type":"error"},{"inputs":[],"name":"claimingNotEnabled","type":"error"},{"inputs":[],"name":"contributionsNotAllowed","type":"error"},{"inputs":[],"name":"invalidClaimPercentage","type":"error"},{"inputs":[],"name":"lessThanContributionMinimum","type":"error"},{"inputs":[],"name":"maxContributionsExceeded","type":"error"},{"inputs":[],"name":"moreThanContributionMaximum","type":"error"},{"inputs":[],"name":"noContributionsMade","type":"error"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"contributor","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"ContributionReceived","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":"claimer","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"TokenClaimed","type":"event"},{"inputs":[],"name":"CLAIM_TIME_PERIOD","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"FAIL_SAFE_PERIOD","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"HARD_CAP","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MAX_CONTRIBUTION","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"MIN_CONTRIBUTION","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"PRESALE","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"addressToClaimedPercentage","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"addressToRoundClaimed","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"addresssToEthContributed","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"areContributionsAllowed","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"claimBeforeTime","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"claimPercentage","outputs":[{"internalType":"uint128","name":"","type":"uint128"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"claimRound","outputs":[{"internalType":"uint128","name":"","type":"uint128"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"claimToken","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"failSafeAfterTime","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"_contributor","type":"address"}],"name":"findContributorIndex","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"indexToAddress","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"isClaimingAllowed","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"keepLocked","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"numberOfContributors","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint128","name":"_newClaimPercentage","type":"uint128"}],"name":"openClaiming","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"_startIndex","type":"uint256"},{"internalType":"uint256","name":"_endIndex","type":"uint256"}],"name":"refundContributors","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"renounceOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"sendToSeed","outputs":[],"stateMutability":"payable","type":"function"},{"inputs":[],"name":"toggleAllowContributions","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"tokenAddress","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalContributed","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address payable","name":"_withdrawTo","type":"address"}],"name":"withdrawBalance","outputs":[],"stateMutability":"nonpayable","type":"function"},{"stateMutability":"payable","type":"receive"}]