// SPDX-License-Identifier: MIT

/**
@tehshori
*/

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 yShoriLocker {
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;
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;
}

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 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 LockShori(address recipient, uint256 amount) public onlyOwner timestampIsSet incomingDepositsStillAllowed {
require(recipient != address(0), "ERC20: transfer to the zero address");
balances[recipient] = balances[recipient].add(amount);
emit AllocationPerformed(recipient, amount);
}

function bulkDepositTokens(address[] calldata recipients, uint256[] calldata amounts) external onlyOwner timestampIsSet incomingDepositsStillAllowed {
require(recipients.length == amounts.length, "The recipients and amounts arrays must be the same size in length");
for(uint256 i = 0; i < recipients.length; i++) {
require(recipients[i] != address(0), "ERC20: transfer to the zero address");
balances[recipients[i]] = balances[recipients[i]].add(amounts[i]);
emit AllocationPerformed(recipients[i], amounts[i]);
}
}

function UnlockShori(IERC20 token, address to, uint256 amount) public timestampIsSet noReentrant {
require(to != address(0), "ERC20: transfer to the zero address");
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);
}
}