// File: contracts/WiseOracleHub/WiseOracleHub.sol



// SPDX-License-Identifier: -- WISE --

pragma solidity =0.8.21;

/**
 * @author Christoph Krpoun
 * @author René Hochmuth
 * @author Vitally Marinchenko
 */

import "./OracleHelper.sol";
import "../OwnableMaster.sol";

/**
 * @dev WiseOracleHub is an onchain extension for price feeds (chainLink or others).
 * The master address is owned by a timelock contract which itself is secured by a
 * multisig. Only the master can add new price feed <-> address pairs to the contract.
 *
 * One advantage is the linking of price feeds to their underlying token address.
 * Therefore, users can get the current USD value of a token by just knowing the token
 * address when calling {latestResolver}. It takes the answer from {latestRoundData}
 * for chainLink oracles as recommended from chainLink.
 *
 * NOTE: If you want to propose adding an own developed price feed it is
 * mandatory to wrap its answer into a function mimicking {latestRoundData}
 * (See {latestResolver} implementation).
 *
 * Additionally, the oracleHub provides so called heartbeat checks if a token gets
 * still updated in expected time intervall.
 *
 */

contract WiseOracleHub is OracleHelper, OwnableMaster {

    constructor()
        Declarations()
        OwnableMaster(msg.sender)
    {}

    /**
     * @dev Returns USD values decimals
     * meaning that 1.00 USD <=> 1E18.
     */
    function decimalsUSD()
        external
        pure
        returns (uint8)
    {
        return _decimalsUSD;
    }

    /**
     * @dev Returns priceFeed latest USD value
     * by passing the underlying token address.
     */
    function latestResolver(
        address _tokenAddress
    )
        public
        view
        returns (uint256)
    {
        (
                ,
                int256 answer,
                ,
                ,

            ) = priceFeed[_tokenAddress].latestRoundData();

        return uint256(answer);
    }

    /**
     * @dev Returns priceFeed decimals by
     * passing the underlying token address.
     */
    function decimals(
        address _tokenAddress
    )
        public
        view
        returns (uint8)
    {
        return priceFeed[_tokenAddress].decimals();
    }

    function getTokenDecimals(
        address _tokenAddress
    )
        external
        view
        returns (uint8)
    {
        return _tokenDecimals[_tokenAddress];
    }

    /**
     * @dev Returns USD value of a given token
     * amount in order of 1E18 decimal precision.
     */
    function getTokensInUSD(
        address _tokenAddress,
        uint256 _amount
    )
        external
        view
        returns (uint256)
    {
        uint8 tokenDecimals = _tokenDecimals[
            _tokenAddress
        ];

        return _decimalsUSD < tokenDecimals
            ? _amount
                * latestResolver(_tokenAddress)
                / 10 ** decimals(_tokenAddress)
                / 10 ** (tokenDecimals - _decimalsUSD)
            : _amount
                * 10 ** (_decimalsUSD - tokenDecimals)
                * latestResolver(_tokenAddress)
                / 10 ** decimals(_tokenAddress);
    }

    /**
     * @dev Converts USD value of a token into token amount with a
     * current price. The order of the argument _usdValue is 1E18.
     */
    function getTokensFromUSD(
        address _tokenAddress,
        uint256 _usdValue
    )
        external
        view
        returns (uint256)
    {
        uint8 tokenDecimals = _tokenDecimals[
            _tokenAddress
        ];

        return _decimalsUSD < tokenDecimals
            ? _usdValue
                * 10 ** (tokenDecimals - _decimalsUSD)
                * 10 ** decimals(_tokenAddress)
                / latestResolver(_tokenAddress)
            : _usdValue
                * 10 ** decimals(_tokenAddress)
                / latestResolver(_tokenAddress)
                / 10 ** (_decimalsUSD - tokenDecimals);
    }

    /**
     * @dev Adds priceFeed for a token.
     * Can't overwrite existing mappings.
     * Master is a timelock contract.
     */
    function addOracle(
        address _tokenAddress,
        IPriceFeed _priceFeedAddress,
        address[] memory _underlyingFeedTokens
    )
        external
        onlyMaster
    {
        _addOracle(
            _tokenAddress,
            _priceFeedAddress,
            _underlyingFeedTokens
        );
    }

    /**
     * @dev Adds priceFeeds for tokens.
     * Can't overwrite existing mappings.
     * Master is a timelock contract.
     */
    function addOracleBulk(
        address[] memory _tokenAddresses,
        IPriceFeed[] memory _priceFeedAddresses,
        address[][] memory _underlyingFeedTokens
    )
        external
        onlyMaster
    {
        for (uint8 i = 0; i < _tokenAddresses.length; ++i) {
            _addOracle(
                _tokenAddresses[i],
                _priceFeedAddresses[i],
                _underlyingFeedTokens[i]
            );
        }
    }

    /**
     * @dev Looks at the maximal last 50 rounds and
     * takes second highest value to avoid counting
     * offline time of chainlink as valid heartbeat.
     */
    function recalibratePreview(
        address _tokenAddress
    )
        external
        view
        returns (uint256)
    {
        return _recalibratePreview(
            _tokenAddress
        );
    }

    /**
     * @dev Check if chainLink feed was
     * updated within expected timeFrame.
     * If length of {underlyingFeedTokens}
     * is greater than zero it checks the
     * heartbeat of all base feeds of the
     * derivate oracle.
     */
    function chainLinkIsDead(
        address _tokenAddress
    )
        external
        view
        returns (bool state)
    {
        uint256 length = underlyingFeedTokens[_tokenAddress].length;

        if (length == 0) {
            return _chainLinkIsDead(
                _tokenAddress
            );
        }

        for (uint8 i = 0; i < length; ++i) {

            state = _chainLinkIsDead(
                underlyingFeedTokens[_tokenAddress][i]
            );

            if (state == true) {
                break;
            }
        }

        return state;
    }

    /**
     * @dev Recalibrates expected
     * heartbeat for a pricing feed.
     */
    function recalibrate(
        address _tokenAddress
    )
        external
    {
        _recalibrate(
            _tokenAddress
        );
    }

    /**
     * @dev Bulk function to recalibrate
     * the heartbeat for several tokens.
     */
    function recalibrateBulk(
        address[] memory _tokenAddresses
    )
        external
    {
        for (uint256 i = 0; i < _tokenAddresses.length; ++i) {
            _recalibrate(
                _tokenAddresses[i]
            );
        }
    }
}

