// File: contracts/StakeManager.sol



pragma solidity >=0.8.0;

import "./interfaces/IStake.sol";
import "./Stake.sol";
import "./Base.sol";

contract StakeManager is Base {
    event StakeCreated(address indexed from, address stakeToken, address rewardToken, address stake);

    event LibUpdated(address indexed newLib);

    address[] public stakes;

    address lib;

    constructor(address _config, address _lib) Base(_config) {
        require(_lib != address(0), "lib address = 0");
        lib = _lib;
    }

    /**
     * @dev update stake library
     */
    function updateLib(address _lib) external onlyCEO() {
        require(_lib != address(0), "lib address = 0");
        lib = _lib;
        emit LibUpdated(_lib);
    }

    /**
     * @dev return number of stake
     */
    function stakeCount() external view returns (uint256) {
        return stakes.length;
    }

    /**
     * @dev return array of all stake contracts
     * @return array of stakes
     */
    function allStakes() external view returns (address[] memory) {
        return stakes;
    }

    /**
     * @dev claim rewards of sepcified address of stakes
     */
    function claims(address[] calldata _stakes) external {
        for (uint256 i; i < _stakes.length; i++) {
            IStake(_stakes[i]).claim0(msg.sender);
        }
    }

    /**
     * @dev create a new stake contract
     * @param _stakeToken address of stakeable token
     * @param _startDate epoch seconds of mining start
     * @param _endDate epoch seconds of mining complete
     * @param _totalReward reward total
     */
    function createStake(
        address _stakeToken,
        uint256 _startDate,
        uint256 _endDate,
        uint256 _totalReward
    ) external onlyCEO() {
        require(_stakeToken != address(0), "zero address");
        require(_endDate > _startDate, "_endDate <= _startDate");
        address rewardToken = config.protocolToken();

        address stakeAddress = clone(lib);
        IStake(stakeAddress).initialize(_stakeToken, rewardToken, _startDate, _endDate, _totalReward);
        TransferHelper.safeTransferFrom(rewardToken, msg.sender, stakeAddress, _totalReward);
        stakes.push(stakeAddress);
        emit StakeCreated(msg.sender, _stakeToken, rewardToken, stakeAddress);
        config.notify(IConfig.EventType.STAKE_CREATED, stakeAddress);
    }

    function clone(address master) internal returns (address instance) {
        // solhint-disable-next-line no-inline-assembly
        assembly {
            let ptr := mload(0x40)
            mstore(ptr, 0x3d602d80600a3d3981f3363d3d373d3d3d363d73000000000000000000000000)
            mstore(add(ptr, 0x14), shl(0x60, master))
            mstore(add(ptr, 0x28), 0x5af43d82803e903d91602b57fd5bf30000000000000000000000000000000000)
            instance := create(0, ptr, 0x37)
        }
        require(instance != address(0), "ERC1167: create failed");
    }
}

// File: contracts/interfaces/IStake.sol



pragma solidity >=0.8.0;

interface IStake {
    function claim0(address _owner) external;

    function initialize(
        address stakeToken,
        address rewardToken,
        uint256 start,
        uint256 end,
        uint256 rewardPerBlock
    ) external;
}

// File: contracts/Stake.sol



pragma solidity >=0.8.0;

import "./interfaces/IERC20.sol";
import "./interfaces/IConfig.sol";
import "./interfaces/IStake.sol";
import "./libs/TransferHelper.sol";

interface IConfigable {
    function getConfig() external returns (IConfig);
}

