From ad74ec2a8d228d763d0e9ef025ee8887f4d7a535 Mon Sep 17 00:00:00 2001 From: Haider-Ali-DS Date: Mon, 2 Jun 2025 20:05:56 +0500 Subject: [PATCH 1/9] adding more logs to debug 0 price --- src/oracle/IOracle.sol | 8 ++++ src/oracle/UniswapV2Oracle.sol | 78 ++++++++++++++++++++++++++++++++++ test/UniswapV2Oracle.t.sol | 27 ++++++++++++ 3 files changed, 113 insertions(+) create mode 100644 src/oracle/IOracle.sol create mode 100644 src/oracle/UniswapV2Oracle.sol create mode 100644 test/UniswapV2Oracle.t.sol diff --git a/src/oracle/IOracle.sol b/src/oracle/IOracle.sol new file mode 100644 index 0000000..34f13a6 --- /dev/null +++ b/src/oracle/IOracle.sol @@ -0,0 +1,8 @@ +// SPDX-License-Identifier: MIT +// Analog's Contracts (last updated v0.1.0) (src/Gateway.sol) + +pragma solidity >=0.8.0; + +interface IOracle { + function getNativePrice() external view returns (uint256); +} diff --git a/src/oracle/UniswapV2Oracle.sol b/src/oracle/UniswapV2Oracle.sol new file mode 100644 index 0000000..d6a06b5 --- /dev/null +++ b/src/oracle/UniswapV2Oracle.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: MIT +// Analog's Contracts (last updated v0.1.0) (src/Gateway.sol) + +pragma solidity >=0.8.0; + +import "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 IOracle { + address public immutable factory; + address public immutable WETH; + address public immutable USDT; + + constructor(address _factory, address _weth, address _usdt) { + factory = _factory; + WETH = _weth; + USDT = _usdt; + } + + function getNativePrice() external view override returns (uint256) { + return getTokenPrice(WETH, USDT, 1 ether); + } + + function getTokenPrice(address tokenA, address tokenB, uint256 amount) public view returns (uint256) { + address pairAddress = IUniswapV2Factory(factory).getPair(tokenA, tokenB); + console.log("pair address: ", pairAddress); + 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"); + console.log("reserve0 is, ", reserve0); + console.log("reserve1 is, ", reserve1); + + address token0 = pair.token0(); + // address token1 = pair.token1(); + + uint256 reserveA; + uint256 reserveB; + + if (tokenA == token0) { + reserveA = uint256(reserve0); + reserveB = uint256(reserve1); + } else { + reserveA = uint256(reserve1); + reserveB = uint256(reserve0); + } + + uint8 decimalsA = IERC20(tokenA).decimals(); + console.log("decimal a", decimalsA); + uint8 decimalsB = IERC20(tokenB).decimals(); + console.log("decimal b", decimalsB); + + uint256 price = (amount * reserveB * (10 ** decimalsB)) / (reserveA * (10 ** decimalsA)); + console.log("price calculated is", price); + + return price; + } + + function getPairAddress(address tokenA, address tokenB) external view returns (address) { + return IUniswapV2Factory(factory).getPair(tokenA, tokenB); + } +} diff --git a/test/UniswapV2Oracle.t.sol b/test/UniswapV2Oracle.t.sol new file mode 100644 index 0000000..f010c41 --- /dev/null +++ b/test/UniswapV2Oracle.t.sol @@ -0,0 +1,27 @@ +// 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"; + +contract UniswapV2OracleTest is Test { + UniswapV2Oracle oracle; + // IUniswapV2Factory factory; + // IUniswapV2Pair pair; + + 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, WETH, USDT); + } + + function testGetNativePrice() public view { + uint256 price = oracle.getNativePrice(); + console.log("WETH/USDT Price:", price); + } +} From 7d1c37220e870f255faaf229bc88b38416c72f83 Mon Sep 17 00:00:00 2001 From: Haider-Ali-DS Date: Tue, 3 Jun 2025 10:22:41 +0500 Subject: [PATCH 2/9] update --- src/oracle/UniswapV2Oracle.sol | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/oracle/UniswapV2Oracle.sol b/src/oracle/UniswapV2Oracle.sol index d6a06b5..199b076 100644 --- a/src/oracle/UniswapV2Oracle.sol +++ b/src/oracle/UniswapV2Oracle.sol @@ -66,8 +66,8 @@ contract UniswapV2Oracle is IOracle { uint8 decimalsB = IERC20(tokenB).decimals(); console.log("decimal b", decimalsB); - uint256 price = (amount * reserveB * (10 ** decimalsB)) / (reserveA * (10 ** decimalsA)); - console.log("price calculated is", price); + // fix price computation + uint156 price = 0; return price; } From 8f4f0f6387a4b7868262517f532d2ceda59ecb3f Mon Sep 17 00:00:00 2001 From: Haider-Ali-DS Date: Tue, 3 Jun 2025 15:52:07 +0500 Subject: [PATCH 3/9] fix price --- src/oracle/IOracle.sol | 2 +- src/oracle/UniswapV2Oracle.sol | 15 ++++++++++----- test/UniswapV2Oracle.t.sol | 4 ++-- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/oracle/IOracle.sol b/src/oracle/IOracle.sol index 34f13a6..0670825 100644 --- a/src/oracle/IOracle.sol +++ b/src/oracle/IOracle.sol @@ -4,5 +4,5 @@ pragma solidity >=0.8.0; interface IOracle { - function getNativePrice() external view returns (uint256); + function getNativePrice() external view returns (uint256, uint256); } diff --git a/src/oracle/UniswapV2Oracle.sol b/src/oracle/UniswapV2Oracle.sol index 199b076..5d45c12 100644 --- a/src/oracle/UniswapV2Oracle.sol +++ b/src/oracle/UniswapV2Oracle.sol @@ -31,11 +31,11 @@ contract UniswapV2Oracle is IOracle { USDT = _usdt; } - function getNativePrice() external view override returns (uint256) { + function getNativePrice() external view override returns (uint256, uint256) { return getTokenPrice(WETH, USDT, 1 ether); } - function getTokenPrice(address tokenA, address tokenB, uint256 amount) public view returns (uint256) { + function getTokenPrice(address tokenA, address tokenB, uint256 amount) public view returns (uint256, uint256) { address pairAddress = IUniswapV2Factory(factory).getPair(tokenA, tokenB); console.log("pair address: ", pairAddress); require(pairAddress != address(0), "Pair does not exist"); @@ -66,10 +66,15 @@ contract UniswapV2Oracle is IOracle { uint8 decimalsB = IERC20(tokenB).decimals(); console.log("decimal b", decimalsB); - // fix price computation - uint156 price = 0; + // 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 * reserveB * (10 ** decimalsA)) / (reserveA * (10 ** decimalsB)); - return price; + uint256 scale = 10 ** decimalsA; + uint256 integer_part = price / scale; + uint256 fraction = price % scale; + return (integer_part, fraction); } function getPairAddress(address tokenA, address tokenB) external view returns (address) { diff --git a/test/UniswapV2Oracle.t.sol b/test/UniswapV2Oracle.t.sol index f010c41..19057b1 100644 --- a/test/UniswapV2Oracle.t.sol +++ b/test/UniswapV2Oracle.t.sol @@ -21,7 +21,7 @@ contract UniswapV2OracleTest is Test { } function testGetNativePrice() public view { - uint256 price = oracle.getNativePrice(); - console.log("WETH/USDT Price:", price); + (uint256 ints, uint256 fraction) = oracle.getNativePrice(); + console.log("WETH/USDT Price:", ints, fraction); } } From 0b5217a081beae586ed9e65c6e7553f6545c4d02 Mon Sep 17 00:00:00 2001 From: Haider-Ali-DS Date: Tue, 3 Jun 2025 17:26:27 +0500 Subject: [PATCH 4/9] improve interface --- src/oracle/IOracle.sol | 2 +- src/oracle/UniswapV2Oracle.sol | 46 +++++++++++----------------------- test/UniswapV2Oracle.t.sol | 4 +-- 3 files changed, 18 insertions(+), 34 deletions(-) diff --git a/src/oracle/IOracle.sol b/src/oracle/IOracle.sol index 0670825..2f5e5c4 100644 --- a/src/oracle/IOracle.sol +++ b/src/oracle/IOracle.sol @@ -4,5 +4,5 @@ pragma solidity >=0.8.0; interface IOracle { - function getNativePrice() external view returns (uint256, uint256); + function getPrice(address token) external view returns (uint256, uint256); } diff --git a/src/oracle/UniswapV2Oracle.sol b/src/oracle/UniswapV2Oracle.sol index 5d45c12..8b6640a 100644 --- a/src/oracle/UniswapV2Oracle.sol +++ b/src/oracle/UniswapV2Oracle.sol @@ -22,56 +22,40 @@ interface IERC20 { contract UniswapV2Oracle is IOracle { address public immutable factory; - address public immutable WETH; address public immutable USDT; - constructor(address _factory, address _weth, address _usdt) { + constructor(address _factory, address _usdt) { factory = _factory; - WETH = _weth; USDT = _usdt; } - function getNativePrice() external view override returns (uint256, uint256) { - return getTokenPrice(WETH, USDT, 1 ether); + function getPrice(address token) external view returns (uint256, uint256) { + return getTokenValueInUSDT(token, 10 ** IERC20(token).decimals()); } - function getTokenPrice(address tokenA, address tokenB, uint256 amount) public view returns (uint256, uint256) { - address pairAddress = IUniswapV2Factory(factory).getPair(tokenA, tokenB); - console.log("pair address: ", pairAddress); + 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"); - console.log("reserve0 is, ", reserve0); - console.log("reserve1 is, ", reserve1); - - address token0 = pair.token0(); - // address token1 = pair.token1(); - - uint256 reserveA; - uint256 reserveB; - if (tokenA == token0) { - reserveA = uint256(reserve0); - reserveB = uint256(reserve1); - } else { - reserveA = uint256(reserve1); - reserveB = uint256(reserve0); - } + (uint256 reserveToken, uint256 reserveUSDT) = + pair.token0() == USDT ? (reserve1, reserve0) : (reserve0, reserve1); - uint8 decimalsA = IERC20(tokenA).decimals(); - console.log("decimal a", decimalsA); - uint8 decimalsB = IERC20(tokenB).decimals(); - console.log("decimal b", decimalsB); + 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 * reserveB * (10 ** decimalsA)) / (reserveA * (10 ** decimalsB)); + // 1e18 * 30000USD * 10 ** 1e18 / 10eth * 10 ** 1e6 + uint256 price = (amount * reserveUSDT * (10 ** tokenDecimals)) / (reserveToken * (10 ** usdtDecimals)); + console.log("price: {}", price); + console.log("reserve0: {}", reserve0); + console.log("reserve1: {}", reserve1); - uint256 scale = 10 ** decimalsA; + uint256 scale = 10 ** tokenDecimals; uint256 integer_part = price / scale; uint256 fraction = price % scale; return (integer_part, fraction); diff --git a/test/UniswapV2Oracle.t.sol b/test/UniswapV2Oracle.t.sol index 19057b1..763c557 100644 --- a/test/UniswapV2Oracle.t.sol +++ b/test/UniswapV2Oracle.t.sol @@ -17,11 +17,11 @@ contract UniswapV2OracleTest is Test { function setUp() public { vm.createSelectFork({urlOrAlias: "https://eth.meowrpc.com"}); - oracle = new UniswapV2Oracle(FACTORY, WETH, USDT); + oracle = new UniswapV2Oracle(FACTORY, USDT); } function testGetNativePrice() public view { - (uint256 ints, uint256 fraction) = oracle.getNativePrice(); + (uint256 ints, uint256 fraction) = oracle.getPrice(WETH); console.log("WETH/USDT Price:", ints, fraction); } } From 1538234101bddb1d028881436f2beb3cc8e37926 Mon Sep 17 00:00:00 2001 From: Haider-Ali-DS Date: Tue, 3 Jun 2025 19:03:07 +0500 Subject: [PATCH 5/9] add another function to estimate one token for another --- src/oracle/IOracle.sol | 1 + src/oracle/UniswapV2Oracle.sol | 28 +++++++++++++++++++++++----- test/UniswapV2Oracle.t.sol | 16 ++++++++++++---- 3 files changed, 36 insertions(+), 9 deletions(-) diff --git a/src/oracle/IOracle.sol b/src/oracle/IOracle.sol index 2f5e5c4..5cf742b 100644 --- a/src/oracle/IOracle.sol +++ b/src/oracle/IOracle.sol @@ -5,4 +5,5 @@ pragma solidity >=0.8.0; interface IOracle { function getPrice(address token) external view returns (uint256, uint256); + function getAmountIn(address tokenIn, address tokenOut, uint256 amountOut) external view returns (uint256); } diff --git a/src/oracle/UniswapV2Oracle.sol b/src/oracle/UniswapV2Oracle.sol index 8b6640a..0a8b86c 100644 --- a/src/oracle/UniswapV2Oracle.sol +++ b/src/oracle/UniswapV2Oracle.sol @@ -33,6 +33,10 @@ contract UniswapV2Oracle is IOracle { 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"); @@ -51,9 +55,6 @@ contract UniswapV2Oracle is IOracle { // 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)); - console.log("price: {}", price); - console.log("reserve0: {}", reserve0); - console.log("reserve1: {}", reserve1); uint256 scale = 10 ** tokenDecimals; uint256 integer_part = price / scale; @@ -61,7 +62,24 @@ contract UniswapV2Oracle is IOracle { return (integer_part, fraction); } - function getPairAddress(address tokenA, address tokenB) external view returns (address) { - return IUniswapV2Factory(factory).getPair(tokenA, tokenB); + 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/UniswapV2Oracle.t.sol b/test/UniswapV2Oracle.t.sol index 763c557..52ed21c 100644 --- a/test/UniswapV2Oracle.t.sol +++ b/test/UniswapV2Oracle.t.sol @@ -6,10 +6,12 @@ pragma solidity >=0.8.0; import {Test, console} from "forge-std/Test.sol"; import {UniswapV2Oracle} from "src/oracle/UniswapV2Oracle.sol"; +interface IERC20 { + function decimals() external view returns (uint8); +} + contract UniswapV2OracleTest is Test { UniswapV2Oracle oracle; - // IUniswapV2Factory factory; - // IUniswapV2Pair pair; address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; address constant USDT = 0xdAC17F958D2ee523a2206206994597C13D831ec7; @@ -21,7 +23,13 @@ contract UniswapV2OracleTest is Test { } function testGetNativePrice() public view { - (uint256 ints, uint256 fraction) = oracle.getPrice(WETH); - console.log("WETH/USDT Price:", ints, fraction); + (uint256 ints,) = oracle.getPrice(WETH); + // same query as above one + (uint256 usdtRequired) = oracle.getAmountIn(USDT, WETH, 1 ether); + uint256 usdtDecimals = IERC20(USDT).decimals(); + uint256 usdtScale = 10 ** usdtDecimals; + uint256 usdtRequiredForOneEth = usdtRequired / usdtScale; + // adding 20 threshold due to algo of uniswap v2 + require(usdtRequiredForOneEth - ints < 20); } } From f4d1dca9c9f9e3438c5151ccc88e4fd6d62ec93d Mon Sep 17 00:00:00 2001 From: Haider-Ali-DS Date: Thu, 5 Jun 2025 17:18:54 +0500 Subject: [PATCH 6/9] add gas network interface and ploting prices code --- foundry.toml | 2 +- plot_prices.py | 95 +++++++++++++++++++++++++++++++++ src/oracle/GasNetworkOracle.sol | 41 ++++++++++++++ src/oracle/IOracle.sol | 6 ++- src/oracle/UniswapV2Oracle.sol | 4 +- test/GasNetworkOracle.t.sol | 23 ++++++++ test/UniswapV2Oracle.t.sol | 45 ++++++++++++++-- uni_prices.csv | 51 ++++++++++++++++++ 8 files changed, 259 insertions(+), 8 deletions(-) create mode 100644 plot_prices.py create mode 100644 src/oracle/GasNetworkOracle.sol create mode 100644 test/GasNetworkOracle.t.sol create mode 100644 uni_prices.csv 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..58736b3 --- /dev/null +++ b/plot_prices.py @@ -0,0 +1,95 @@ +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, api_key): + 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() + data_len = len(data['Data']['Data']) + last_item = data['Data']['Data'][data_len - 1] + if data['Response'] == 'Success' and data_len > 0: + if timestamp - last_item['time'] <= 60: + # allowing one min diff + return data['Data']['Data'][0]['close'] + else: + return 0 +def main(): + csv_data = read_csv('uni_prices.csv') + + timestamps = [] + csv_prices = [] + api_prices = [] + + for ts, price in csv_data.items(): + api_price = fetch_prices(ts, api_key) + if api_price: + timestamps.append(ts) + csv_prices.append(price) + api_prices.append(api_price) + + print(timestamps); + print(csv_prices); + print(api_prices); + + # timestamps = [1749105683, 1749102011, 1749098375, 1749094775, 1749091175, 1749087551, 1749083951, 1749080363, 1749076763, 1749073079, 1749069491, 1749065879, 1749062255, 1749058619, 1749055019, 1749051419, 1749047807, 1749044183, 1749040559, 1749036923, 1749033275, 1749029675, 1749026075, 1749022427, 1749018779, 1749015179, 1749011567, 1749007979, 1749004343, 1749000731, 1748997119, 1748993519, 1748989919, 1748986259, 1748982671, 1748979047, 1748975447, 1748971847, 1748968247, 1748964647, 1748961035, 1748957387, 1748953775, 1748950187, 1748946587, 1748942951, 1748939291, 1748935667, 1748932055, 1748928443] + # csv_prices = [2609.8080762655322, 2621.480490403204, 2624.393160328884, 2622.7686279534264, 2619.7231588548534, 2610.3963278671695, 2609.545936613066, 2608.729875162142, 2614.1021109699313, 2612.849426415079, 2609.528150105592, 2629.978419276374, 2639.693585306237, 2641.1199286093947, 2651.1515553967374, 2653.429993570316, 2610.912013801284, 2616.554217935566, 2623.7705098568176, 2634.658956898744, 2640.6144521020756, 2634.892883303356, 2637.2214214676064, 2626.727496929253, 2620.77461018866, 2628.379186142654, 2631.9788198198744, 2635.816798914804, 2617.5044214035506, 2610.3798778314335, 2595.2227034083376, 2600.5514233505082, 2595.497388260199, 2608.125684065392, 2605.341895595558, 2620.625110387102, 2614.4468008942767, 2620.2157734437033, 2616.703457689552, 2643.3060943956352, 2628.4498082456616, 2620.172618189739, 2603.6126702299416, 2613.240784817929, 2612.434634630224, 2601.7559333260524, 2601.6135961284067, 2613.2477388445636, 2612.322965102058, 2599.466433363167] + # api_prices = [2611.79, 2615.1, 2630.77, 2629.6, 2627.53, 2606.69, 2612.21, 2609.82, 2612.91, 2618.81, 2610.49, 2626.76, 2636.21, 2638.42, 2655.51, 2648.87, 2614.97, 2621.86, 2622.5, 2627.96, 2636.65, 2632.8, 2639.91, 2627.62, 2624.14, 2621.03, 2625.66, 2634.53, 2614.45, 2612.09, 2589.82, 2595.93, 2600.22, 2611.75, 2598.33, 2626.38, 2614.02, 2623.73, 2610.51, 2642.56, 2632.72, 2621.35, 2608.12, 2607.72, 2616.27, 2601.82, 2604.33, 2609.74, 2604.84, 2600.63] + dates = [datetime.fromtimestamp(ts) for ts in timestamps] + + plt.figure(figsize=(14, 7)) + + # Plot with different colors and markers + plt.plot(dates, csv_prices, label='UNI Price', marker='o', color='blue', markersize=5) + plt.plot(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) + + # Add more grid lines + plt.grid(True, which='both', ls='-', alpha=0.5) + + ax = plt.gca() + + # X-axis: grid/ticks every 30 minutes + ax.xaxis.set_major_locator(mdates.HourLocator(interval=2)) + ax.xaxis.set_major_formatter(mdates.DateFormatter('%m-%d %H:%M')) + + # Y-axis: grid/ticks every $1 + ax.yaxis.set_major_locator(MultipleLocator(2)) + ax.yaxis.set_minor_locator(MultipleLocator(2)) + + # Grid lines + ax.grid(which='major', axis='both', linestyle='-', alpha=0.7) + ax.grid(which='minor', axis='both', linestyle=':', alpha=0.4) + + 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 index 5cf742b..eeb72b3 100644 --- a/src/oracle/IOracle.sol +++ b/src/oracle/IOracle.sol @@ -3,7 +3,11 @@ pragma solidity >=0.8.0; -interface IOracle { +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 index 0a8b86c..d153670 100644 --- a/src/oracle/UniswapV2Oracle.sol +++ b/src/oracle/UniswapV2Oracle.sol @@ -3,7 +3,7 @@ pragma solidity >=0.8.0; -import "src/oracle/IOracle.sol"; +import {IPriceOracle} from "src/oracle/IOracle.sol"; import {Test, console} from "forge-std/Test.sol"; interface IUniswapV2Factory { @@ -20,7 +20,7 @@ interface IERC20 { function decimals() external view returns (uint8); } -contract UniswapV2Oracle is IOracle { +contract UniswapV2Oracle is IPriceOracle { address public immutable factory; address public immutable USDT; 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 index 52ed21c..8e35c26 100644 --- a/test/UniswapV2Oracle.t.sol +++ b/test/UniswapV2Oracle.t.sol @@ -5,6 +5,7 @@ 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); @@ -20,16 +21,52 @@ contract UniswapV2OracleTest is Test { function setUp() public { vm.createSelectFork({urlOrAlias: "https://eth.meowrpc.com"}); oracle = new UniswapV2Oracle(FACTORY, USDT); + vm.makePersistent(address(oracle)); } function testGetNativePrice() public view { - (uint256 ints,) = oracle.getPrice(WETH); - // same query as above one + (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; - // adding 20 threshold due to algo of uniswap v2 - require(usdtRequiredForOneEth - ints < 20); + console.log("ETH pool price", usdtRequiredForOneEth); + require(usdtRequiredForOneEth - usdPrice < 50); + } + + function testGeneratePricesRange() public { + 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 From 697414e258bb38f06c253e65f857c445202b7001 Mon Sep 17 00:00:00 2001 From: Haider-Ali-DS Date: Thu, 5 Jun 2025 17:58:46 +0500 Subject: [PATCH 7/9] improve plot --- plot_prices.py | 78 +++++++++++++++++++++++++++++++------------------- 1 file changed, 48 insertions(+), 30 deletions(-) diff --git a/plot_prices.py b/plot_prices.py index 58736b3..a2266c9 100644 --- a/plot_prices.py +++ b/plot_prices.py @@ -17,7 +17,7 @@ def read_csv(path): data[timestamp] = price return data -def fetch_prices(timestamp, api_key): +def fetch_prices(timestamp): url = "https://min-api.cryptocompare.com/data/v2/histominute" params = { 'fsym': 'ETH', @@ -29,67 +29,85 @@ def fetch_prices(timestamp, api_key): response = requests.get(url, params=params) if response.status_code == 200: data = response.json() - data_len = len(data['Data']['Data']) - last_item = data['Data']['Data'][data_len - 1] + 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: - # allowing one min diff - return data['Data']['Data'][0]['close'] + return last_item['time'], last_item['close'] else: - return 0 + return last_item['time'], 0 def main(): csv_data = read_csv('uni_prices.csv') - timestamps = [] - csv_prices = [] + uni_timestamps = [] + uni_prices = [] + api_timestamps = [] api_prices = [] for ts, price in csv_data.items(): - api_price = fetch_prices(ts, api_key) + (api_ts, api_price)= fetch_prices(ts) if api_price: - timestamps.append(ts) - csv_prices.append(price) + uni_timestamps.append(ts) + uni_prices.append(price) + api_timestamps.append(api_ts) api_prices.append(api_price) - print(timestamps); - print(csv_prices); + print(uni_timestamps); + print(uni_prices); + print(api_timestamps); print(api_prices); - # timestamps = [1749105683, 1749102011, 1749098375, 1749094775, 1749091175, 1749087551, 1749083951, 1749080363, 1749076763, 1749073079, 1749069491, 1749065879, 1749062255, 1749058619, 1749055019, 1749051419, 1749047807, 1749044183, 1749040559, 1749036923, 1749033275, 1749029675, 1749026075, 1749022427, 1749018779, 1749015179, 1749011567, 1749007979, 1749004343, 1749000731, 1748997119, 1748993519, 1748989919, 1748986259, 1748982671, 1748979047, 1748975447, 1748971847, 1748968247, 1748964647, 1748961035, 1748957387, 1748953775, 1748950187, 1748946587, 1748942951, 1748939291, 1748935667, 1748932055, 1748928443] - # csv_prices = [2609.8080762655322, 2621.480490403204, 2624.393160328884, 2622.7686279534264, 2619.7231588548534, 2610.3963278671695, 2609.545936613066, 2608.729875162142, 2614.1021109699313, 2612.849426415079, 2609.528150105592, 2629.978419276374, 2639.693585306237, 2641.1199286093947, 2651.1515553967374, 2653.429993570316, 2610.912013801284, 2616.554217935566, 2623.7705098568176, 2634.658956898744, 2640.6144521020756, 2634.892883303356, 2637.2214214676064, 2626.727496929253, 2620.77461018866, 2628.379186142654, 2631.9788198198744, 2635.816798914804, 2617.5044214035506, 2610.3798778314335, 2595.2227034083376, 2600.5514233505082, 2595.497388260199, 2608.125684065392, 2605.341895595558, 2620.625110387102, 2614.4468008942767, 2620.2157734437033, 2616.703457689552, 2643.3060943956352, 2628.4498082456616, 2620.172618189739, 2603.6126702299416, 2613.240784817929, 2612.434634630224, 2601.7559333260524, 2601.6135961284067, 2613.2477388445636, 2612.322965102058, 2599.466433363167] - # api_prices = [2611.79, 2615.1, 2630.77, 2629.6, 2627.53, 2606.69, 2612.21, 2609.82, 2612.91, 2618.81, 2610.49, 2626.76, 2636.21, 2638.42, 2655.51, 2648.87, 2614.97, 2621.86, 2622.5, 2627.96, 2636.65, 2632.8, 2639.91, 2627.62, 2624.14, 2621.03, 2625.66, 2634.53, 2614.45, 2612.09, 2589.82, 2595.93, 2600.22, 2611.75, 2598.33, 2626.38, 2614.02, 2623.73, 2610.51, 2642.56, 2632.72, 2621.35, 2608.12, 2607.72, 2616.27, 2601.82, 2604.33, 2609.74, 2604.84, 2600.63] - dates = [datetime.fromtimestamp(ts) for ts in timestamps] + 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)) - # Plot with different colors and markers - plt.plot(dates, csv_prices, label='UNI Price', marker='o', color='blue', markersize=5) - plt.plot(dates, api_prices, label='API Price', marker='x', color='green', markersize=5) + # Plot each series with their actual timestamps + 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) - # Add more grid lines - plt.grid(True, which='both', ls='-', alpha=0.5) + # Set unified time range for both series + 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() - - # X-axis: grid/ticks every 30 minutes + ax.xaxis.set_major_locator(mdates.HourLocator(interval=2)) ax.xaxis.set_major_formatter(mdates.DateFormatter('%m-%d %H:%M')) - - # Y-axis: grid/ticks every $1 + ax.yaxis.set_major_locator(MultipleLocator(2)) - ax.yaxis.set_minor_locator(MultipleLocator(2)) - - # Grid lines + 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) - plt.gcf().autofmt_xdate() + differences = [abs(u - a) for u, a in zip(uni_prices, api_prices)] + max_diff = max(differences) + max_diff_index = differences.index(max_diff) + + # Get corresponding timestamps + 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') + + # Add text annotation + 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() From 75bde7a24cb4ef03b6349e18a3f286b4c7b79738 Mon Sep 17 00:00:00 2001 From: Haider-Ali-DS Date: Fri, 6 Jun 2025 12:21:28 +0500 Subject: [PATCH 8/9] ignore price saving test --- test/UniswapV2Oracle.t.sol | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/UniswapV2Oracle.t.sol b/test/UniswapV2Oracle.t.sol index 8e35c26..df2ea2c 100644 --- a/test/UniswapV2Oracle.t.sol +++ b/test/UniswapV2Oracle.t.sol @@ -41,6 +41,8 @@ contract UniswapV2OracleTest is Test { } 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"; From 1e974b57a9bcfb9ca5668db5b927c8fcd20cd945 Mon Sep 17 00:00:00 2001 From: Haider-Ali-DS Date: Fri, 6 Jun 2025 13:04:12 +0500 Subject: [PATCH 9/9] some comment updates --- plot_prices.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/plot_prices.py b/plot_prices.py index a2266c9..39193b9 100644 --- a/plot_prices.py +++ b/plot_prices.py @@ -39,8 +39,8 @@ def fetch_prices(timestamp): return last_item['time'], 0 def main(): csv_data = read_csv('uni_prices.csv') - uni_timestamps = [] + uni_prices = [] api_timestamps = [] api_prices = [] @@ -63,7 +63,6 @@ def main(): plt.figure(figsize=(14, 7)) - # Plot each series with their actual timestamps 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) @@ -72,7 +71,6 @@ def main(): plt.ylabel('Price (USD)', fontsize=12) plt.legend(fontsize=12) - # Set unified time range for both series all_dates = uni_dates + api_dates x_min = min(all_dates) x_max = max(all_dates) @@ -94,12 +92,10 @@ def main(): max_diff = max(differences) max_diff_index = differences.index(max_diff) - # Get corresponding timestamps 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') - # Add text annotation 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,