// SPDX-License-Identifier: MIT

/**
Liquidity Locker. 
1% fees claimable every 24 hours
Locked for 180 in constructor parameter.
*/

pragma solidity 0.8.18;

library Address {

    function isContract(address account) internal view returns (bool) {


        return account.code.length > 0;
    }

    function sendValue(address payable recipient, uint256 amount) internal {
        require(address(this).balance >= amount, "Address: insufficient balance");

        (bool success, ) = recipient.call{value: amount}("");
        require(success, "Address: unable to send value, recipient may have reverted");
    }

    function functionCall(address target, bytes memory data) internal returns (bytes memory) {
        return functionCallWithValue(target, data, 0, "Address: low-level call failed");
    }

    function functionCall(
        address target,
        bytes memory data,
        string memory errorMessage
    ) internal returns (bytes memory) {
        return functionCallWithValue(target, data, 0, errorMessage);
    }


    function functionCallWithValue(
        address target,
        bytes memory data,
        uint256 value
    ) internal returns (bytes memory) {
        return functionCallWithValue(target, data, value, "Address: low-level call with value failed");
    }

    function functionCallWithValue(
        address target,
        bytes memory data,
        uint256 value,
        string memory errorMessage
    ) internal returns (bytes memory) {
        require(address(this).balance >= value, "Address: insufficient balance for call");
        (bool success, bytes memory returndata) = target.call{value: value}(data);
        return verifyCallResultFromTarget(target, success, returndata, errorMessage);
    }

    function functionStaticCall(address target, bytes memory data) internal view returns (bytes memory) {
        return functionStaticCall(target, data, "Address: low-level static call failed");
    }

    function functionStaticCall(
        address target,
        bytes memory data,
        string memory errorMessage
    ) internal view returns (bytes memory) {
        (bool success, bytes memory returndata) = target.staticcall(data);
        return verifyCallResultFromTarget(target, success, returndata, errorMessage);
    }

    function functionDelegateCall(address target, bytes memory data) internal returns (bytes memory) {
        return functionDelegateCall(target, data, "Address: low-level delegate call failed");
    }

    function functionDelegateCall(
        address target,
        bytes memory data,
        string memory errorMessage
    ) internal returns (bytes memory) {
        (bool success, bytes memory returndata) = target.delegatecall(data);
        return verifyCallResultFromTarget(target, success, returndata, errorMessage);
    }

    function verifyCallResultFromTarget(
        address target,
        bool success,
        bytes memory returndata,
        string memory errorMessage
    ) internal view returns (bytes memory) {
        if (success) {
            if (returndata.length == 0) {
               
                require(isContract(target), "Address: call to non-contract");
            }
            return returndata;
        } else {
            _revert(returndata, errorMessage);
        }
    }

    function verifyCallResult(
        bool success,
        bytes memory returndata,
        string memory errorMessage
    ) internal pure returns (bytes memory) {
        if (success) {
            return returndata;
        } else {
            _revert(returndata, errorMessage);
        }
    }

    function _revert(bytes memory returndata, string memory errorMessage) private pure {
        
        if (returndata.length > 0) {
            
            assembly {
                let returndata_size := mload(returndata)
                revert(add(32, returndata), returndata_size)
            }
        } else {
            revert(errorMessage);
        }
    }
    }

interface IERC20Permit {

    function permit(
        address owner,
        address spender,
        uint256 value,
        uint256 deadline,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) external;

    function nonces(address owner) external view returns (uint256);

    function DOMAIN_SEPARATOR() external view returns (bytes32);
}

interface IERC20 {

    event Transfer(address indexed from, address indexed to, uint256 value);

    event Approval(address indexed owner, address indexed spender, uint256 value);

    function totalSupply() external view returns (uint256);

    function balanceOf(address account) external view returns (uint256);

    function transfer(address to, uint256 amount) external returns (bool);

    function allowance(address owner, address spender) external view returns (uint256);

    function approve(address spender, uint256 amount) external returns (bool);

    function transferFrom(
        address from,
        address to,
        uint256 amount
    ) external returns (bool);
}

