Skip to content

Commit

Permalink
chore: add gas price spike protection
Browse files Browse the repository at this point in the history
  • Loading branch information
alexandre-abrioux committed May 1, 2022
1 parent fb7369c commit 21ba5b9
Show file tree
Hide file tree
Showing 6 changed files with 159 additions and 8 deletions.
1 change: 1 addition & 0 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,4 @@ export const GAS_STATION_API_CHAIN_ID = 1;
export const GAS_STATION_API_URL =
"https://ethgasstation.info/json/ethgasAPI.json";
export const GAS_STATION_API_REQUEST_TIMEOUT_MS = 10000;
export const MAX_GAS_PRICE_GWEI = 1000;
20 changes: 16 additions & 4 deletions src/gas-price/gas-price-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
GAS_STATION_API_CHAIN_ID,
GAS_STATION_API_REQUEST_TIMEOUT_MS,
GAS_STATION_API_URL,
MAX_GAS_PRICE_GWEI,
} from "../config";
import GasStationGasPriceProvider from "./providers/gas-station-gas-price-provider";
import ProviderGasPriceProvider from "./providers/provider-gas-price-provider";
Expand All @@ -24,6 +25,7 @@ import Eip1559GasPriceProvider, {
Eip1559Profile,
} from "./providers/eip1559-gas-price-provider";
import { BigNumber } from "ethers";
import SpikeProtectionGasPriceProvider from "./providers/spike-protection-gas-price-provider";