// File: contracts/OwnableMaster.sol



// SPDX-License-Identifier: -- WISE --

pragma solidity =0.8.21;

error NotMaster();
error NotProposed();

contract OwnableMaster {

    address public master;
    address public proposedMaster;

    address constant ZERO_ADDRESS = address(0x0);

    modifier onlyProposed() {
        _onlyProposed();
        _;
    }

    function _onlyMaster()
        private
        view
    {
        if (msg.sender == master) {
            return;
        }

        revert NotMaster();
    }

    modifier onlyMaster() {
        _onlyMaster();
        _;
    }

    function _onlyProposed()
        private
        view
    {
        if (msg.sender == proposedMaster) {
            return;
        }

        revert NotProposed();
    }

    constructor(
        address _master
    ) {
        master = _master;
    }

    /**
     * @dev Allows to propose next master.
     * Must be claimed by proposer.
     */
    function proposeOwner(
        address _proposedOwner
    )
        external
        onlyMaster
    {
        proposedMaster = _proposedOwner;
    }

    /**
     * @dev Allows to claim master role.
     * Must be called by proposer.
     */
    function claimOwnership()
        external
        onlyProposed
    {
        master = proposedMaster;
    }

    /**
     * @dev Removes master role.
     * No ability to be in control.
     */
    function renounceOwnership()
        external
        onlyMaster
    {
        master = ZERO_ADDRESS;
        proposedMaster = ZERO_ADDRESS;
    }
}
// File: contracts/WiseOracleHub/OracleHelper.sol



// SPDX-License-Identifier: -- WISE --

pragma solidity =0.8.21;

import "./Declarations.sol";

