Skip to content
This repository was archived by the owner on Jun 24, 2025. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 #
Expand Down
109 changes: 109 additions & 0 deletions plot_prices.py
Original file line number Diff line number Diff line change
@@ -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()
41 changes: 41 additions & 0 deletions src/oracle/GasNetworkOracle.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
13 changes: 13 additions & 0 deletions src/oracle/IOracle.sol
Original file line number Diff line number Diff line change
@@ -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);
}
85 changes: 85 additions & 0 deletions src/oracle/UniswapV2Oracle.sol
Original file line number Diff line number Diff line change
@@ -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;
}
}
23 changes: 23 additions & 0 deletions test/GasNetworkOracle.t.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
74 changes: 74 additions & 0 deletions test/UniswapV2Oracle.t.sol
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading