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