Initial commit
This commit is contained in:
commit
ea7498bc2a
49
.eslintrc.js
Normal file
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
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
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
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)
|
69
contracts/GasPriceOracle.sol
Normal file
69
contracts/GasPriceOracle.sol
Normal file
@ -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
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
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
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
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
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;
|
23
maticgasstation.Dockerfile
Normal file
23
maticgasstation.Dockerfile
Normal file
@ -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
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
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
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
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"]
|
||||
}
|
Loading…
Reference in New Issue
Block a user