@v4.5.0
/**
 * @title SafeERC20
 * @dev Wrappers around ERC20 operations that throw on failure (when the token
 * contract returns false). Tokens that return no value (and instead revert or
 * throw on failure) are also supported, non-reverting calls are assumed to be
 * successful.
 * To use this library you can add a `using SafeERC20 for IERC20;` statement to your contract,
 * which allows you to call the safe operations as `token.safeTransfer(...)`, etc.
 */
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)
        );
    }

    /**
     * @dev Deprecated. This function has issues similar to the ones found in
     * {IERC20-approve}, and its usage is discouraged.
     *
     * Whenever possible, use {safeIncreaseAllowance} and
     * {safeDecreaseAllowance} instead.
     */
    function safeApprove(
        IERC20 token,
        address spender,
        uint256 value
    ) internal {
        // safeApprove should only be called when setting an initial allowance,
        // or when resetting it to zero. To increase and decrease it, use
        // 'safeIncreaseAllowance' and 'safeDecreaseAllowance'
        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
                )
            );
        }
    }

    /**
     * @dev Imitates a Solidity high-level call (i.e. a regular function call to a contract), relaxing the requirement
     * on the return value: the return value is optional (but if data is returned, it must not be false).
     * @param token The token targeted by the call.
     * @param data The call data (encoded using abi.encode or one of its variants).
     */
    function _callOptionalReturn(IERC20 token, bytes memory data) private {
        // We need to perform a low level call here, to bypass Solidity's return data size checking mechanism, since
        // we're implementing it ourselves. We use {Address.functionCall} to perform this call, which verifies that
        // the target address contains contract code and also asserts for success in the low-level call.

        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"
            );
        }
    }
}