contract Stake is IStake {
    event StakeUpdated(address indexed staker, bool isIncrease, uint256 stakeChanged, uint256 stakeAmount);

    event Claimed(address indexed staker, uint256 reward);

    // stake token address, ERC20
    address public stakeToken;

    bool initialized;

    bool locker;

    // reward token address, ERC20
    address public rewardToken;

    // Mining start date(epoch second)
    uint256 public startDateOfMining;

    // Mining end date(epoch second)
    uint256 public endDateOfMining;

    // controller address
    address controller;

    // reward per Second
    uint256 public rewardPerSecond;

    // timestamp of last updated
    uint256 public lastUpdatedTimestamp;

    uint256 public rewardPerTokenStored;

    // staked total
    uint256 private _totalSupply;

    struct StakerInfo {
        // exclude reward's amount
        uint256 rewardDebt;
        // stake total
        uint256 amount;
        // pending reward
        uint256 reward;
    }

    // staker's StakerInfo
    mapping(address => StakerInfo) public stakers;

    /* ========== MODIFIER ========== */

    modifier stakeable() {
        require(block.timestamp <= endDateOfMining, "stake not begin or complete");
        _;
    }

    modifier enable() {
        require(initialized, "initialized = false");
        _;
    }

    modifier onlyController {
        require(controller == msg.sender, "only controller");
        _;
    }

    modifier lock() {
        require(locker == false, "locked");
        locker = true;
        _;
        locker = false;
    }

    modifier updateReward(address _staker) {
        rewardPerTokenStored = rewardPerToken();
        lastUpdatedTimestamp = lastTimeRewardApplicable();
        if (_staker != address(0) && stakers[_staker].amount > 0) {
            stakers[_staker].reward = rewardOf(_staker);
            stakers[_staker].rewardDebt = rewardPerTokenStored;
        }
        _;
    }

    constructor() {}

    /* ========== MUTATIVE FUNCTIONS ========== */
    /**
     * @dev initialize contract
     * @param _stake address of stake token
     * @param _reward address of reward token
     * @param _start epoch seconds of mining starts
     * @param _end epoch seconds of mining complete
     * @param _totalReward totalReward
     */
    function initialize(
        address _stake,
        address _reward,
        uint256 _start,
        uint256 _end,
        uint256 _totalReward
    ) external override {
        require(!initialized, "initialized = true");
        // only initialize once
        initialized = true;
        controller = msg.sender;
        stakeToken = _stake;
        rewardToken = _reward;
        startDateOfMining = _start;
        endDateOfMining = _end;
        rewardPerSecond = _totalReward / (_end - _start + 1);
    }

    /**
     * @dev stake token
     * @param _amount amount of token to be staked
     */
    function stake(uint256 _amount) external enable() lock() updateReward(msg.sender) {
        require(_amount > 0, "amount = 0");
        require(block.timestamp <= endDateOfMining, "stake not begin or complete");
        _totalSupply += _amount;
        stakers[msg.sender].amount += _amount;
        TransferHelper.safeTransferFrom(stakeToken, msg.sender, address(this), _amount);
        emit StakeUpdated(msg.sender, true, _amount, stakers[msg.sender].amount);
        _notify();
    }

    /**
     * @dev unstake token
     * @param _amount amount of token to be unstaked
     */
    function unstake(uint256 _amount) public enable() lock() updateReward(msg.sender) {
        require(_amount > 0, "amount = 0");
        require(stakers[msg.sender].amount >= _amount, "insufficient amount");
        _claim(msg.sender);
        _totalSupply -= _amount;
        stakers[msg.sender].amount -= _amount;
        TransferHelper.safeTransfer(stakeToken, msg.sender, _amount);
        emit StakeUpdated(msg.sender, false, _amount, stakers[msg.sender].amount);
        _notify();
    }

    /**
     * @dev claim rewards
     */
    function claim() external enable() lock() updateReward(msg.sender) {
        _claim(msg.sender);
    }

    /**
     * @dev quit, claim reward + unstake all
     */
    function quit() external enable() lock() updateReward(msg.sender) {
        unstake(stakers[msg.sender].amount);
        _claim(msg.sender);
    }

    /**
     * @dev claim rewards, only owner allowed
     * @param _staker staker address
     */
    function claim0(address _staker) external override onlyController() enable() updateReward(msg.sender) {
        _claim(_staker);
    }

    /* ========== VIEWs ========== */
    function lastTimeRewardApplicable() public view returns (uint256) {
        if (block.timestamp < startDateOfMining) return startDateOfMining;
        return block.timestamp > endDateOfMining ? endDateOfMining : block.timestamp;
    }

    function rewardPerToken() public view returns (uint256) {
        if (_totalSupply == 0 || block.timestamp < startDateOfMining) {
            return rewardPerTokenStored;
        }
        return
            rewardPerTokenStored +
            ((lastTimeRewardApplicable() - lastUpdatedTimestamp) * rewardPerSecond * 1e18) /
            _totalSupply;
    }

    /**
     * @dev amount of stake per address
     * @param _staker staker address
     * @return amount of stake
     */
    function stakeOf(address _staker) external view returns (uint256) {
        return stakers[_staker].amount;
    }

    /**
     * @dev amount of reward per address
     * @param _staker address
     * @return value reward amount of _staker
     */
    function rewardOf(address _staker) public view returns (uint256 value) {
        StakerInfo memory info = stakers[_staker];
        return (info.amount * rewardPerToken() - info.rewardDebt) / 1e18 + info.reward;
    }

    /* ========== INTERNAL FUNCTIONS ========== */
    /**
     * @dev claim reward
     * @param _staker address
     */
    function _claim(address _staker) private {
        uint256 reward = stakers[_staker].reward;
        if (reward > 0) {
            stakers[_staker].reward = 0;
            IConfig config = IConfigable(controller).getConfig();
            uint256 claimFeeRate = config.claimFeeRate();
            uint256 out = (reward * (10000 - claimFeeRate)) / 10000;
            uint256 fee = reward - out;
            TransferHelper.safeTransfer(rewardToken, _staker, out);

            if (fee > 0) {
                // transfer to feeTo
                TransferHelper.safeTransfer(rewardToken, config.feeTo(), fee);
            }
            emit Claimed(_staker, reward);
            _notify();
        }
    }

    function _notify() private {
        IConfigable(controller).getConfig().notify(IConfig.EventType.STAKE_UPDATED, address(this));
    }
}