export const gasPriceProviderTypes = [
"eth-provider",
Expand Down Expand Up @@ -62,6 +64,7 @@ export const createGasPriceProvider = async (
provider,
GAS_PRICE_MULTIPLIER
);
let proxiedGasPriceProvider: GasPriceProvider = providerGasPriceProvider;

switch (type) {
case "fastest":
Expand All @@ -83,10 +86,11 @@ export const createGasPriceProvider = async (
timeout: GAS_STATION_API_REQUEST_TIMEOUT_MS,
profile: type,
});
return new ChainGasPriceProvider([
proxiedGasPriceProvider = new ChainGasPriceProvider([
gasStationProvider,
providerGasPriceProvider,
]);
break;
case "eip-1559-urgent":
case "eip-1559-fast":
case "eip-1559-normal":
Expand All @@ -110,9 +114,17 @@ export const createGasPriceProvider = async (
profile = "normal";
break;
}
return new Eip1559GasPriceProvider(provider, profile);
proxiedGasPriceProvider = new Eip1559GasPriceProvider(
provider,
profile
);
break;
default:
log.debug(`using gas price predictor from ethereum provider`);
}

log.debug(`using gas price predictor from ethereum provider`);
return providerGasPriceProvider;
return new SpikeProtectionGasPriceProvider(
proxiedGasPriceProvider,
MAX_GAS_PRICE_GWEI
);
};
47 changes: 47 additions & 0 deletions src/gas-price/providers/spike-protection-gas-price-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright 2020 Cartesi Pte. Ltd.

// Licensed under the Apache License, Version 2.0 (the "License"); you may not use
// this file except in compliance with the License. You may obtain a copy of the
// License at http://www.apache.org/licenses/LICENSE-2.0

// Unless required by applicable law or agreed to in writing, software distributed
// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
// CONDITIONS OF ANY KIND, either express or implied. See the License for the
// specific language governing permissions and limitations under the License.

import { GasPriceOverrides, GasPriceProvider } from "../gas-price-provider";
import { BigNumber, utils } from "ethers";

export default class SpikeProtectionGasPriceProvider
implements GasPriceProvider
{
private readonly proxiedGasPriceProviders: GasPriceProvider;
private readonly maxGasPrice: BigNumber;

constructor(
proxiedGasPriceProviders: GasPriceProvider,
maxGasPriceGwei: number
) {
this.proxiedGasPriceProviders = proxiedGasPriceProviders;
this.maxGasPrice = utils.parseUnits(maxGasPriceGwei.toString(), "gwei");
}

getGasPrice = async (): Promise<GasPriceOverrides> => {
const overrides = await this.proxiedGasPriceProviders.getGasPrice();
let gasPrice: BigNumber;
if ("maxFeePerGas" in overrides) {
gasPrice = overrides.maxFeePerGas;
} else {
gasPrice = overrides.gasPrice;
}
if (gasPrice.lt(this.maxGasPrice)) {
return overrides;
}
throw new Error(
`Gas price higher than ${utils.formatUnits(
this.maxGasPrice,
"gwei"
)}`
);
};
}
4 changes: 2 additions & 2 deletions test/gas-price/providers/chain-gas-price-provider.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ describe("chain gas price provider test suite", () => {
it("should not provide gas price with no providers", async () => {
sandbox.stub(log, "error").returns();
const gasPriceProvider = new ChainGasPriceProvider([]);
expect(gasPriceProvider.getGasPrice()).to.be.rejectedWith(
await expect(gasPriceProvider.getGasPrice()).to.be.rejectedWith(
"no valid gas price returned from the chain of gas price providers"
);
});
Expand All @@ -77,7 +77,7 @@ describe("chain gas price provider test suite", () => {
gasPriceProvider1,
gasPriceProvider2,
]);
expect(gasPriceProvider.getGasPrice()).to.be.rejectedWith(
await expect(gasPriceProvider.getGasPrice()).to.be.rejectedWith(
"no valid gas price returned from the chain of gas price providers"
);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,15 @@ describe("gas station gas price provider test suite", () => {
sandbox.stub(log, "error").returns();
sandbox.stub(axios, "get").returns(Promise.reject("test error"));
const provider = new GasStationGasPriceProvider();
expect(provider.getGasPrice).to.throw;
await expect(provider.getGasPrice()).to.be.rejected;
});

it("should reject on missing price", async () => {
const profile: GasStationProfile = "fast";
sandbox.stub(log, "error").returns();
sandbox.stub(axios, "get").returns(Promise.resolve({ data: {} }));
const gasPriceProvider = new GasStationGasPriceProvider({ profile });
expect(gasPriceProvider.getGasPrice()).to.eventually.throw(
await expect(gasPriceProvider.getGasPrice()).to.be.rejectedWith(
`gas station did not return a ${profile} price`
);
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
// Copyright 2021 Cartesi Pte. Ltd.

// Licensed under the Apache License, Version 2.0 (the "License"); you may not use
// this file except in compliance with the License. You may obtain a copy of the
// License at http://www.apache.org/licenses/LICENSE-2.0

// Unless required by applicable law or agreed to in writing, software distributed
// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
// CONDITIONS OF ANY KIND, either express or implied. See the License for the
// specific language governing permissions and limitations under the License.

import chain, { expect } from "chai";
import chaiAsPromised from "chai-as-promised";
import sinon from "sinon";
import {
GasPriceOverride,
GasPriceOverrideEip1559,
GasPriceOverrides,
GasPriceProvider,
} from "../../../src/gas-price/gas-price-provider";
import { BigNumber, utils } from "ethers";
import ChainGasPriceProvider from "../../../src/gas-price/providers/chain-gas-price-provider";
import log from "loglevel";
import SpikeProtectionGasPriceProvider from "../../../src/gas-price/providers/spike-protection-gas-price-provider";

chain.use(chaiAsPromised);
const sandbox = sinon.createSandbox();

class gasProviderMock implements GasPriceProvider {
private readonly gasPrice: BigNumber;
private readonly isEip1559: boolean;

constructor(gasPrice: BigNumber, isEip1559 = false) {
this.gasPrice = gasPrice;
this.isEip1559 = isEip1559;
}

async getGasPrice(): Promise<GasPriceOverrides> {
if (this.isEip1559) {
const maxPriorityFeePerGas = utils.parseUnits("2", "gwei");
const maxFeePerGas = maxPriorityFeePerGas.add(this.gasPrice);
return {
maxFeePerGas: maxFeePerGas,
maxPriorityFeePerGas: maxPriorityFeePerGas,
};
}
return { gasPrice: this.gasPrice };
}
}

describe("spike protection gas price provider test suite", () => {
afterEach(() => {
sandbox.restore();
});

it("should provide gas price when under threshold", async () => {
sandbox.stub(log, "error").returns();
const currentGasPrice = utils.parseUnits("50", "gwei");
const gasPriceProvider1 = new SpikeProtectionGasPriceProvider(
new gasProviderMock(currentGasPrice, false),
100
);
const gasPriceProvider2 = new SpikeProtectionGasPriceProvider(
new gasProviderMock(currentGasPrice, true),
100
);
const gasPrice1 =
(await gasPriceProvider1.getGasPrice()) as GasPriceOverride;
const gasPrice2 =
(await gasPriceProvider2.getGasPrice()) as GasPriceOverrideEip1559;
expect(gasPrice1.gasPrice).to.be.deep.eq(currentGasPrice);
expect(gasPrice2.maxFeePerGas).to.be.deep.eq(
currentGasPrice.add(gasPrice2.maxPriorityFeePerGas)
);
});

it("should reject when over threshold", async () => {
sandbox.stub(log, "error").returns();
const currentGasPrice = utils.parseUnits("50", "gwei");
const gasPriceProvider1 = new SpikeProtectionGasPriceProvider(
new gasProviderMock(currentGasPrice, false),
50
);
const gasPriceProvider2 = new SpikeProtectionGasPriceProvider(
new gasProviderMock(currentGasPrice, true),
50
);
await expect(gasPriceProvider1.getGasPrice()).to.be.rejected;
await expect(gasPriceProvider2.getGasPrice()).to.be.rejected;
});
});

0 comments on commit 21ba5b9

Please # to comment.