Skip to content
This repository has been archived by the owner on Oct 19, 2024. It is now read-only.

Commit

Permalink
feat: Transaction Gas Price Escalator middleware (#81)
Browse files Browse the repository at this point in the history
* fix(signers): make Signer send by blocking on Ledger calls

* fix(providers): use Arc in WS impl to allow cloning

* feat(middleware): add geometric gas price escalator

* test(middleware): ensure that we can still stack everything up

* fix(middleware): default to tokio/async-std

* chore: fix clippy

* docs(middleware): add docs and rename middlewares

* chore: fix doctests

* feat: add linear gas escalator

https://github.com/makerdao/pymaker/blob/master/tests/test_gas.py\#L107
https://github.com/makerdao/pymaker/blob/master/pymaker/gas.py\#L129

* feat: add constructors to gas escalators
  • Loading branch information
gakonst authored Oct 8, 2020
1 parent aa37f74 commit 62b7ce4
Show file tree
Hide file tree
Showing 28 changed files with 636 additions and 83 deletions.
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions ethers-contract/tests/common/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use ethers_core::{

use ethers_contract::{Contract, ContractFactory};
use ethers_core::utils::{GanacheInstance, Solc};
use ethers_middleware::Client;
use ethers_middleware::signer::SignerMiddleware;
use ethers_providers::{Http, Middleware, Provider};
use ethers_signers::LocalWallet;
use std::{convert::TryFrom, sync::Arc, time::Duration};
Expand Down Expand Up @@ -44,15 +44,15 @@ pub fn compile_contract(name: &str, filename: &str) -> (Abi, Bytes) {
(contract.abi.clone(), contract.bytecode.clone())
}

type HttpWallet = Client<Provider<Http>, LocalWallet>;
type HttpWallet = SignerMiddleware<Provider<Http>, LocalWallet>;

/// connects the private key to http://localhost:8545
pub fn connect(ganache: &GanacheInstance, idx: usize) -> Arc<HttpWallet> {
let provider = Provider::<Http>::try_from(ganache.endpoint())
.unwrap()
.interval(Duration::from_millis(10u64));
let wallet: LocalWallet = ganache.keys()[idx].clone().into();
Arc::new(Client::new(provider, wallet))
Arc::new(SignerMiddleware::new(provider, wallet))
}

/// Launches a ganache instance and deploys the SimpleStorage contract
Expand Down
4 changes: 2 additions & 2 deletions ethers-contract/tests/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -342,7 +342,7 @@ mod eth_tests {
mod celo_tests {
use super::*;
use ethers::{
middleware::Client,
middleware::signer::SignerMiddleware,
providers::{Http, Provider},
signers::LocalWallet,
types::BlockNumber,
Expand All @@ -363,7 +363,7 @@ mod celo_tests {
.parse::<LocalWallet>()
.unwrap();

let client = Client::new(provider, wallet);
let client = SignerMiddleware::new(provider, wallet);
let client = Arc::new(client);

let factory = ContractFactory::new(abi, bytecode, client);
Expand Down
5 changes: 5 additions & 0 deletions ethers-middleware/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,13 @@ serde-aux = "0.6.1"
reqwest = { version = "0.10.4", default-features = false, features = ["json", "rustls-tls"] }
url = { version = "2.1.1", default-features = false }

# optional for runtime
tokio = { version = "0.2.22", optional = true }
async-std = { version = "1.6.5", optional = true }

[dev-dependencies]
ethers = { version = "0.1.3", path = "../ethers" }
futures-executor = { version = "0.3.5", features = ["thread-pool"] }

rustc-hex = "2.1.0"
tokio = { version = "0.2.21", default-features = false, features = ["rt-core", "macros"] }
Expand Down
114 changes: 114 additions & 0 deletions ethers-middleware/src/gas_escalator/geometric.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
use super::GasEscalator;
use ethers_core::types::U256;

/// Geometrically increasing gas price.
///
/// Start with `initial_price`, then increase it every 'every_secs' seconds by a fixed coefficient.
/// Coefficient defaults to 1.125 (12.5%), the minimum increase for Parity to replace a transaction.
/// Coefficient can be adjusted, and there is an optional upper limit.
///
/// https://github.com/makerdao/pymaker/blob/master/pymaker/gas.py#L168
#[derive(Clone, Debug)]
pub struct GeometricGasPrice {
every_secs: u64,
coefficient: f64,
max_price: Option<U256>,
}

impl GeometricGasPrice {
/// Constructor
///
/// Note: Providing `None` to `max_price` requires giving it a type-hint, so you'll need
/// to call this like `GeometricGasPrice::new(1.125, 60u64, None::<u64>)`.
pub fn new<T: Into<U256>, K: Into<u64>>(
coefficient: f64,
every_secs: K,
max_price: Option<T>,
) -> Self {
GeometricGasPrice {
every_secs: every_secs.into(),
coefficient,
max_price: max_price.map(Into::into),
}
}
}

impl GasEscalator for GeometricGasPrice {
fn get_gas_price(&self, initial_price: U256, time_elapsed: u64) -> U256 {
let mut result = initial_price.as_u64() as f64;

if time_elapsed >= self.every_secs {
let iters = time_elapsed / self.every_secs;
for _ in 0..iters {
result *= self.coefficient;
}
}

let mut result = U256::from(result.ceil() as u64);
if let Some(max_price) = self.max_price {
result = std::cmp::min(result, max_price);
}
result
}
}

#[cfg(test)]
// https://github.com/makerdao/pymaker/blob/master/tests/test_gas.py#L165
mod tests {
use super::*;

#[test]
fn gas_price_increases_with_time() {
let oracle = GeometricGasPrice::new(1.125, 10u64, None::<u64>);
let initial_price = U256::from(100);

assert_eq!(oracle.get_gas_price(initial_price, 0), 100.into());
assert_eq!(oracle.get_gas_price(initial_price, 1), 100.into());
assert_eq!(oracle.get_gas_price(initial_price, 10), 113.into());
assert_eq!(oracle.get_gas_price(initial_price, 15), 113.into());
assert_eq!(oracle.get_gas_price(initial_price, 20), 127.into());
assert_eq!(oracle.get_gas_price(initial_price, 30), 143.into());
assert_eq!(oracle.get_gas_price(initial_price, 50), 181.into());
assert_eq!(oracle.get_gas_price(initial_price, 100), 325.into());
}

#[test]
fn gas_price_should_obey_max_value() {
let oracle = GeometricGasPrice::new(1.125, 60u64, Some(2500));
let initial_price = U256::from(1000);

assert_eq!(oracle.get_gas_price(initial_price, 0), 1000.into());
assert_eq!(oracle.get_gas_price(initial_price, 1), 1000.into());
assert_eq!(oracle.get_gas_price(initial_price, 59), 1000.into());
assert_eq!(oracle.get_gas_price(initial_price, 60), 1125.into());
assert_eq!(oracle.get_gas_price(initial_price, 119), 1125.into());
assert_eq!(oracle.get_gas_price(initial_price, 120), 1266.into());
assert_eq!(oracle.get_gas_price(initial_price, 1200), 2500.into());
assert_eq!(oracle.get_gas_price(initial_price, 3000), 2500.into());
assert_eq!(oracle.get_gas_price(initial_price, 1000000), 2500.into());
}

#[test]
fn behaves_with_realistic_values() {
let oracle = GeometricGasPrice::new(1.25, 10u64, None::<u64>);
const GWEI: f64 = 1000000000.0;
let initial_price = U256::from(100 * GWEI as u64);

for seconds in &[0u64, 1, 10, 12, 30, 60] {
println!(
"gas price after {} seconds is {}",
seconds,
oracle.get_gas_price(initial_price, *seconds).as_u64() as f64 / GWEI
);
}

let normalized = |time| oracle.get_gas_price(initial_price, time).as_u64() as f64 / GWEI;

assert_eq!(normalized(0), 100.0);
assert_eq!(normalized(1), 100.0);
assert_eq!(normalized(10), 125.0);
assert_eq!(normalized(12), 125.0);
assert_eq!(normalized(30), 195.3125);
assert_eq!(normalized(60), 381.469726563);
}
}
78 changes: 78 additions & 0 deletions ethers-middleware/src/gas_escalator/linear.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
use super::GasEscalator;
use ethers_core::types::U256;

/// Linearly increasing gas price.
///
///
/// Start with `initial_price`, then increase it by fixed amount `increase_by` every `every_secs` seconds
/// until the transaction gets confirmed. There is an optional upper limit.
///
/// https://github.com/makerdao/pymaker/blob/master/pymaker/gas.py#L129
#[derive(Clone, Debug)]
pub struct LinearGasPrice {
every_secs: u64,
increase_by: U256,
max_price: Option<U256>,
}

impl LinearGasPrice {
/// Constructor
pub fn new<T: Into<U256>>(
increase_by: T,
every_secs: impl Into<u64>,
max_price: Option<T>,
) -> Self {
LinearGasPrice {
every_secs: every_secs.into(),
increase_by: increase_by.into(),
max_price: max_price.map(Into::into),
}
}
}

impl GasEscalator for LinearGasPrice {
fn get_gas_price(&self, initial_price: U256, time_elapsed: u64) -> U256 {
let mut result = initial_price + self.increase_by * (time_elapsed / self.every_secs) as u64;
dbg!(time_elapsed, self.every_secs);
if let Some(max_price) = self.max_price {
result = std::cmp::min(result, max_price);
}
result
}
}

#[cfg(test)]
// https://github.com/makerdao/pymaker/blob/master/tests/test_gas.py#L107
mod tests {
use super::*;

#[test]
fn gas_price_increases_with_time() {
let oracle = LinearGasPrice::new(100, 60u64, None);
let initial_price = U256::from(1000);

assert_eq!(oracle.get_gas_price(initial_price, 0), 1000.into());
assert_eq!(oracle.get_gas_price(initial_price, 1), 1000.into());
assert_eq!(oracle.get_gas_price(initial_price, 59), 1000.into());
assert_eq!(oracle.get_gas_price(initial_price, 60), 1100.into());
assert_eq!(oracle.get_gas_price(initial_price, 119), 1100.into());
assert_eq!(oracle.get_gas_price(initial_price, 120), 1200.into());
assert_eq!(oracle.get_gas_price(initial_price, 1200), 3000.into());
}

#[test]
fn gas_price_should_obey_max_value() {
let oracle = LinearGasPrice::new(100, 60u64, Some(2500));
let initial_price = U256::from(1000);

assert_eq!(oracle.get_gas_price(initial_price, 0), 1000.into());
assert_eq!(oracle.get_gas_price(initial_price, 1), 1000.into());
assert_eq!(oracle.get_gas_price(initial_price, 59), 1000.into());
assert_eq!(oracle.get_gas_price(initial_price, 60), 1100.into());
assert_eq!(oracle.get_gas_price(initial_price, 119), 1100.into());
assert_eq!(oracle.get_gas_price(initial_price, 120), 1200.into());
assert_eq!(oracle.get_gas_price(initial_price, 1200), 2500.into());
assert_eq!(oracle.get_gas_price(initial_price, 3000), 2500.into());
assert_eq!(oracle.get_gas_price(initial_price, 1000000), 2500.into());
}
}
Loading

0 comments on commit 62b7ce4

Please sign in to comment.