// File: contracts/Base.sol



pragma solidity >=0.8.0;

import "./interfaces/IConfig.sol";

contract Base {
    event ConfigUpdated(address indexed owner, address indexed config);

    IConfig internal config;

    modifier onlyCEO() {
        require(msg.sender == config.ceo(), "only CEO");
        _;
    }

    constructor(address _configAddr) {
        require(_configAddr != address(0), "config address = 0");
        config = IConfig(_configAddr);
    }

    function updateConfig(address _config) external onlyCEO() {
        require(_config != address(0), "config address = 0");
        require(address(config) != _config, "address identical");
        config = IConfig(_config);
        emit ConfigUpdated(msg.sender, _config);
    }

    function configAddress() external view returns (address) {
        return address(config);
    }

    function getConfig() external view returns (IConfig) {
        return config;
    }
}

// File: contracts/interfaces/IERC20.sol



pragma solidity >=0.8.0;

interface IERC20 {
    event Approval(address indexed owner, address indexed spender, uint256 value);
    event Transfer(address indexed from, address indexed to, uint256 value);

    function name() external view returns (string memory);

    function symbol() external view returns (string memory);

    function decimals() external view returns (uint8);

    function totalSupply() external view returns (uint256);

    function balanceOf(address owner) external view returns (uint256);

    function allowance(address owner, address spender) external view returns (uint256);

    function approve(address spender, uint256 value) external returns (bool);

    function transfer(address to, uint256 value) external returns (bool);

    function transferFrom(
        address from,
        address to,
        uint256 value
    ) external returns (bool);
}

// File: contracts/interfaces/IConfig.sol



pragma solidity >=0.8.0;

interface IConfig {
    enum EventType {FUND_CREATED, FUND_UPDATED, STAKE_CREATED, STAKE_UPDATED, REG_CREATED, REG_UPDATED, PFUND_CREATED, PFUND_UPDATED}

    function ceo() external view returns (address);

    function protocolPool() external view returns (address);

    function protocolToken() external view returns (address);

    function feeTo() external view returns (address);

    function nameRegistry() external view returns (address);

    //  function investTokenWhitelist() external view returns (address[] memory);

    function tokenMinFundSize(address token) external view returns (uint256);

    function investFeeRate() external view returns (uint256);

    function redeemFeeRate() external view returns (uint256);

    function claimFeeRate() external view returns (uint256);

    function poolCreationRate() external view returns (uint256);

    function slot0() external view returns (uint256);

    function slot1() external view returns (uint256);

    function slot2() external view returns (uint256);

    function slot3() external view returns (uint256);

    function slot4() external view returns (uint256);

    function notify(EventType _type, address _src) external;
}

// File: contracts/libs/TransferHelper.sol



pragma solidity >=0.8.0;

// helper methods for interacting with ERC20 tokens and sending ETH that do not consistently return true/false
library TransferHelper {
    function safeApprove(
        address token,
        address to,
        uint256 value
    ) internal {
        // bytes4(keccak256(bytes('approve(address,uint256)')));
        (bool success, bytes memory data) =
            token.call(abi.encodeWithSelector(0x095ea7b3, to, value));
        require(
            success && (data.length == 0 || abi.decode(data, (bool))),
            'TransferHelper: APPROVE_FAILED'
        );
    }

    function safeTransfer(
        address token,
        address to,
        uint256 value
    ) internal {
        // bytes4(keccak256(bytes('transfer(address,uint256)')));
        (bool success, bytes memory data) =
            token.call(abi.encodeWithSelector(0xa9059cbb, to, value));
        require(
            success && (data.length == 0 || abi.decode(data, (bool))),
            'TransferHelper: TRANSFER_FAILED'
        );
    }

    function safeTransferFrom(
        address token,
        address from,
        address to,
        uint256 value
    ) internal {
        // bytes4(keccak256(bytes('transferFrom(address,address,uint256)')));
        (bool success, bytes memory data) =
            token.call(abi.encodeWithSelector(0x23b872dd, from, to, value));
        require(
            success && (data.length == 0 || abi.decode(data, (bool))),
            'TransferHelper: TRANSFER_FROM_FAILED'
        );
    }

    function safeTransferETH(address to, uint256 value) internal {
        (bool success, ) = to.call{value: value}(new bytes(0));
        require(success, 'TransferHelper: ETH_TRANSFER_FAILED');
    }
}