diff --git a/foundry.toml b/foundry.toml index 0a2a7d3..1a4a28a 100644 --- a/foundry.toml +++ b/foundry.toml @@ -5,7 +5,7 @@ out = "out" libs = ["dependencies"] auto_detect_remappings = false # Permissions -fs_permissions = [{ access = "read-write", path = "gas.csv" }] +fs_permissions = [{ access = "read-write", path = "gas.csv" },{ access = "read-write", path = "uni_prices.csv" }] ######## # Lint # diff --git a/plot_prices.py b/plot_prices.py new file mode 100644 index 0000000..39193b9 --- /dev/null +++ b/plot_prices.py @@ -0,0 +1,109 @@ +import csv +import os +import requests +import matplotlib.pyplot as plt +import matplotlib.dates as mdates +from datetime import datetime +from matplotlib.ticker import MultipleLocator + +def read_csv(path): + data = {} + with open(path, 'r') as f: + reader = csv.reader(f) + for row in reader: + if row[0].isdigit(): + timestamp = int(row[1]) + price = float(row[2]) + data[timestamp] = price + return data + +def fetch_prices(timestamp): + url = "https://min-api.cryptocompare.com/data/v2/histominute" + params = { + 'fsym': 'ETH', + 'tsym': 'USDT', + 'toTs': timestamp, + 'limit': 1, + } + + response = requests.get(url, params=params) + if response.status_code == 200: + data = response.json() + api_data = data['Data']['Data'] + data_len = len(api_data) + last_item = api_data[data_len - 1] + if data['Response'] == 'Success' and data_len > 0: + if timestamp - last_item['time'] <= 60: + return last_item['time'], last_item['close'] + else: + return last_item['time'], 0 +def main(): + csv_data = read_csv('uni_prices.csv') + uni_timestamps = [] + + uni_prices = [] + api_timestamps = [] + api_prices = [] + + for ts, price in csv_data.items(): + (api_ts, api_price)= fetch_prices(ts) + if api_price: + uni_timestamps.append(ts) + uni_prices.append(price) + api_timestamps.append(api_ts) + api_prices.append(api_price) + + print(uni_timestamps); + print(uni_prices); + print(api_timestamps); + print(api_prices); + + uni_dates = [datetime.fromtimestamp(ts) for ts in uni_timestamps] + api_dates = [datetime.fromtimestamp(ts) for ts in api_timestamps] + + plt.figure(figsize=(14, 7)) + + plt.plot(uni_dates, uni_prices, label='UNI Price', marker='o', color='blue', markersize=5) + plt.plot(api_dates, api_prices, label='API Price', marker='x', color='green', markersize=5) + + plt.title('ETH Price Comparison: UNI vs API', fontsize=14) + plt.xlabel('Time', fontsize=12) + plt.ylabel('Price (USD)', fontsize=12) + plt.legend(fontsize=12) + + all_dates = uni_dates + api_dates + x_min = min(all_dates) + x_max = max(all_dates) + plt.xlim(x_min, x_max) + + ax = plt.gca() + + ax.xaxis.set_major_locator(mdates.HourLocator(interval=2)) + ax.xaxis.set_major_formatter(mdates.DateFormatter('%m-%d %H:%M')) + + ax.yaxis.set_major_locator(MultipleLocator(2)) + ax.yaxis.set_minor_locator(MultipleLocator(1)) + + ax.grid(which='major', axis='both', linestyle='-', alpha=0.7) + ax.grid(which='minor', axis='both', linestyle=':', alpha=0.4) + + + differences = [abs(u - a) for u, a in zip(uni_prices, api_prices)] + max_diff = max(differences) + max_diff_index = differences.index(max_diff) + + max_uni_ts = uni_timestamps[max_diff_index] + max_api_ts = api_timestamps[max_diff_index] + max_time_str = datetime.fromtimestamp(max_uni_ts).strftime('%Y-%m-%d %H:%M:%S') + + text_str = f"Max Difference: {max_diff:.4f}\nAt UNI timestamp: {max_time_str}" + plt.text(0.02, 0.02, text_str, + transform=ax.transAxes, + fontsize=10, + bbox=dict(facecolor='white', alpha=0.8)) + + plt.gcf().autofmt_xdate() + plt.show() + +if __name__ == "__main__": + main() diff --git a/src/oracle/GasNetworkOracle.sol b/src/oracle/GasNetworkOracle.sol new file mode 100644 index 0000000..50925de --- /dev/null +++ b/src/oracle/GasNetworkOracle.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +// Analog's Contracts (last updated v0.1.0) (src/Gateway.sol) + +pragma solidity >=0.8.0; + +import {IGasPriceOracle} from "src/oracle/IOracle.sol"; +import {Test, console} from "forge-std/Test.sol"; + +interface IGasNetOracle { + /** + * @param systemid: + * 1 for Bitcoin chains + * 2 for Evm chains + * @param cid: + * chainId of the chain + * @param typ: + * 107: Base fee (EIP-1559) + * 115: Blob base fee (post-EIP-4844 chains) + * 322: 90th percentile priority fee + * @param tin: + * miliseconds, return zero if the data is older than mili seconds + */ + function getInTime(uint8 systemid, uint64 cid, uint16 typ, uint48 tin) + external + view + returns (uint256 value, uint64 height, uint48 timestamp); +} + +contract GasNetworkOracle is IGasPriceOracle { + address public immutable gasNet; + + constructor(address _gasNet) { + gasNet = _gasNet; + } + + function getGasPrice(uint64 chainId, uint16 ty, uint48 tin) external view returns (uint256 value) { + // miliseconds are of two hours need to check on this later. + (uint256 gasPrice,,) = IGasNetOracle(gasNet).getInTime(2, chainId, ty, tin); + return gasPrice; + } +} diff --git a/src/oracle/IOracle.sol b/src/oracle/IOracle.sol new file mode 100644 index 0000000..eeb72b3 --- /dev/null +++ b/src/oracle/IOracle.sol @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: MIT +// Analog's Contracts (last updated v0.1.0) (src/Gateway.sol) + +pragma solidity >=0.8.0; + +interface IPriceOracle { + function getPrice(address token) external view returns (uint256, uint256); + function getAmountIn(address tokenIn, address tokenOut, uint256 amountOut) external view returns (uint256); +} + +interface IGasPriceOracle { + function getGasPrice(uint64 chainId, uint16 ty, uint48 maxAge) external view returns (uint256 value); +} diff --git a/src/oracle/UniswapV2Oracle.sol b/src/oracle/UniswapV2Oracle.sol new file mode 100644 index 0000000..d153670 --- /dev/null +++ b/src/oracle/UniswapV2Oracle.sol @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: MIT +// Analog's Contracts (last updated v0.1.0) (src/Gateway.sol) + +pragma solidity >=0.8.0; + +import {IPriceOracle} from "src/oracle/IOracle.sol"; +import {Test, console} from "forge-std/Test.sol"; + +interface IUniswapV2Factory { + function getPair(address tokenA, address tokenB) external view returns (address pair); +} + +interface IUniswapV2Pair { + function getReserves() external view returns (uint112 reserve0, uint112 reserve1, uint32 blockTimestampLast); + function token0() external view returns (address); + function token1() external view returns (address); +} + +interface IERC20 { + function decimals() external view returns (uint8); +} + +contract UniswapV2Oracle is IPriceOracle { + address public immutable factory; + address public immutable USDT; + + constructor(address _factory, address _usdt) { + factory = _factory; + USDT = _usdt; + } + + function getPrice(address token) external view returns (uint256, uint256) { + return getTokenValueInUSDT(token, 10 ** IERC20(token).decimals()); + } + + function getPairAddress(address tokenA, address tokenB) external view returns (address) { + return IUniswapV2Factory(factory).getPair(tokenA, tokenB); + } + + function getTokenValueInUSDT(address token, uint256 amount) public view returns (uint256, uint256) { + address pairAddress = IUniswapV2Factory(factory).getPair(token, USDT); + require(pairAddress != address(0), "Pair does not exist"); + + IUniswapV2Pair pair = IUniswapV2Pair(pairAddress); + (uint112 reserve0, uint112 reserve1,) = pair.getReserves(); + require(reserve0 > 0 && reserve1 > 0, "Insufficient liquidity"); + + (uint256 reserveToken, uint256 reserveUSDT) = + pair.token0() == USDT ? (reserve1, reserve0) : (reserve0, reserve1); + + uint8 tokenDecimals = IERC20(token).decimals(); + uint8 usdtDecimals = IERC20(USDT).decimals(); // address token1 = pair.token1(); + + // e.g. lets say we wanna get price fo 1eth + // and pool have 10 eth and 30000 usd then + // 1e18 * 30000USD * 10 ** 1e18 / 10eth * 10 ** 1e6 + uint256 price = (amount * reserveUSDT * (10 ** tokenDecimals)) / (reserveToken * (10 ** usdtDecimals)); + + uint256 scale = 10 ** tokenDecimals; + uint256 integer_part = price / scale; + uint256 fraction = price % scale; + return (integer_part, fraction); + } + + function getAmountIn(address tokenIn, address tokenOut, uint256 amountOut) external view returns (uint256) { + address pairAddress = IUniswapV2Factory(factory).getPair(tokenIn, tokenOut); + require(pairAddress != address(0), "Pair does not exist"); + + IUniswapV2Pair pair = IUniswapV2Pair(pairAddress); + + (uint112 reserve0, uint112 reserve1,) = pair.getReserves(); + require(reserve0 > 0 && reserve1 > 0, "Insufficient liquidity"); + + bool isToken0In = pair.token0() == tokenIn; + (uint256 reserveIn, uint256 reserveOut) = + isToken0In ? (uint256(reserve0), uint256(reserve1)) : (uint256(reserve1), uint256(reserve0)); + + uint256 numerator = reserveIn * amountOut * 1000; + // uniswap v2 fee is 0.3% + uint256 denominator = (reserveOut - amountOut) * 997; + uint256 amountIn = (numerator / denominator) + 1; + + return amountIn; + } +} diff --git a/test/GasNetworkOracle.t.sol b/test/GasNetworkOracle.t.sol new file mode 100644 index 0000000..0b9b62c --- /dev/null +++ b/test/GasNetworkOracle.t.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: MIT +// Analog's Contracts (last updated v0.1.0) (src/Gateway.sol) + +pragma solidity >=0.8.0; + +import {Test, console} from "forge-std/Test.sol"; +import {IGasPriceOracle} from "src/oracle/IOracle.sol"; +import {GasNetworkOracle} from "src/oracle/GasNetworkOracle.sol"; + +contract GasNetworkOracleTest is Test { + GasNetworkOracle oracle; + address constant ArbitrumMainnet = 0x1c51B22954af03FE11183aaDF43F6415907a9287; + + function setUp() public { + vm.createSelectFork({urlOrAlias: "https://arb1.arbitrum.io/rpc"}); + oracle = new GasNetworkOracle(ArbitrumMainnet); + } + + function testGasPrice() public view { + (uint256 value) = IGasPriceOracle(oracle).getGasPrice(1, 322, 7200000); + assert(value > 0); + } +} diff --git a/test/UniswapV2Oracle.t.sol b/test/UniswapV2Oracle.t.sol new file mode 100644 index 0000000..df2ea2c --- /dev/null +++ b/test/UniswapV2Oracle.t.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: MIT +// Analog's Contracts (last updated v0.1.0) (src/Gateway.sol) + +pragma solidity >=0.8.0; + +import {Test, console} from "forge-std/Test.sol"; +import {UniswapV2Oracle} from "src/oracle/UniswapV2Oracle.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; + +interface IERC20 { + function decimals() external view returns (uint8); +} + +contract UniswapV2OracleTest is Test { + UniswapV2Oracle oracle; + + address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + address constant USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7; + address constant FACTORY = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f; + + function setUp() public { + vm.createSelectFork({urlOrAlias: "https://eth.meowrpc.com"}); + oracle = new UniswapV2Oracle(FACTORY, USDT); + vm.makePersistent(address(oracle)); + } + + function testGetNativePrice() public view { + (uint256 usdPrice,) = oracle.getPrice(WETH); + console.log("ETH USDT price", usdPrice); + assert(usdPrice > 0); + } + + function testGetAmountIn() public view { + (uint256 usdPrice,) = oracle.getPrice(WETH); + (uint256 usdtRequired) = oracle.getAmountIn(USDT, WETH, 1 ether); + uint256 usdtDecimals = IERC20(USDT).decimals(); + uint256 usdtScale = 10 ** usdtDecimals; + uint256 usdtRequiredForOneEth = usdtRequired / usdtScale; + console.log("ETH pool price", usdtRequiredForOneEth); + require(usdtRequiredForOneEth - usdPrice < 50); + } + + function testGeneratePricesRange() public { + // skipping the test due to nature of constant rpc queries + vm.skip(true); + uint256 BLOCKS_TO_ITERATE = 50; + uint256 BLOCK_STEP = 299; + string memory path = "uni_prices.csv"; + string memory rpc_url = "https://eth-mainnet.public.blastapi.io"; + + uint256 startBlock = block.number; + string memory csv = string.concat("block_number,timestamp,price\n"); + + for (uint256 i = 0; i < BLOCKS_TO_ITERATE; i++) { + uint256 targetBlock = startBlock - (i * BLOCK_STEP); + vm.createSelectFork(rpc_url, targetBlock); + uint256 timestamp = block.timestamp; + (uint256 usdPrice, uint256 fraction) = oracle.getPrice(WETH); + csv = string.concat( + csv, + Strings.toString(targetBlock), + ",", + Strings.toString(timestamp), + ",", + Strings.toString(usdPrice), + ".", + Strings.toString(fraction), + "\n" + ); + } + + vm.writeFile(path, csv); + } +} diff --git a/uni_prices.csv b/uni_prices.csv new file mode 100644 index 0000000..e1d5d59 --- /dev/null +++ b/uni_prices.csv @@ -0,0 +1,51 @@ +block_number,timestamp,price +22636693,1749105683,2609.808076265532095180 +22636394,1749102011,2621.480490403203857798 +22636095,1749098375,2624.393160328883899511 +22635796,1749094775,2622.768627953426326819 +22635497,1749091175,2619.723158854853223527 +22635198,1749087551,2610.396327867169402364 +22634899,1749083951,2609.545936613065795023 +22634600,1749080363,2608.729875162141764401 +22634301,1749076763,2614.102110969931344932 +22634002,1749073079,2612.849426415078947522 +22633703,1749069491,2609.528150105592056902 +22633404,1749065879,2629.978419276373883105 +22633105,1749062255,2639.69358530623668582 +22632806,1749058619,2641.119928609394508552 +22632507,1749055019,2651.151555396737483315 +22632208,1749051419,2653.429993570316036952 +22631909,1749047807,2610.91201380128385796 +22631610,1749044183,2616.554217935565935726 +22631311,1749040559,2623.770509856817489539 +22631012,1749036923,2634.658956898743669552 +22630713,1749033275,2640.61445210207564004 +22630414,1749029675,2634.892883303356012749 +22630115,1749026075,2637.221421467606547742 +22629816,1749022427,2626.72749692925304526 +22629517,1749018779,2620.774610188659997120 +22629218,1749015179,2628.379186142653812449 +22628919,1749011567,2631.978819819874542670 +22628620,1749007979,2635.816798914804067936 +22628321,1749004343,2617.504421403550660296 +22628022,1749000731,2610.379877831433309262 +22627723,1748997119,2595.222703408337425286 +22627424,1748993519,2600.55142335050829777 +22627125,1748989919,2595.497388260198852413 +22626826,1748986259,2608.125684065391981683 +22626527,1748982671,2605.341895595557931087 +22626228,1748979047,2620.625110387101725716 +22625929,1748975447,2614.446800894276899947 +22625630,1748971847,2620.215773443703316764 +22625331,1748968247,2616.703457689552053223 +22625032,1748964647,2643.306094395635139333 +22624733,1748961035,2628.449808245661732807 +22624434,1748957387,2620.172618189738950580 +22624135,1748953775,2603.612670229941585613 +22623836,1748950187,2613.240784817929330125 +22623537,1748946587,2612.434634630224209653 +22623238,1748942951,2601.755933326052318361 +22622939,1748939291,2601.613596128406849868 +22622640,1748935667,2613.247738844563448784 +22622341,1748932055,2612.322965102058051715 +22622042,1748928443,2599.466433363166989073