contract MarsStaking is Ownable, ReentrancyGuard {
    using SafeMath for uint256;
    using SafeERC20 for IERC20;

    struct UserInfo {
        uint256 stakedAmount; // How many MARS tokens the user has provided.
        uint256 rewardDebt; // number of reward token user will get. it is cleared when user stake or unstake token.
        uint256 lastDepositTime; // time when user deposited
        uint256 unstakeStartTime; // time when when user clicked unstake btn.
        uint256 pendingAmount; // # of tokens that is pending of 7 days.
    }

    event TokenStaked(address user, uint256 amount);
    event TokenWithdraw(address user, uint256 amount);
    event ClaimToken(address user, uint256 receiveAmount);
    event HarvestToken(address user, uint256 receiveAmount);
    event RewardRateSet(uint256 value);
    event RewardReceived(address receiver, uint256 rewardAmount);

    address public marsToken;
    mapping(address => UserInfo) public userInfos;

    uint256 public totalStakedAmount;
    uint256 public rewardRate;
    uint256 public UNSTAKE_TIMEOFF = 1 days;
    uint256 public emergencyPercentageToWithdraw = 91; // 91%

    constructor(address _token) {
        require(_token != address(0), "Zero address: Token");
        marsToken = _token;
        totalStakedAmount = 0;
        rewardRate = 50; // APY 50%
    }

    function setRewardRate(uint256 _newRate) public onlyOwner {
        require(_newRate != 0, "rate should be over 1");
        rewardRate = _newRate;
        emit RewardRateSet(_newRate);
    }

    function stakeToken(uint256 _amount)
        external
        nonReentrant
        updateReward(msg.sender)
    {
        require(_amount > 0, "invalid deposit amount");

        userInfos[msg.sender].stakedAmount = userInfos[msg.sender]
            .stakedAmount
            .add(_amount);
        userInfos[msg.sender].lastDepositTime = block.timestamp;
        IERC20(marsToken).safeTransferFrom(msg.sender, address(this), _amount);
        totalStakedAmount = totalStakedAmount.add(_amount);
        emit TokenStaked(msg.sender, _amount);
    }

    /**
    7 days Timer will start
  **/
    function unstakeToken(uint256 _amount, bool isEmergency)
        external
        nonReentrant
        updateReward(msg.sender)
    {
        require(_amount > 0, "invalid deposit amount");
        require(
            userInfos[msg.sender].stakedAmount >= _amount,
            "unstake amount is bigger than you staked"
        );

        uint256 outAmount = _amount + userInfos[msg.sender].rewardDebt;

        if (isEmergency) {
            outAmount = outAmount.mul(emergencyPercentageToWithdraw).div(100);
        }

        userInfos[msg.sender].stakedAmount = userInfos[msg.sender]
            .stakedAmount
            .sub(_amount);
        userInfos[msg.sender].rewardDebt = 0;
        userInfos[msg.sender].lastDepositTime = block.timestamp;

        if (isEmergency) {
            // send token to msg.sender.
            IERC20(marsToken).safeTransfer(msg.sender, outAmount);
            emit ClaimToken(msg.sender, outAmount);
            return;
        }

        userInfos[msg.sender].unstakeStartTime = block.timestamp;
        userInfos[msg.sender].pendingAmount =
            userInfos[msg.sender].pendingAmount +
            outAmount;

        emit TokenWithdraw(msg.sender, outAmount);
    }

    function harvest() external nonReentrant updateReward(msg.sender) {
        require(userInfos[msg.sender].rewardDebt > 0, "No rewards to harvest!");

        uint256 outAmount = userInfos[msg.sender].rewardDebt;

        userInfos[msg.sender].rewardDebt = 0;
        userInfos[msg.sender].lastDepositTime = block.timestamp;

        IERC20(marsToken).safeTransfer(msg.sender, outAmount);
        emit HarvestToken(msg.sender, outAmount);
    }

    // this function will be called after 7 days. actual token transfer happens here.
    function claim() external nonReentrant {
        require(
            (block.timestamp - userInfos[msg.sender].unstakeStartTime) >=
                UNSTAKE_TIMEOFF,
            "invalid time: must be greater than 7 days"
        );
        uint256 receiveAmount = userInfos[msg.sender].pendingAmount;
        require(receiveAmount > 0, "no available amount");
        require(
            IERC20(marsToken).balanceOf(address(this)) >= receiveAmount,
            "staking contract has not enough MARS token"
        );

        IERC20(marsToken).safeTransfer(msg.sender, receiveAmount);
        totalStakedAmount = IERC20(marsToken).balanceOf(address(this)).sub(
            receiveAmount
        );
        userInfos[msg.sender].pendingAmount = 0;
        userInfos[msg.sender].unstakeStartTime = block.timestamp;

        emit ClaimToken(msg.sender, receiveAmount);
    }

    function isClaimable(address user) external view returns (bool) {
        if (userInfos[user].unstakeStartTime == 0) return false;

        return
            (block.timestamp - userInfos[user].unstakeStartTime >
                UNSTAKE_TIMEOFF)
                ? true
                : false;
    }

    function timeDiffForClaim(address user) external view returns (uint256) {
        return
            (userInfos[user].unstakeStartTime + UNSTAKE_TIMEOFF >
                block.timestamp)
                ? userInfos[user].unstakeStartTime +
                    UNSTAKE_TIMEOFF -
                    block.timestamp
                : 0;
    }

    function setUnstakeTimeoff(uint256 timeInMinutes) external onlyOwner {
        require(
            timeInMinutes > 0 && timeInMinutes <= 525600,
            "unstake time off should be between 0 and 525600 (1 year)"
        );
        UNSTAKE_TIMEOFF = timeInMinutes * 1 minutes;
    }

    function setEmergencyWithdrawal(uint256 percentageToWithdraw)
        external
        onlyOwner
    {
        require(
            percentageToWithdraw >= 90 && percentageToWithdraw <= 100,
            "emergency to withdraw percentage should be 90-100%"
        );
        emergencyPercentageToWithdraw = percentageToWithdraw;
    }

    function calcReward(address account) public view returns (uint256) {
        uint256 rewardVal = (block.timestamp -
            userInfos[account].lastDepositTime)
            .mul(userInfos[account].stakedAmount)
            .mul(rewardRate)
            .div(365 days);
        return rewardVal.div(100).add(userInfos[account].rewardDebt);
    }

    modifier updateReward(address account) {
        if (account != address(0)) {
            if (userInfos[account].lastDepositTime == 0) {
                userInfos[account].lastDepositTime = block.timestamp;
                userInfos[account].rewardDebt = 0;
            } else {
                userInfos[account].rewardDebt = calcReward(account);
            }
        }
        _;
    }
}
[{"inputs":[{"internalType":"address","name":"_token","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"user","type":"address"},{"indexed":false,"internalType":"uint256","name":"receiveAmount","type":"uint256"}],"name":"ClaimToken","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"user","type":"address"},{"indexed":false,"internalType":"uint256","name":"receiveAmount","type":"uint256"}],"name":"HarvestToken","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":false,"internalType":"uint256","name":"value","type":"uint256"}],"name":"RewardRateSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"receiver","type":"address"},{"indexed":false,"internalType":"uint256","name":"rewardAmount","type":"uint256"}],"name":"RewardReceived","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"user","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"TokenStaked","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"user","type":"address"},{"indexed":false,"internalType":"uint256","name":"amount","type":"uint256"}],"name":"TokenWithdraw","type":"event"},{"inputs":[],"name":"UNSTAKE_TIMEOFF","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"account","type":"address"}],"name":"calcReward","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"claim","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"emergencyPercentageToWithdraw","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"harvest","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"user","type":"address"}],"name":"isClaimable","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"marsToken","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"renounceOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"rewardRate","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"percentageToWithdraw","type":"uint256"}],"name":"setEmergencyWithdrawal","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_newRate","type":"uint256"}],"name":"setRewardRate","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"timeInMinutes","type":"uint256"}],"name":"setUnstakeTimeoff","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"_amount","type":"uint256"}],"name":"stakeToken","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"user","type":"address"}],"name":"timeDiffForClaim","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"totalStakedAmount","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":"uint256","name":"_amount","type":"uint256"},{"internalType":"bool","name":"isEmergency","type":"bool"}],"name":"unstakeToken","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"userInfos","outputs":[{"internalType":"uint256","name":"stakedAmount","type":"uint256"},{"internalType":"uint256","name":"rewardDebt","type":"uint256"},{"internalType":"uint256","name":"lastDepositTime","type":"uint256"},{"internalType":"uint256","name":"unstakeStartTime","type":"uint256"},{"internalType":"uint256","name":"pendingAmount","type":"uint256"}],"stateMutability":"view","type":"function"}]