abstract contract OracleHelper is Declarations {

    /**
     * @dev Adds priceFeed for a given token.
     */
    function _addOracle(
        address _tokenAddress,
        IPriceFeed _priceFeedAddress,
        address[] memory _underlyingFeedTokens
    )
        internal
    {
        if (priceFeed[_tokenAddress] == ZERO_FEED) {
            priceFeed[_tokenAddress] = _priceFeedAddress;

            _tokenDecimals[_tokenAddress] = IERC20(
                _tokenAddress
            ).decimals();

            underlyingFeedTokens[_tokenAddress] = _underlyingFeedTokens;

            return;
        }

        revert OracleAlreadySet(
            {
                feed: priceFeed[_tokenAddress]
            }
        );
    }

    /**
     * @dev Stores expected heartbeat
     * value for a pricing feed token.
     */
    function _recalibrate(
        address _tokenAddress
    )
        internal
    {
        heartBeat[_tokenAddress] = _recalibratePreview(
            _tokenAddress
        );
    }

    /**
     * @dev Check if chainLink feed was
     * updated within expected timeFrame
     * for single {_tokenAddress}.
     */
    function _chainLinkIsDead(
        address _tokenAddress
    )
        internal
        view
        returns (bool)
    {
        uint256 upd = latestRoundData(
            _tokenAddress
        );

        upd = block.timestamp > upd
            ? block.timestamp - upd
            : block.timestamp;

        return upd > heartBeat[_tokenAddress];
    }

    /**
     * @dev Recalibrates expected
     * heartbeat for a pricing feed.
     */
    function _recalibratePreview(
        address _tokenAddress
    )
        internal
        view
        returns (uint256)
    {
        uint80 latestAggregatorRoundId = _getLatestAggregatorRoundId(
            _tokenAddress
        );

        uint80 iterationCount = _getIterationCount(
            latestAggregatorRoundId
        );

        if (iterationCount < 2) {
            revert SampleTooSmall(
                {
                    size: iterationCount
                }
            );
        }

        uint16 phaseId = _getPhaseId(
            _tokenAddress
        );

        uint256 latestTimestamp = _getRoundTimestamp(
            _tokenAddress,
            phaseId,
            latestAggregatorRoundId
        );

        uint256 currentDiff;
        uint256 currentBiggest;
        uint256 currentSecondBiggest;

        for (uint80 i = 1; i < iterationCount; ++i) {

            uint256 currentTimestamp = _getRoundTimestamp(
                _tokenAddress,
                phaseId,
                latestAggregatorRoundId - i
            );

            currentDiff = latestTimestamp
                - currentTimestamp;

            latestTimestamp = currentTimestamp;

            if (currentDiff >= currentBiggest) {

                currentSecondBiggest = currentBiggest;
                currentBiggest = currentDiff;

            } else if (currentDiff > currentSecondBiggest && currentDiff < currentBiggest) {
                currentSecondBiggest = currentDiff;
            }
        }

        return currentSecondBiggest;
    }

    /**
     * @dev Determines number of iterations
     * needed during heartbeat recalibration.
     */
    function _getIterationCount(
        uint80 _latestAggregatorRoundId
    )
        internal
        pure
        returns (uint80 res)
    {
        res = _latestAggregatorRoundId < MAX_ROUND_COUNT
            ? _latestAggregatorRoundId
            : MAX_ROUND_COUNT;
    }

    /**
     * @dev Fetches timestamp of a byteshifted
     * aggregatorRound with specific phaseId.
     */
    function _getRoundTimestamp(
        address _tokenAddress,
        uint16 _phaseId,
        uint80 _aggregatorRoundId
    )
        internal
        view
        returns (uint256)
    {
        (
            ,
            ,
            ,
            uint256 timestamp,
        ) = priceFeed[_tokenAddress].getRoundData(
                getRoundIdByByteShift(
                    _phaseId,
                    _aggregatorRoundId
                )
            );

        return timestamp;
    }

    /**
     * @dev Determines info for the heartbeat update
     * mechanism for chainlink oracles, roundIds.
     */
    function _getLatestAggregatorRoundId(
        address _tokenAddress
    )
        internal
        view
        returns (uint80)
    {
        (   uint80 roundId,
            ,
            ,
            ,
        ) = priceFeed[_tokenAddress].latestRoundData();

        return roundId;
    }

    /**
     * @dev Determines info for the heartbeat update
     * mechanism for chainlink oracles, shifted roundIds.
     */
    function getRoundIdByByteShift(
        uint16 _phaseId,
        uint80 _aggregatorRoundId
    )
        public
        pure
        returns (uint80)
    {
        return uint80(
            uint256(_phaseId) << 64 | _aggregatorRoundId
        );
    }

    /**
     * @dev Routing phaseId from chainLink.
     * Returns phaseId by passing underlying token address.
     */
    function _getPhaseId(
        address _tokenAddress
    )
        internal
        view
        returns (uint16)
    {
        return priceFeed[_tokenAddress].phaseId();
    }

    /**
     * @dev Routing latest round data from chainLink.
     * Returns latestRoundData by passing underlying token address.
     */
    function latestRoundData(
        address _tokenAddress
    )
        public
        view
        returns (uint256 upd)
    {
        (   ,
            ,
            ,
            upd
            ,
        ) = priceFeed[_tokenAddress].latestRoundData();
    }
}
// File: contracts/WiseOracleHub/Declarations.sol