library SafeERC20 {
    using Address for address;

    function safeTransfer(
        IERC20 token,
        address to,
        uint256 value
    ) internal {
        _callOptionalReturn(token, abi.encodeWithSelector(token.transfer.selector, to, value));
    }

    function safeTransferFrom(
        IERC20 token,
        address from,
        address to,
        uint256 value
    ) internal {
        _callOptionalReturn(token, abi.encodeWithSelector(token.transferFrom.selector, from, to, value));
    }

    function safeApprove(
        IERC20 token,
        address spender,
        uint256 value
    ) internal {

        require(
            (value == 0) || (token.allowance(address(this), spender) == 0),
            "SafeERC20: approve from non-zero to non-zero allowance"
        );
        _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, value));
    }

    function safeIncreaseAllowance(
        IERC20 token,
        address spender,
        uint256 value
    ) internal {
        uint256 newAllowance = token.allowance(address(this), spender) + value;
        _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, newAllowance));
    }

    function safeDecreaseAllowance(
        IERC20 token,
        address spender,
        uint256 value
    ) internal {
        unchecked {
            uint256 oldAllowance = token.allowance(address(this), spender);
            require(oldAllowance >= value, "SafeERC20: decreased allowance below zero");
            uint256 newAllowance = oldAllowance - value;
            _callOptionalReturn(token, abi.encodeWithSelector(token.approve.selector, spender, newAllowance));
        }
    }

    function safePermit(
        IERC20Permit token,
        address owner,
        address spender,
        uint256 value,
        uint256 deadline,
        uint8 v,
        bytes32 r,
        bytes32 s
    ) internal {
        uint256 nonceBefore = token.nonces(owner);
        token.permit(owner, spender, value, deadline, v, r, s);
        uint256 nonceAfter = token.nonces(owner);
        require(nonceAfter == nonceBefore + 1, "SafeERC20: permit did not succeed");
    }

    function _callOptionalReturn(IERC20 token, bytes memory data) private {

        bytes memory returndata = address(token).functionCall(data, "SafeERC20: low-level call failed");
        if (returndata.length > 0) {
            // Return data is optional
            require(abi.decode(returndata, (bool)), "SafeERC20: ERC20 operation did not succeed");
        }
    }
    }

library SafeMath {

    function tryAdd(uint256 a, uint256 b) internal pure returns (bool, uint256) {
        unchecked {
            uint256 c = a + b;
            if (c < a) return (false, 0);
            return (true, c);
        }
    }

    function trySub(uint256 a, uint256 b) internal pure returns (bool, uint256) {
        unchecked {
            if (b > a) return (false, 0);
            return (true, a - b);
        }
    }

    function tryMul(uint256 a, uint256 b) internal pure returns (bool, uint256) {
        unchecked {

            if (a == 0) return (true, 0);
            uint256 c = a * b;
            if (c / a != b) return (false, 0);
            return (true, c);
        }
    }

    function tryDiv(uint256 a, uint256 b) internal pure returns (bool, uint256) {
        unchecked {
            if (b == 0) return (false, 0);
            return (true, a / b);
        }
    }

    function tryMod(uint256 a, uint256 b) internal pure returns (bool, uint256) {
        unchecked {
            if (b == 0) return (false, 0);
            return (true, a % b);
        }
    }

    function add(uint256 a, uint256 b) internal pure returns (uint256) {
        return a + b;
    }

    function sub(uint256 a, uint256 b) internal pure returns (uint256) {
        return a - b;
    }

    function mul(uint256 a, uint256 b) internal pure returns (uint256) {
        return a * b;
    }

    function div(uint256 a, uint256 b) internal pure returns (uint256) {
        return a / b;
    }

    function mod(uint256 a, uint256 b) internal pure returns (uint256) {
        return a % b;
    }

    function sub(
        uint256 a,
        uint256 b,
        string memory errorMessage
    ) internal pure returns (uint256) {
        unchecked {
            require(b <= a, errorMessage);
            return a - b;
        }
    }

    function div(
        uint256 a,
        uint256 b,
        string memory errorMessage
    ) internal pure returns (uint256) {
        unchecked {
            require(b > 0, errorMessage);
            return a / b;
        }
    }

    function mod(
        uint256 a,
        uint256 b,
        string memory errorMessage
    ) internal pure returns (uint256) {
        unchecked {
            require(b > 0, errorMessage);
            return a % b;
        }
    }
    }

