contract CrabStrategy is IStrategyV2, Governance {
    using SafeMath for uint256;

    struct vaultData {
        IUniswapV3Pool pool;
        CoinsulUniV3Vault vault;
        int24 baseThreshold;
        int24 limitThreshold;
        uint256 period;
        int24 minTickMove;
        int24 maxTwapDeviation;
        uint32 twapDuration;
        uint256 lastTimestamp;
        int24 tickSpacing;
        int24 lastTick;
        bool isInitialized;
    }

    mapping(address => vaultData) public vaultStrategyData;

    address public keeper;

    /**
     * @param _keeper Account that can call `rebalance()`
     */
    constructor(address _keeper) {
        keeper = _keeper;
    }

    /**
     * @param _vaultLoc Underlying Coinsul Uni V3 Vault
     * @param _baseThreshold Used to determine base order range
     * @param _limitThreshold Used to determine limit order range
     * @param _period Can only rebalance if this length of time has passed
     * @param _minTickMove Can only rebalance if price has moved at least this much
     * @param _maxTwapDeviation Max deviation from TWAP during rebalance
     * @param _twapDuration TWAP duration in seconds for deviation check
     */
    function registerVault(
        address _vaultLoc,
        int24 _baseThreshold,
        int24 _limitThreshold,
        uint256 _period,
        int24 _minTickMove,
        int24 _maxTwapDeviation,
        uint32 _twapDuration
    ) public onlyGovernance {
        CoinsulUniV3Vault _vault = CoinsulUniV3Vault(_vaultLoc);
        IUniswapV3Pool _pool = _vault.pool();
        int24 _tickSpacing = _pool.tickSpacing();
        _checkThreshold(_baseThreshold, _tickSpacing);
        _checkThreshold(_limitThreshold, _tickSpacing);

        require(_minTickMove >= 0, "minTickMove must be >= 0");
        require(_maxTwapDeviation >= 0, "maxTwapDeviation must be >= 0");
        require(_twapDuration > 0, "twapDuration must be > 0");
        int24 _lastTick;
        (, _lastTick, , , , , ) = _pool.slot0();

        vaultStrategyData[_vaultLoc] = vaultData({
            pool: _pool,
            vault: _vault,
            baseThreshold: _baseThreshold,
            limitThreshold: _limitThreshold,
            period: _period,
            minTickMove: _minTickMove,
            maxTwapDeviation: _maxTwapDeviation,
            twapDuration: _twapDuration,
            lastTimestamp: 0x0,
            tickSpacing: _tickSpacing,
            lastTick: _lastTick,
            isInitialized: true
        });
    }

    /**
     * @notice Calculates new ranges for orders and calls `vault.rebalance()`
     * so that vault can update its positions. Can only be called by keeper.
     */
    function rebalance(address _vault) external override {
        require(shouldRebalance(_vault), "cannot rebalance");

        vaultData storage _data = vaultStrategyData[_vault];
        int24 tick = getTick(_data.pool);
        int24 tickFloor = _floor(tick, _data.tickSpacing);
        int24 tickCeil = tickFloor + _data.tickSpacing;

        vaultStrategyData[_vault].vault.rebalance(
            0,
            0,
            tickFloor - _data.baseThreshold,
            tickCeil + _data.baseThreshold,
            tickFloor - _data.limitThreshold,
            tickFloor,
            tickCeil,
            tickCeil + _data.limitThreshold
        );

        _data.lastTimestamp = block.timestamp;
        _data.lastTick = tick;
    }

    function shouldRebalance(address _vault) public view override returns (bool) {
        //require that a vault be registered before it can be rebalanced
        require(vaultStrategyData[_vault].isInitialized, "vault not registered");

        // check called by keeper
        if (msg.sender != keeper) {
            return false;
        }

        vaultData memory _data = vaultStrategyData[_vault];

        // check enough time has passed
        if (block.timestamp < _data.lastTimestamp.add(_data.period)) {
            return false;
        }

        // check price has moved enough
        int24 tick = getTick(_data.pool);
        int24 tickMove = tick > _data.lastTick ? tick - _data.lastTick : _data.lastTick - tick;
        if (tickMove < _data.minTickMove) {
            return false;
        }

        // check price near twap
        int24 twap = getTwap(_data.pool, _data.twapDuration);
        int24 twapDeviation = tick > twap ? tick - twap : twap - tick;
        if (twapDeviation > _data.maxTwapDeviation) {
            return false;
        }

        // check price not too close to boundary
        int24 maxThreshold = _data.baseThreshold > _data.limitThreshold
            ? _data.baseThreshold
            : _data.limitThreshold;
        if (tick < TickMath.MIN_TICK + maxThreshold + _data.tickSpacing) {
            return false;
        }
        if (tick > TickMath.MAX_TICK - maxThreshold - _data.tickSpacing) {
            return false;
        }

        return true;
    }

    function getTick(IUniswapV3Pool _pool) public view returns (int24) {
        (, int24 tick, , , , , ) = _pool.slot0();
        return tick;
    }

    /// @dev Fetches time-weighted average price in ticks from Uniswap pool.
    function getTwap(IUniswapV3Pool pool, uint32 _twapDuration) public view returns (int24) {
        uint32[] memory secondsAgo = new uint32[](2);
        secondsAgo[0] = _twapDuration;
        secondsAgo[1] = 0;

        (int56[] memory tickCumulatives, ) = pool.observe(secondsAgo);
        return int24((tickCumulatives[1] - tickCumulatives[0]) / _twapDuration);
    }

    /// @dev Rounds tick down towards negative infinity so that it's a multiple
    /// of `tickSpacing`.
    function _floor(int24 tick, int24 tickSpacing) internal view returns (int24) {
        int24 compressed = tick / tickSpacing;
        if (tick < 0 && tick % tickSpacing != 0) compressed--;
        return compressed * tickSpacing;
    }

    function _checkThreshold(int24 threshold, int24 _tickSpacing) internal pure {
        require(threshold > 0, "threshold must be > 0");
        require(threshold <= TickMath.MAX_TICK, "threshold too high");
        require(threshold % _tickSpacing == 0, "threshold must be multiple of tickSpacing");
    }

    function setKeeper(address _keeper) external onlyGovernance {
        keeper = _keeper;
    }

    function setBaseThreshold(address _vault, int24 _baseThreshold)
        external
        onlyVaultGovernance(_vault)
    {
        _checkThreshold(_baseThreshold, vaultStrategyData[_vault].tickSpacing);
        vaultStrategyData[_vault].baseThreshold = _baseThreshold;
    }

    function setLimitThreshold(address _vault, int24 _limitThreshold)
        external
        onlyVaultGovernance(_vault)
    {
        _checkThreshold(_limitThreshold, vaultStrategyData[_vault].tickSpacing);
        vaultStrategyData[_vault].limitThreshold = _limitThreshold;
    }

    function setPeriod(address _vault, uint256 _period) external onlyVaultGovernance(_vault) {
        vaultStrategyData[_vault].period = _period;
    }

    function setMinTickMove(address _vault, int24 _minTickMove)
        external
        onlyVaultGovernance(_vault)
    {
        require(_minTickMove >= 0, "minTickMove must be >= 0");
        vaultStrategyData[_vault].minTickMove = _minTickMove;
    }

    function setMaxTwapDeviation(address _vault, int24 _maxTwapDeviation)
        external
        onlyVaultGovernance(_vault)
    {
        require(_maxTwapDeviation >= 0, "maxTwapDeviation must be >= 0");
        vaultStrategyData[_vault].maxTwapDeviation = _maxTwapDeviation;
    }

    function setTwapDuration(address _vault, uint32 _twapDuration)
        external
        onlyVaultGovernance(_vault)
    {
        require(_twapDuration > 0, "twapDuration must be > 0");
        vaultStrategyData[_vault].twapDuration = _twapDuration;
    }

    /// @dev uses same governance as underlying vault.
    modifier onlyVaultGovernance(address _vault) {
        CoinsulUniV3Vault vault = vaultStrategyData[_vault].vault;
        require(_vault == address(vault), "vault not registered");
        require(msg.sender == vault.governance(), "governance");
        _;
    }
}
/**
 * @title   Crab Strategy
 *
 * @notice Rebalancing strategy for Coinsul Uni V3 Vault that maintains the two
 *         following range orders:
 *
 *          1. Base order is placed between X - B and X + B + TS.
 *          2. Limit order is placed between X - L and X, or between X + TS
 *             and X + L + TS, depending on which token it holds more of.
 *
 *          where:
 *
 *              X = current tick rounded down to multiple of tick spacing
 *              TS = tick spacing
 *              B = base threshold
 *              L = limit threshold
 *
 *          Note that after these two orders, the vault should have deposited
 *          all its tokens and should only have a few wei left.
 *
 *          Because the limit order tries to sell whichever token the vault
 *          holds more of, the vault's holdings will have a tendency to get
 *          closer to a 1:1 balance. This enables it to continue providing
 *          liquidity without running out of inventory of either token, and
 *          achieves this without the need to swap directly on Uniswap and pay
 *          fees.
 */
function getTwap(IUniswapV3Pool pool, uint32 _twapDuration) public view returns (int24) {
    uint32[] memory secondsAgo = new uint32[](2);
    secondsAgo[0] = _twapDuration;
    secondsAgo[1] = 0;

    (int56[] memory tickCumulatives, ) = pool.observe(secondsAgo);
    return int24((tickCumulatives[1] - tickCumulatives[0]) / _twapDuration);
}

/// @dev Fetches time-weighted average price in ticks from Uniswap pool.