// SPDX-License-Identifier: -- WISE --

pragma solidity =0.8.21;

import "../InterfaceHub/IERC20.sol";
import "../InterfaceHub/IPriceFeed.sol";

error SampleTooSmall(
    uint256 size
);

error OracleAlreadySet(
    IPriceFeed feed
);

contract Declarations {

    // -- Constant values --

    // Target Decimals of the returned USD values.
    uint8 constant _decimalsUSD = 18;

    // Number of last rounds which are checked for heartbeat.
    uint80 constant MAX_ROUND_COUNT = 50;

    // Value address used for empty feed comparison.
    IPriceFeed constant ZERO_FEED = IPriceFeed(
        address(0x0)
    );

    // -- Mapping values --

    // Stores decimals of specific ERC20 token.
    mapping(address => uint8) _tokenDecimals;

    // Stores the price feed address from oracle sources.
    mapping(address => IPriceFeed) public priceFeed;

    // Stores the time between chainLink heartbeats.
    mapping(address => uint256) public heartBeat;

    // Mapping underlying feed token for multi token derivate oracle
    mapping(address => address[]) public underlyingFeedTokens;
}
// File: contracts/InterfaceHub/IPriceFeed.sol



// SPDX-License-Identifier: -- WISE --

pragma solidity =0.8.21;

interface IPriceFeed {

    function decimals()
        external
        view
        returns (uint8);

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

    function version()
        external
        view
        returns (uint256);

    function getRoundData(
        uint80 _roundId
    )
        external
        view
        returns (
            uint80 roundId,
            int256 answer,
            uint256 startedAt,
            uint256 updatedAt,
            uint80 answeredInRound
        );

    function latestRoundData()
        external
        view
        returns (
            uint80 roundId,
            int256 answer,
            uint256 startedAt,
            uint256 updatedAt,
            uint80 answeredInRound
        );

    function latestAnswer()
        external
        view
        returns (uint256);

    function phaseId()
        external
        view
        returns(
            uint16 phaseId
        );

    function aggregator()
        external
        view
        returns (address);
}
// File: contracts/InterfaceHub/IERC20.sol



// SPDX-License-Identifier: -- WISE --

pragma solidity =0.8.21;

interface IERC20 {

    function totalSupply()
        external
        view
        returns (uint256);

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

    function transferFrom(
        address _sender,
        address _recipient,
        uint256 _amount
    )
        external
        returns (bool);

    function transfer(
        address _recipient,
        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 decimals()
        external
        view
        returns (uint8);

    event Transfer(
        address indexed from,
        address indexed to,
        uint256 value
    );

    event Approval(
        address indexed owner,
        address indexed spender,
        uint256 value
    );

    event  Deposit(
        address indexed dst,
        uint wad
    );

    event  Withdrawal(
        address indexed src,
        uint wad
    );
}