Initial commit

This commit is contained in:
Tornado Contrib 2024-04-11 23:23:08 +00:00
commit ea7498bc2a
Signed by: tornadocontrib
GPG Key ID: 60B4DF1A076C64B1
16 changed files with 5207 additions and 0 deletions

49
.eslintrc.js Normal file

@ -0,0 +1,49 @@
module.exports = {
"env": {
"es2021": true,
"node": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended"
],
"overrides": [
{
"env": {
"node": true
},
"files": [
".eslintrc.{js,cjs}"
],
"parserOptions": {
"sourceType": "script"
}
}
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"plugins": [
"@typescript-eslint"
],
"rules": {
"indent": [
"error",
2
],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"error",
"single"
],
"semi": [
"error",
"always"
]
}
}

16
.gitignore vendored Normal file

@ -0,0 +1,16 @@
node_modules
.env
# Hardhat files
/cache
/artifacts
# TypeChain files
/typechain
/typechain-types
# solidity-coverage files
/coverage
/coverage.json
/flatten

11
Dockerfile Normal file

@ -0,0 +1,11 @@
FROM node:20-alpine
WORKDIR /app
COPY ["package.json", "yarn.lock", "./"]
RUN npm i -g yarn && yarn
COPY . .
EXPOSE 7000
ENTRYPOINT ["yarn", "start"]

3
README.md Normal file