contract LiqLocker {
        bool internal locked;
        using SafeERC20 for IERC20;
        using SafeMath for uint256;
        address payable public owner;
        bool public allIncomingDepositsFinalised;
        uint256 public initialTimestamp;
        bool public timestampSet;
        uint256 public timePeriod;
        mapping(address => uint256) public alreadyWithdrawn;
        mapping(address => uint256) public balances;
        uint256 public contractBalance;
        uint256 public initialBalance;
        bool public initialBalanceSet = false;
        mapping(address => bool) public hasBalance;
        uint256 public rewardClaimTime;

IERC20 public erc20Contract;
        event TokensDeposited(address from, uint256 amount);
        event AllocationPerformed(address recipient, uint256 amount);
        event TokensUnlocked(address recipient, uint256 amount);

    constructor(IERC20 _erc20_contract_address) {
        allIncomingDepositsFinalised = false;
        owner = payable(msg.sender);
        timestampSet = false;
        require(address(_erc20_contract_address) != address(0), "_erc20_contract_address address can not be zero");
        erc20Contract = _erc20_contract_address;
        locked = false;
        initialBalance = 0;
        rewardClaimTime = block.timestamp.add(24 hours);
    }

    modifier noReentrant() {
    require(!locked, "No re-entrancy");
    locked = true;
    _;

    locked = false;
    }

    modifier incomingDepositsStillAllowed() {
    require(allIncomingDepositsFinalised == false, "Incoming deposits have been finalised.");
    _;
    }

    modifier onlyOwner() {
    require(msg.sender == owner, "Message sender must be the contract's owner.");
    _;
    }

    modifier timestampNotSet() {
    require(timestampSet == false, "The time stamp has already been set.");
    _;
    }

    modifier timestampIsSet() {
    require(timestampSet == true, "Please set the time stamp first, then try again.");
    _;
    }

    receive() payable external incomingDepositsStillAllowed {
    contractBalance = contractBalance.add(msg.value);
    
    emit TokensDeposited(msg.sender, msg.value);
    }

    function setInitialBalance(address account, uint256 amount) public onlyOwner {
        require(!initialBalanceSet, "Initial balance has already been set");
        require(amount > 0, "Initial balance amount must be greater than zero");
        require(hasBalance[account] == false, "Initial balance has already been set for account");
    
    initialBalance = amount;
    hasBalance[account] = true;
    initialBalanceSet = true;
    }

    modifier rewardClaimTimePassed() {
    require(block.timestamp >= rewardClaimTime, "reward claim timeframe has not passed");
    _;
    }

    
    function claimReward() public onlyOwner timestampIsSet noReentrant rewardClaimTimePassed {
    uint256 erc20Balance = erc20Contract.balanceOf(address(this));
    uint256 additionalTokens = erc20Balance - initialBalance;
    uint256 percentage = (additionalTokens * 100) / initialBalance;
    uint256 reward = (percentage * initialBalance) / 100;
    uint256 totalReward = reward + alreadyWithdrawn[owner];
    
    uint256 maxWithdrawal = erc20Balance / 100; //withdraw 1% as a reward
    if (totalReward > maxWithdrawal) {
        totalReward = maxWithdrawal;
    }
    
    require(reward > 0, "No additional rewards to claim");
    
    alreadyWithdrawn[owner] = alreadyWithdrawn[owner].add(totalReward);
    erc20Contract.safeTransfer(owner, totalReward);
    }

    function finalizeAllIncomingDeposits() public onlyOwner timestampIsSet incomingDepositsStillAllowed {
    allIncomingDepositsFinalised = true;
    }

    function setTimestamp(uint256 _timePeriodInSeconds) public onlyOwner timestampNotSet {
    timestampSet = true;
    initialTimestamp = block.timestamp;
    timePeriod = initialTimestamp.add(_timePeriodInSeconds);
    }

    function withdrawEth(uint256 amount) public onlyOwner noReentrant{
        require(amount <= contractBalance, "Insufficient funds");
    
    contractBalance = contractBalance.sub(amount);
    owner.transfer(amount);
    }

    function lockTokens(address recipient) public onlyOwner timestampIsSet incomingDepositsStillAllowed {
        uint256 erc20Balance = erc20Contract.balanceOf(address(this));
    
    balances[recipient] = erc20Balance;
    emit AllocationPerformed(recipient, erc20Balance);
    }

    function UnlockTokens(IERC20 token, address to, uint256 amount) public timestampIsSet noReentrant {
        require(balances[to] >= amount, "Insufficient token balance, try lesser amount");
        require(msg.sender == to, "Only the token recipient can perform the unlock");
        require(token == erc20Contract, "Token parameter must be the same as the erc20 contract address which was passed into the constructor");
    
    if (block.timestamp >= timePeriod) {
    alreadyWithdrawn[to] = alreadyWithdrawn[to].add(amount);
    balances[to] = balances[to].sub(amount);
    token.safeTransfer(to, amount);

    emit TokensUnlocked(to, amount);
    } 
    else {
    
    revert("Tokens are only available after correct time period has elapsed");
    }
    }

    function transferAccidentallyLockedTokens(IERC20 token, uint256 amount) public onlyOwner noReentrant {
        require(address(token) != address(0), "Token address can not be zero");
        require(token != erc20Contract, "Token address can not be ERC20 address which was passed into the constructor");
    
    token.safeTransfer(owner, amount);
    }
}