// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;


/**
* @title StableSwapGuard
* @author Geminon Protocol
* @notice Calculates safety fees to make oracle front-running exploit unprofitable.
*/
contract StableSwapGuard {

    struct priceRecord {
        uint64 lastTimestamp;
        uint32 weightedPrice;
        uint256 volume;
    }

    mapping(address => priceRecord) public priceRecordsLongs;
    mapping(address => priceRecord) public priceRecordsShorts;
    
    
    /// @dev Updates the price record of the stablecoin for the given trade direction
    function _updatePriceRecord(address stable, uint256 usdPrice, uint256 amount, bool isOpLong) internal {
        if (isOpLong) {
            priceRecord memory record = priceRecordsLongs[stable];
            priceRecordsLongs[stable] = _modifyRecord(record, usdPrice, amount);
        } else {
            priceRecord memory record = priceRecordsShorts[stable];
            priceRecordsShorts[stable] = _modifyRecord(record, usdPrice, amount);
        }
    }

    /// @dev Calculates the minimum fee rate to avoid front-running exploits for a pair of stablecoins (6 decimals)
    function _safetyFeeStablecoins(
        address stableIn, 
        address stableOut, 
        uint256 usdPriceIn, 
        uint256 usdPriceOut
    ) internal view returns(uint256) {
        uint256 fee1 = _safetyFeeStablecoin(stableIn, usdPriceIn, false);
        uint256 fee2 = _safetyFeeStablecoin(stableOut, usdPriceOut, true);
        return fee1 > fee2 ? fee1 : fee2;
    }
    
    /// @dev Calculates the minimum fee rate to avoid front-running exploits for a single stablecoin (6 decimals)
    function _safetyFeeStablecoin(address stable, uint256 usdPrice, bool isOpLong) internal view returns(uint256) {
        return _volatility(stable, usdPrice, isOpLong) / 1e12;
    }

    
    /// @dev Calculates a weighted mean of the price for the last 5 minutes
    function _modifyRecord(
        priceRecord memory record, 
        uint256 usdPrice, 
        uint256 amount
    ) private view returns(priceRecord memory) {
        uint64 timestamp;
        uint32 weightedPrice;
        uint256 volume;

        if (block.timestamp - record.lastTimestamp > 300) {
            weightedPrice = uint32(usdPrice/1e12);
            volume = amount;
        } else {
            uint256 w = (amount * 1e6) / (amount + record.volume);
            weightedPrice = uint32((w*usdPrice/1e12 + (1e6-w)*record.weightedPrice) / 1e6);
            volume = record.volume + amount;
        }
        timestamp = uint64(block.timestamp);

        return priceRecord(timestamp, weightedPrice, volume);
    }


    /// @dev Calculates the price variation of the stablecoin for a given trade direction (18 decimals)
    function _volatility(address stable, uint256 usdPrice, bool isOpLong) private view returns(uint256) {
        uint32 weightedPrice;
        
        if (isOpLong)
            weightedPrice = priceRecordsLongs[stable].weightedPrice;
        else
            weightedPrice = priceRecordsShorts[stable].weightedPrice;
        
        return _absRet(usdPrice, uint256(weightedPrice)*1e12);
    }

    /// @dev Calculates the absolute return of two prices
    function _absRet(uint256 price, uint256 basePrice) private pure returns(uint256) {
        if (basePrice == 0) 
            return 0;
            
        if (price >= basePrice)
            return ((price - basePrice) * 1e18) / basePrice;
        else
            return ((basePrice - price) * 1e18) / basePrice;
    }
}
[{"inputs":[{"internalType":"address","name":"gex","type":"address"},{"internalType":"address","name":"usdi","type":"address"},{"internalType":"address","name":"oracle","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"account","type":"address"}],"name":"MintPaused","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"account","type":"address"}],"name":"MintUnpaused","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":"address","name":"account","type":"address"}],"name":"RedeemPaused","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"address","name":"account","type":"address"}],"name":"RedeemUnpaused","type":"event"},{"inputs":[],"name":"GEX","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"USDI","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"address","name":"stablecoin","type":"address"}],"name":"addReserves","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"addReservesGEX","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"},{"internalType":"uint32","name":"swapFee","type":"uint32"}],"name":"addStablecoin","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountGEX","type":"uint256"},{"internalType":"uint256","name":"baseFee","type":"uint256"}],"name":"amountFeeGEX","outputs":[{"internalType":"uint256","name":"fee","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"stable","type":"address"},{"internalType":"uint256","name":"amountStable","type":"uint256"},{"internalType":"uint256","name":"usdPrice","type":"uint256"}],"name":"amountFeeMint","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"stable","type":"address"},{"internalType":"uint256","name":"amountStable","type":"uint256"},{"internalType":"uint256","name":"usdPrice","type":"uint256"}],"name":"amountFeeRedeem","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"stableIn","type":"address"},{"internalType":"address","name":"stableOut","type":"address"},{"internalType":"uint256","name":"usdPriceIn","type":"uint256"},{"internalType":"uint256","name":"usdPriceOut","type":"uint256"},{"internalType":"uint256","name":"amountOut","type":"uint256"}],"name":"amountFeeSwap","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"stablecoin","type":"address"},{"internalType":"uint256","name":"amountGEX","type":"uint256"}],"name":"amountMint","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"stablecoin","type":"address"},{"internalType":"uint256","name":"amountStablecoin","type":"uint256"}],"name":"amountRedeem","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"stablecoin","type":"address"},{"internalType":"uint256","name":"amount","type":"uint256"}],"name":"amountUSDI","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"applyOracleChange","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"arbitrageur","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"baseMintFee","outputs":[{"internalType":"uint32","name":"","type":"uint32"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"baseRedeemFee","outputs":[{"internalType":"uint32","name":"","type":"uint32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"baseSwapFees","outputs":[{"internalType":"uint32","name":"","type":"uint32"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"address","name":"stablecoin","type":"address"}],"name":"burnReserves","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"cancelChangeRequests","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"changeRequests","outputs":[{"internalType":"bool","name":"changeRequested","type":"bool"},{"internalType":"uint64","name":"timestampRequest","type":"uint64"},{"internalType":"address","name":"newAddressRequested","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"collectFees","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountGEX","type":"uint256"},{"internalType":"uint256","name":"baseFee","type":"uint256"}],"name":"feeGEX","outputs":[{"internalType":"uint256","name":"fee","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"stable","type":"address"},{"internalType":"uint256","name":"amountStable","type":"uint256"}],"name":"feeStablecoinMint","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"stable","type":"address"},{"internalType":"uint256","name":"amountStable","type":"uint256"}],"name":"feeStablecoinRedeem","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"stableIn","type":"address"},{"internalType":"address","name":"stableOut","type":"address"},{"internalType":"uint256","name":"usdPriceIn","type":"uint256"},{"internalType":"uint256","name":"usdPriceOut","type":"uint256"},{"internalType":"uint256","name":"amountOut","type":"uint256"}],"name":"feeSwap","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getBalanceGEX","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"inGEXAmount","type":"uint256"},{"internalType":"address","name":"stablecoin","type":"address"}],"name":"getMintInfo","outputs":[{"internalType":"uint256","name":"gexPriceUSD","type":"uint256"},{"internalType":"uint256","name":"stablecoinPriceUSD","type":"uint256"},{"internalType":"uint256","name":"fee","type":"uint256"},{"internalType":"uint256","name":"feeAmount","type":"uint256"},{"internalType":"uint256","name":"outStablecoinAmount","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"inStablecoinAmount","type":"uint256"},{"internalType":"address","name":"stablecoin","type":"address"}],"name":"getRedeemInfo","outputs":[{"internalType":"uint256","name":"stablecoinPriceUSD","type":"uint256"},{"internalType":"uint256","name":"gexPriceUSD","type":"uint256"},{"internalType":"uint256","name":"fee","type":"uint256"},{"internalType":"uint256","name":"feeAmount","type":"uint256"},{"internalType":"uint256","name":"outGEXAmount","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"stablecoin","type":"address"}],"name":"getSafeMintRatio","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"stablecoin","type":"address"}],"name":"getSafeRedeemRatio","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"inAmount","type":"uint256"},{"internalType":"address","name":"stableIn","type":"address"},{"internalType":"address","name":"stableOut","type":"address"}],"name":"getStableSwapInfo","outputs":[{"internalType":"uint256","name":"inStablecoinPriceUSD","type":"uint256"},{"internalType":"uint256","name":"outStablecoinPriceUSD","type":"uint256"},{"internalType":"uint256","name":"quoteS2S1","type":"uint256"},{"internalType":"uint256","name":"fee","type":"uint256"},{"internalType":"uint256","name":"feeAmount","type":"uint256"},{"internalType":"uint256","name":"outStablecoinAmount","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getTVC","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"isMigrationRequested","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"isMintPaused","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"isRedeemPaused","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"migrateMinter","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"migrationMinter","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"stablecoin","type":"address"},{"internalType":"uint256","name":"amountInGEX","type":"uint256"}],"name":"mintStablecoin","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"mintedTokens","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"oracleAge","outputs":[{"internalType":"uint64","name":"","type":"uint64"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"oracleGeminon","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":"pauseMint","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"priceRecordsLongs","outputs":[{"internalType":"uint64","name":"lastTimestamp","type":"uint64"},{"internalType":"uint32","name":"weightedPrice","type":"uint32"},{"internalType":"uint256","name":"volume","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"priceRecordsShorts","outputs":[{"internalType":"uint64","name":"lastTimestamp","type":"uint64"},{"internalType":"uint32","name":"weightedPrice","type":"uint32"},{"internalType":"uint256","name":"volume","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"amountGEX","type":"uint256"}],"name":"receiveMigration","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"stablecoin","type":"address"},{"internalType":"uint256","name":"amountInStablecoin","type":"uint256"}],"name":"redeemStablecoin","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"token","type":"address"}],"name":"removeStablecoin","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"renounceOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newContract","type":"address"}],"name":"requestAddAddress","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"actualContract","type":"address"},{"internalType":"address","name":"newContract","type":"address"}],"name":"requestAddressChange","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"requestBailoutFromPool","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newMinter","type":"address"}],"name":"requestMigration","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"oldContract","type":"address"}],"name":"requestRemoveAddress","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"arbitrageur_","type":"address"}],"name":"setArbitrageur","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"feesCollector","type":"address"}],"name":"setCollector","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint32","name":"value","type":"uint32"}],"name":"setMintFee","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint32","name":"value","type":"uint32"}],"name":"setRedeemFee","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"stable","type":"address"},{"internalType":"uint32","name":"value","type":"uint32"}],"name":"setSwapFee","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"stableIn","type":"address"},{"internalType":"address","name":"stableOut","type":"address"},{"internalType":"uint256","name":"amountIn","type":"uint256"}],"name":"stableSwap","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"stablecoins","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"timestampMigrationRequest","outputs":[{"internalType":"uint64","name":"","type":"uint64"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"unpauseMint","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"validTokens","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"}]