@ -0,0 +1,3 @@
# gas-price-oracle
Decentralized Gas Price Oracle that proxies recommended fee value from [polygon gas station](https://docs.polygon.technology/tools/gas/polygon-gas-station/#mainnet)

@ -0,0 +1,69 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/**
* @dev Updates Polygon's recommended maxPriorityFeePerGas from polygon gas station
*/
contract GasPriceOracle {
address public owner;
uint32 public pastGasPrice;
uint256 public GAS_UNIT = 1e9;
uint32 public timestamp;
/**
* @dev Similar with how the chainlink's gas price feed works,
* A new answer is written when the gas price moves
* more than the derivation thresold or heartbeat ( 2 hours )
* have passed since the last answer was written onchain
*/
uint32 public derivationThresold = 25;
uint32 public heartbeat = 2 hours;
modifier onlyOwner {
require(msg.sender == owner);
_;
}
constructor() {
owner = msg.sender;
timestamp = uint32(block.timestamp);
}
function changeOwnership(address _owner) external onlyOwner {
owner = _owner;
}
function changeGasUnit(uint32 _gasUnit) external onlyOwner {
GAS_UNIT = _gasUnit;
}
function changeDerivationThresold(uint32 _derivationThresold) external onlyOwner {
derivationThresold = _derivationThresold;
}
function changeHeartbeat(uint32 _heartbeat) external onlyOwner {
heartbeat = _heartbeat;
}
function setGasPrice(uint32 _gasPrice) external onlyOwner {
pastGasPrice = _gasPrice;
timestamp = uint32(block.timestamp);
}
function gasPrice() external view returns (uint256) {
return GAS_UNIT * uint256(pastGasPrice);
}
function maxFeePerGas() external view returns (uint256) {
return block.basefee;
}
function maxPriorityFeePerGas() external view returns (uint256) {
return GAS_UNIT * uint256(pastGasPrice);
}
}

216
contracts/Multicall3.sol Normal file

@ -0,0 +1,216 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.12;
/// @title Multicall3
/// @notice Aggregate results from multiple function calls
/// @dev Multicall & Multicall2 backwards-compatible
/// @dev Aggregate methods are marked `payable` to save 24 gas per call
/// @author Michael Elliot <mike@makerdao.com>
/// @author Joshua Levine <joshua@makerdao.com>
/// @author Nick Johnson <arachnid@notdot.net>
/// @author Andreas Bigger <andreas@nascent.xyz>
/// @author Matt Solomon <matt@mattsolomon.dev>
contract Multicall3 {
struct Call {
address target;
bytes callData;
}
struct Call3 {
address target;
bool allowFailure;
bytes callData;
}
struct Call3Value {
address target;
bool allowFailure;
uint256 value;
bytes callData;
}
struct Result {
bool success;
bytes returnData;
}
/// @notice Backwards-compatible call aggregation with Multicall
/// @param calls An array of Call structs
/// @return blockNumber The block number where the calls were executed
/// @return returnData An array of bytes containing the responses
function aggregate(Call[] calldata calls) public payable returns (uint256 blockNumber, bytes[] memory returnData) {
blockNumber = block.number;
uint256 length = calls.length;
returnData = new bytes[](length);
Call calldata call;
for (uint256 i = 0; i < length;) {
bool success;
call = calls[i];
(success, returnData[i]) = call.target.call(call.callData);
require(success, "Multicall3: call failed");
unchecked { ++i; }
}
}
/// @notice Backwards-compatible with Multicall2
/// @notice Aggregate calls without requiring success
/// @param requireSuccess If true, require all calls to succeed
/// @param calls An array of Call structs
/// @return returnData An array of Result structs
function tryAggregate(bool requireSuccess, Call[] calldata calls) public payable returns (Result[] memory returnData) {
uint256 length = calls.length;
returnData = new Result[](length);
Call calldata call;
for (uint256 i = 0; i < length;) {
Result memory result = returnData[i];
call = calls[i];
(result.success, result.returnData) = call.target.call(call.callData);
if (requireSuccess) require(result.success, "Multicall3: call failed");
unchecked { ++i; }
}
}
/// @notice Backwards-compatible with Multicall2
/// @notice Aggregate calls and allow failures using tryAggregate
/// @param calls An array of Call structs
/// @return blockNumber The block number where the calls were executed
/// @return blockHash The hash of the block where the calls were executed
/// @return returnData An array of Result structs
function tryBlockAndAggregate(bool requireSuccess, Call[] calldata calls) public payable returns (uint256 blockNumber, bytes32 blockHash, Result[] memory returnData) {
blockNumber = block.number;
blockHash = blockhash(block.number);
returnData = tryAggregate(requireSuccess, calls);
}
/// @notice Backwards-compatible with Multicall2
/// @notice Aggregate calls and allow failures using tryAggregate
/// @param calls An array of Call structs
/// @return blockNumber The block number where the calls were executed
/// @return blockHash The hash of the block where the calls were executed
/// @return returnData An array of Result structs
function blockAndAggregate(Call[] calldata calls) public payable returns (uint256 blockNumber, bytes32 blockHash, Result[] memory returnData) {
(blockNumber, blockHash, returnData) = tryBlockAndAggregate(true, calls);
}
/// @notice Aggregate calls, ensuring each returns success if required
/// @param calls An array of Call3 structs
/// @return returnData An array of Result structs
function aggregate3(Call3[] calldata calls) public payable returns (Result[] memory returnData) {
uint256 length = calls.length;
returnData = new Result[](length);
Call3 calldata calli;
for (uint256 i = 0; i < length;) {
Result memory result = returnData[i];
calli = calls[i];
(result.success, result.returnData) = calli.target.call(calli.callData);
assembly {
// Revert if the call fails and failure is not allowed
// `allowFailure := calldataload(add(calli, 0x20))` and `success := mload(result)`
if iszero(or(calldataload(add(calli, 0x20)), mload(result))) {
// set "Error(string)" signature: bytes32(bytes4(keccak256("Error(string)")))
mstore(0x00, 0x08c379a000000000000000000000000000000000000000000000000000000000)
// set data offset
mstore(0x04, 0x0000000000000000000000000000000000000000000000000000000000000020)
// set length of revert string
mstore(0x24, 0x0000000000000000000000000000000000000000000000000000000000000017)
// set revert string: bytes32(abi.encodePacked("Multicall3: call failed"))
mstore(0x44, 0x4d756c746963616c6c333a2063616c6c206661696c6564000000000000000000)
revert(0x00, 0x64)
}
}
unchecked { ++i; }
}
}
/// @notice Aggregate calls with a msg value
/// @notice Reverts if msg.value is less than the sum of the call values
/// @param calls An array of Call3Value structs
/// @return returnData An array of Result structs
function aggregate3Value(Call3Value[] calldata calls) public payable returns (Result[] memory returnData) {
uint256 valAccumulator;
uint256 length = calls.length;
returnData = new Result[](length);
Call3Value calldata calli;
for (uint256 i = 0; i < length;) {
Result memory result = returnData[i];
calli = calls[i];
uint256 val = calli.value;
// Humanity will be a Type V Kardashev Civilization before this overflows - andreas
// ~ 10^25 Wei in existence << ~ 10^76 size uint fits in a uint256
unchecked { valAccumulator += val; }
(result.success, result.returnData) = calli.target.call{value: val}(calli.callData);
assembly {
// Revert if the call fails and failure is not allowed
// `allowFailure := calldataload(add(calli, 0x20))` and `success := mload(result)`
if iszero(or(calldataload(add(calli, 0x20)), mload(result))) {
// set "Error(string)" signature: bytes32(bytes4(keccak256("Error(string)")))
mstore(0x00, 0x08c379a000000000000000000000000000000000000000000000000000000000)
// set data offset
mstore(0x04, 0x0000000000000000000000000000000000000000000000000000000000000020)
// set length of revert string
mstore(0x24, 0x0000000000000000000000000000000000000000000000000000000000000017)
// set revert string: bytes32(abi.encodePacked("Multicall3: call failed"))
mstore(0x44, 0x4d756c746963616c6c333a2063616c6c206661696c6564000000000000000000)
revert(0x00, 0x84)
}
}
unchecked { ++i; }
}
// Finally, make sure the msg.value = SUM(call[0...i].value)
require(msg.value == valAccumulator, "Multicall3: value mismatch");
}
/// @notice Returns the block hash for the given block number
/// @param blockNumber The block number
function getBlockHash(uint256 blockNumber) public view returns (bytes32 blockHash) {
blockHash = blockhash(blockNumber);
}
/// @notice Returns the block number
function getBlockNumber() public view returns (uint256 blockNumber) {
blockNumber = block.number;
}
/// @notice Returns the block coinbase
function getCurrentBlockCoinbase() public view returns (address coinbase) {
coinbase = block.coinbase;
}
/// @notice Returns the block difficulty
function getCurrentBlockDifficulty() public view returns (uint256 difficulty) {
difficulty = block.difficulty;
}
/// @notice Returns the block gas limit
function getCurrentBlockGasLimit() public view returns (uint256 gaslimit) {
gaslimit = block.gaslimit;
}
/// @notice Returns the block timestamp
function getCurrentBlockTimestamp() public view returns (uint256 timestamp) {
timestamp = block.timestamp;
}
/// @notice Returns the (ETH) balance of a given address
function getEthBalance(address addr) public view returns (uint256 balance) {
balance = addr.balance;
}
/// @notice Returns the block hash of the last block
function getLastBlockHash() public view returns (bytes32 blockHash) {
unchecked {
blockHash = blockhash(block.number - 1);
}
}
/// @notice Gets the base fee of the given block
/// @notice Can revert if the BASEFEE opcode is not implemented by the given chain
function getBasefee() public view returns (uint256 basefee) {
basefee = block.basefee;
}
/// @notice Returns the chain id
function getChainId() public view returns (uint256 chainid) {
chainid = block.chainid;
}
}

33
docker-compose.yml Normal file

@ -0,0 +1,33 @@
version: "3"
services:
maticgasstation:
container_name: maticgasstation
image: maticgasstation
restart: always
stop_grace_period: 30m
environment:
- POS_RPC=https://polygon-mainnet.chainnodes.org/d692ae63-0a7e-43e0-9da9-fe4f4cc6c607
- ZKEVM_RPC=https://1rpc.io/polygon/zkevm
- PORT=7000
- SAFE=30
- STANDARD=32
- FAST=50
- HISTORY_BLOCKS=15
build:
context: .
dockerfile: maticgasstation.Dockerfile
expose:
- '127.0.0.1:7000:7000'
gaspriceoracle:
container_name: gaspriceoracle
image: gaspriceoracle
restart: always
stop_grace_period: 30m
env_file:
- ./docker.env
build:
context: .
expose:
- '127.0.0.1:7000:7000'

3
docker.env Normal file

@ -0,0 +1,3 @@
GAS_STATION=http://maticgasstation:7000
RPC_URL=https://polygon-mainnet.chainnodes.org/d692ae63-0a7e-43e0-9da9-fe4f4cc6c607
MNEMONIC=

7
hardhat.config.d.ts vendored Normal file

@ -0,0 +1,7 @@
import { HardhatUserConfig } from 'hardhat/config';
import '@nomicfoundation/hardhat-toolbox';
import '@nomicfoundation/hardhat-ethers';
import 'hardhat-storage-layout';
import 'hardhat-tracer';
declare const config: HardhatUserConfig;
export default config;

78
hardhat.config.ts Normal file

@ -0,0 +1,78 @@
import fs from 'fs';
import path from 'path';
import process from 'process';
import { task, HardhatUserConfig } from 'hardhat/config';
import '@nomicfoundation/hardhat-toolbox';
import '@nomicfoundation/hardhat-ethers';
import 'hardhat-storage-layout';
import 'hardhat-tracer';
task('flatten:all', 'Flatten all contracts each file under flatten directory')
.setAction(async (taskArgs, hre) => {
const allFilesAndFolders = fs.readdirSync('contracts', { recursive: true }) as Array<string>;
const allFolders = allFilesAndFolders.filter(f => fs.statSync(path.join('contracts', f)).isDirectory());
const allFiles = allFilesAndFolders.filter(f => !allFolders.includes(f));
fs.rmSync('flatten', { force: true, recursive: true });
fs.mkdirSync('flatten');
allFolders.forEach(f => {
fs.mkdirSync(path.join('flatten', f), { recursive: true });
});
await Promise.all(allFiles.map(async (f) => {
const contract = path.join('contracts', f);
const contractTo = path.join('flatten', f);
try {
const flatten = await hre.run('flatten:get-flattened-sources', { files: [contract] });
fs.writeFileSync(contractTo, flatten);
console.log(`Wrote ${contractTo} contract`);
} catch (e) {
// Catching circular contracts
console.log(`Failed to write ${contractTo} contract`);
console.log(e);
}
}));
});
const config: HardhatUserConfig = {
defaultNetwork: 'hardhat',
solidity: {
compilers: [
{
version: '0.8.25',
settings: {
evmVersion: 'paris',
optimizer: {
enabled: true,
runs: 200,
},
},
},
],
},
networks: {
develop: {
url: process.env.RPC_URL || '',
accounts: {
mnemonic: process.env.MNEMONIC || 'test test test test test test test test test test test junk',
initialIndex: Number(process.env.MNEMONIC_INDEX) || 0,
},
},
polygon: {
url: process.env.RPC_URL || 'https://polygon-mainnet.chainnodes.org/d692ae63-0a7e-43e0-9da9-fe4f4cc6c607',
accounts: {
mnemonic: process.env.MNEMONIC || 'test test test test test test test test test test test junk',
initialIndex: Number(process.env.MNEMONIC_INDEX) || 0,
},
},
hardhat: {},
},
etherscan: {
apiKey: process.env.ETHERSCAN
},
sourcify: {
enabled: true
}
};
export default config;

@ -0,0 +1,23 @@
FROM ubuntu:jammy
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update \
&& apt-get install -y curl nano git wget nmap net-tools build-essential software-properties-common \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /root
RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
apt-get install -y nodejs
ENV REPO=https://github.com/maticnetwork/maticgasstation
ENV VERSION=ca8c49c24de98dedac7196b1b07068feeebe856a
RUN git clone $REPO --branch $VERSION && \
cd maticgasstation && \
npm i
EXPOSE 7000
ENTRYPOINT ["node", "src/index.js"]

52
package.json Normal file

@ -0,0 +1,52 @@
{
"name": "gas-price-oracle",
"version": "1.0.0",
"main": "index.js",
"license": "MIT",
"scripts": {
"types": "tsc --declaration --emitDeclarationOnly",
"compile": "hardhat compile && hardhat flatten:all",
"build": "yarn compile && yarn types",
"start": "yarn build && ts-node ./src/index.ts",
"lint": "eslint . --ext .ts --ignore-pattern typechain-types"
},
"files": [
"contracts",
"src",
".eslintrc.js",
".gitignore",
"docker-compose.yml",
"docker.env",
"Dockerfile",
"maticgasstation.Dockerfile",
"hardhat.config.ts",
"tsconfig.json",
"yarn.lock"
],
"devDependencies": {
"@nomicfoundation/hardhat-chai-matchers": "^2.0.6",
"@nomicfoundation/hardhat-ethers": "^3.0.5",
"@nomicfoundation/hardhat-network-helpers": "^1.0.10",
"@nomicfoundation/hardhat-toolbox": "4.0.0",
"@nomicfoundation/hardhat-verify": "^2.0.5",
"@typechain/ethers-v6": "^0.5.1",
"@typechain/hardhat": "^9.1.0",
"@types/chai": "^4.3.14",
"@types/mocha": "^9.1.1",
"@types/node": "^20.12.2",
"@typescript-eslint/eslint-plugin": "^7.4.0",
"@typescript-eslint/parser": "^7.4.0",
"chai": "^4.4.1",
"dotenv": "^16.4.5",
"eslint": "^8.57.0",
"ethers": "^6.11.1",
"hardhat": "2.20.1",
"hardhat-gas-reporter": "^1.0.10",
"hardhat-storage-layout": "^0.1.7",
"hardhat-tracer": "^2.8.1",
"solidity-coverage": "^0.8.11",
"ts-node": "^10.9.2",
"typechain": "^8.3.2",
"typescript": "^5.4.3"
}
}

14
src/index.d.ts vendored Normal file

@ -0,0 +1,14 @@
import { BaseContract } from 'ethers';
import "dotenv/config";
export interface gasstation {
standard: {
maxPriorityFee: number;
maxFee: number;
};
}
export interface Call3 {
contract: BaseContract;
name: string;
params?: any[];
allowFailure?: boolean;
}

145
src/index.ts Normal file

@ -0,0 +1,145 @@
import { JsonRpcProvider, Wallet, HDNodeWallet, BaseContract, parseUnits } from 'ethers';
import { GasPriceOracle, GasPriceOracle__factory, Multicall3, Multicall3__factory } from '../typechain-types';
import "dotenv/config"
const ORACLE_ADDRESS = process.env.ORACLE_ADDRESS || '0xF81A8D8D3581985D3969fe53bFA67074aDFa8F3C';
const GAS_STATION = process.env.GAS_STATION || 'https://gasstation.polygon.technology/v2';
const UPDATE_INTERVAL = Number(process.env.UPDATE_INTERVAL) || 600;
const RPC_URL = process.env.RPC_URL as string;
const MNEMONIC = process.env.MNEMONIC as string;
let wallet: HDNodeWallet;
let Oracle: GasPriceOracle;
let Multicall: Multicall3;
export interface gasstation {
standard: {
maxPriorityFee: number;
maxFee: number;
};
}
export interface Call3 {
contract: BaseContract;
name: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
params?: any[];
allowFailure?: boolean;
}
async function multicall(calls: Call3[]) {
const calldata = calls.map((call) => ({
target: call.contract.target,
callData: call.contract.interface.encodeFunctionData(call.name, call.params),
allowFailure: call.allowFailure ?? false
}));
const returnData = await Multicall.aggregate3.staticCall(calldata);
const res = returnData.map((call, i) => {
const [result, data] = call;
const decodeResult = (result && data && data !== '0x') ? calls[i].contract.interface.decodeFunctionResult(calls[i].name, data) : null;
return !decodeResult ? null : decodeResult.length === 1 ? decodeResult[0] : decodeResult;
});
return res;
}
async function fetchGasStation() {
const { standard } = await (await fetch(GAS_STATION, {
method: 'GET',
headers: {
'Accept': 'application/json'
}
})).json() as unknown as gasstation;
return standard;
}
async function updateOracle() {
try {
const [[owner, pastGasPrice, timestamp, derivationThresold, heartbeat], gasData] = await Promise.all([
multicall([
{
contract: Oracle,
name: 'owner'
},
{
contract: Oracle,
name: 'pastGasPrice'
},
{
contract: Oracle,
name: 'timestamp'
},
{
contract: Oracle,
name: 'derivationThresold'
},
{
contract: Oracle,
name: 'heartbeat'
}
]),
fetchGasStation(),
]);
if (wallet.address !== owner) {
const errMsg = `Connected wallet ${wallet.address} is not an owner (${owner}) of the Oracle contract!`;
throw new Error(errMsg);
}
const currentMaxFee = parseInt(`${gasData.maxFee}`);
const currentPriorityFee = parseInt(`${gasData.maxPriorityFee}`);
const isInRange = currentPriorityFee * Number(derivationThresold) / 100 <= Number(pastGasPrice)
&& currentPriorityFee * (100 + Number(derivationThresold)) / 100 >= Number(pastGasPrice);
const isOutdated = Number(timestamp) <= (Date.now() / 1000) - Number(heartbeat) + (UPDATE_INTERVAL * 2);
const shouldUpdate = !isInRange || isOutdated;
if (!shouldUpdate) {
console.log(`Skipping gas price update, current ${currentPriorityFee} recorded ${pastGasPrice}`);
return;
}
const TxPriorityFeePerGas = parseUnits(`${currentPriorityFee}`, 'gwei') * 12n / 10n;
await Oracle.setGasPrice(currentPriorityFee, {
gasLimit: '50000',
maxFeePerGas: parseUnits(`${currentMaxFee}`, 'gwei') * 2n + TxPriorityFeePerGas,
maxPriorityFeePerGas: TxPriorityFeePerGas,
}).then(t => t.wait());
console.log(`Updated gas price to ${currentPriorityFee}`);
} catch (err) {
console.log('Failed to update gas price');
console.log(err);
}
}
async function start() {
console.log('Starting gas price oracle');
const staticNetwork = await new JsonRpcProvider(RPC_URL).getNetwork();
const provider = new JsonRpcProvider(RPC_URL, staticNetwork, {
staticNetwork,
});
provider.pollingInterval = 1000;
wallet = Wallet.fromPhrase(MNEMONIC, provider);
Oracle = GasPriceOracle__factory.connect(ORACLE_ADDRESS, wallet);
Multicall = Multicall3__factory.connect('0xcA11bde05977b3631167028862bE2a173976CA11', provider);
updateOracle();
setInterval(updateOracle, UPDATE_INTERVAL * 1000);
}
start();

20
tsconfig.json Normal file

@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "es2020",
"module": "commonjs",
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"types": ["@types/node"]
},
"include": [
"./src",
"./scripts",
"./test",
"./typechain-types"
],
"exclude": ["node_modules"],
"files": ["./hardhat.config.ts"]
}

4468
yarn.lock Normal file

File diff suppressed because it is too large Load Diff