Skip to content

Commit

Permalink
feat(example): use custom handlers to pay for gas with erc-20 (#1901)
Browse files Browse the repository at this point in the history
* feat: add erc20_gas and impl PreExecutionHandler

* feat: shorten

* feat: impl post execution trait

* feat: impl Erc20PostExecution new

* fix: Erc20PostExecutionError

* feat: add validation

* feat: add main and fix imports

* feat: complete impl

* feat: organize handlers

* chore: remove pub

* feat: custom evm with generic handlers

* chore: naming

* fix address

* fix workspace test

* fix workspace test

* clippy

* resolve merge

* cleanup and doc
  • Loading branch information
royvardhan authored Dec 31, 2024
1 parent 543b4bb commit 60ce865
Show file tree
Hide file tree
Showing 8 changed files with 579 additions and 0 deletions.
14 changes: 14 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ members = [
"examples/database_components",
"examples/uniswap_get_reserves",
"examples/uniswap_v2_usdc_swap",
"examples/erc20_gas",
#"examples/custom_opcodes",
]
resolver = "2"
Expand Down
39 changes: 39 additions & 0 deletions examples/erc20_gas/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
[package]
name = "example-erc20-gas"
version = "0.0.0"
publish = false
authors.workspace = true
edition.workspace = true
keywords.workspace = true
license.workspace = true
repository.workspace = true
readme.workspace = true


[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]

[lints.rust]
unreachable_pub = "warn"
unused_must_use = "deny"
rust_2018_idioms = "deny"

[lints.rustdoc]
all = "warn"

[dependencies]
revm.workspace = true
database = { workspace = true, features = ["std", "alloydb"] }

# tokio
tokio = { version = "1.40", features = ["rt-multi-thread", "macros"] }

# alloy
alloy-sol-types = { version = "0.8.2", default-features = false, features = [
"std",
] }
alloy-transport-http = "0.6"
alloy-provider = "0.6"
reqwest = { version = "0.12" }
anyhow = "1.0.89"
34 changes: 34 additions & 0 deletions examples/erc20_gas/src/handlers/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
pub mod post_execution;
pub mod pre_execution;
pub mod validation;

pub use post_execution::Erc20PostExecution;
pub use pre_execution::Erc20PreExecution;
pub use validation::Erc20Validation;

use revm::{
context::{block::BlockEnv, tx::TxEnv, CfgEnv, Context},
context_interface::result::{EVMError, InvalidTransaction},
database_interface::Database,
handler::{EthExecution, EthHandler},
Evm,
};

pub type Erc20GasError<DB> = EVMError<<DB as Database>::Error, InvalidTransaction>;

pub type Erc20GasContext<DB> = Context<BlockEnv, TxEnv, CfgEnv, DB>;

pub type CustomHandler<
CTX,
ERROR,
VAL = Erc20Validation<CTX, ERROR>,
PREEXEC = Erc20PreExecution<CTX, ERROR>,
EXEC = EthExecution<CTX, ERROR>,
POSTEXEC = Erc20PostExecution<CTX, ERROR>,
> = EthHandler<CTX, ERROR, VAL, PREEXEC, EXEC, POSTEXEC>;

pub type CustomEvm<DB> = Evm<
Erc20GasError<DB>,
Erc20GasContext<DB>,
CustomHandler<Erc20GasContext<DB>, Erc20GasError<DB>>,
>;
109 changes: 109 additions & 0 deletions examples/erc20_gas/src/handlers/post_execution.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
use crate::{token_operation, TREASURY};
use revm::{
context::Cfg,
context_interface::{
result::{HaltReason, HaltReasonTrait, InvalidHeader, InvalidTransaction, ResultAndState},
Block, JournalDBError, Transaction, TransactionGetter,
},
handler::{EthPostExecution, EthPostExecutionContext, EthPostExecutionError, FrameResult},
handler_interface::PostExecutionHandler,
precompile::PrecompileErrors,
primitives::U256,
specification::hardfork::SpecId,
};

pub struct Erc20PostExecution<CTX, ERROR, HALTREASON = HaltReason> {
inner: EthPostExecution<CTX, ERROR, HALTREASON>,
}

impl<CTX, ERROR, HALTREASON> Erc20PostExecution<CTX, ERROR, HALTREASON> {
pub fn new() -> Self {
Self {
inner: EthPostExecution::new(),
}
}
}

impl<CTX, ERROR, HALTREASON> Default for Erc20PostExecution<CTX, ERROR, HALTREASON> {
fn default() -> Self {
Self::new()
}
}

impl<CTX, ERROR, HALTREASON> PostExecutionHandler for Erc20PostExecution<CTX, ERROR, HALTREASON>
where
CTX: EthPostExecutionContext<ERROR>,
ERROR: EthPostExecutionError<CTX>
+ From<InvalidTransaction>
+ From<InvalidHeader>
+ From<JournalDBError<CTX>>
+ From<PrecompileErrors>,
HALTREASON: HaltReasonTrait,
{
type Context = CTX;
type Error = ERROR;
type ExecResult = FrameResult;
type Output = ResultAndState<HALTREASON>;

fn refund(
&self,
context: &mut Self::Context,
exec_result: &mut Self::ExecResult,
eip7702_refund: i64,
) {
self.inner.refund(context, exec_result, eip7702_refund)
}

fn reimburse_caller(
&self,
context: &mut Self::Context,
exec_result: &mut Self::ExecResult,
) -> Result<(), Self::Error> {
let basefee = context.block().basefee() as u128;
let caller = context.tx().common_fields().caller();
let effective_gas_price = context.tx().effective_gas_price(basefee);
let gas = exec_result.gas();

let reimbursement =
effective_gas_price.saturating_mul((gas.remaining() + gas.refunded() as u64) as u128);
token_operation::<CTX, ERROR>(context, TREASURY, caller, U256::from(reimbursement))?;

Ok(())
}

fn reward_beneficiary(
&self,
context: &mut Self::Context,
exec_result: &mut Self::ExecResult,
) -> Result<(), Self::Error> {
let tx = context.tx();
let beneficiary = context.block().beneficiary();
let basefee = context.block().basefee() as u128;
let effective_gas_price = tx.effective_gas_price(basefee);
let gas = exec_result.gas();

let coinbase_gas_price = if context.cfg().spec().into().is_enabled_in(SpecId::LONDON) {
effective_gas_price.saturating_sub(basefee)
} else {
effective_gas_price
};

let reward =
coinbase_gas_price.saturating_mul((gas.spent() - gas.refunded() as u64) as u128);
token_operation::<CTX, ERROR>(context, TREASURY, beneficiary, U256::from(reward))?;

Ok(())
}

fn output(
&self,
context: &mut Self::Context,
result: Self::ExecResult,
) -> Result<Self::Output, Self::Error> {
self.inner.output(context, result)
}

fn clear(&self, context: &mut Self::Context) {
self.inner.clear(context)
}
}
65 changes: 65 additions & 0 deletions examples/erc20_gas/src/handlers/pre_execution.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
use crate::{token_operation, TREASURY};
use revm::{
context_interface::{
result::InvalidHeader, transaction::Eip4844Tx, Block, Transaction, TransactionGetter,
TransactionType,
},
handler::{EthPreExecution, EthPreExecutionContext, EthPreExecutionError},
handler_interface::PreExecutionHandler,
precompile::PrecompileErrors,
primitives::U256,
};

pub struct Erc20PreExecution<CTX, ERROR> {
inner: EthPreExecution<CTX, ERROR>,
}

impl<CTX, ERROR> Erc20PreExecution<CTX, ERROR> {
pub fn new() -> Self {
Self {
inner: EthPreExecution::new(),
}
}
}

impl<CTX, ERROR> Default for Erc20PreExecution<CTX, ERROR> {
fn default() -> Self {
Self::new()
}
}

impl<CTX, ERROR> PreExecutionHandler for Erc20PreExecution<CTX, ERROR>
where
CTX: EthPreExecutionContext,
ERROR: EthPreExecutionError<CTX> + From<InvalidHeader> + From<PrecompileErrors>,
{
type Context = CTX;
type Error = ERROR;

fn load_accounts(&self, context: &mut Self::Context) -> Result<(), Self::Error> {
self.inner.load_accounts(context)
}

fn apply_eip7702_auth_list(&self, context: &mut Self::Context) -> Result<u64, Self::Error> {
self.inner.apply_eip7702_auth_list(context)
}

fn deduct_caller(&self, context: &mut Self::Context) -> Result<(), Self::Error> {
let basefee = context.block().basefee() as u128;
let blob_price = context.block().blob_gasprice().unwrap_or_default();
let effective_gas_price = context.tx().effective_gas_price(basefee);

let mut gas_cost =
(context.tx().common_fields().gas_limit() as u128).saturating_mul(effective_gas_price);

if context.tx().tx_type().into() == TransactionType::Eip4844 {
let blob_gas = context.tx().eip4844().total_blob_gas() as u128;
gas_cost = gas_cost.saturating_add(blob_price.saturating_mul(blob_gas));
}

let caller = context.tx().common_fields().caller();
token_operation::<CTX, ERROR>(context, caller, TREASURY, U256::from(gas_cost))?;

Ok(())
}
}
104 changes: 104 additions & 0 deletions examples/erc20_gas/src/handlers/validation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
use crate::TOKEN;
use alloy_sol_types::SolValue;
use revm::{
context::Cfg,
context_interface::{
result::InvalidTransaction, transaction::Eip4844Tx, Journal, Transaction,
TransactionGetter, TransactionType,
},
handler::{EthValidation, EthValidationContext, EthValidationError},
handler_interface::ValidationHandler,
primitives::{keccak256, U256},
};
use std::cmp::Ordering;

pub struct Erc20Validation<CTX, ERROR> {
inner: EthValidation<CTX, ERROR>,
}

impl<CTX, ERROR> Erc20Validation<CTX, ERROR> {
pub fn new() -> Self {
Self {
inner: EthValidation::new(),
}
}
}

impl<CTX, ERROR> Default for Erc20Validation<CTX, ERROR> {
fn default() -> Self {
Self::new()
}
}

impl<CTX, ERROR> ValidationHandler for Erc20Validation<CTX, ERROR>
where
CTX: EthValidationContext,
ERROR: EthValidationError<CTX>,
{
type Context = CTX;
type Error = ERROR;

fn validate_env(&self, context: &Self::Context) -> Result<(), Self::Error> {
self.inner.validate_env(context)
}

fn validate_tx_against_state(&self, context: &mut Self::Context) -> Result<(), Self::Error> {
let caller = context.tx().common_fields().caller();
let caller_nonce = context.journal().load_account(caller)?.data.info.nonce;
let token_account = context.journal().load_account(TOKEN)?.data.clone();

if !context.cfg().is_nonce_check_disabled() {
let tx_nonce = context.tx().common_fields().nonce();
let state_nonce = caller_nonce;
match tx_nonce.cmp(&state_nonce) {
Ordering::Less => {
return Err(ERROR::from(InvalidTransaction::NonceTooLow {
tx: tx_nonce,
state: state_nonce,
}))
}
Ordering::Greater => {
return Err(ERROR::from(InvalidTransaction::NonceTooHigh {
tx: tx_nonce,
state: state_nonce,
}))
}
_ => (),
}
}

let mut balance_check = U256::from(context.tx().common_fields().gas_limit())
.checked_mul(U256::from(context.tx().max_fee()))
.and_then(|gas_cost| gas_cost.checked_add(context.tx().common_fields().value()))
.ok_or(InvalidTransaction::OverflowPaymentInTransaction)?;

if context.tx().tx_type().into() == TransactionType::Eip4844 {
let tx = context.tx().eip4844();
let data_fee = tx.calc_max_data_fee();
balance_check = balance_check
.checked_add(data_fee)
.ok_or(InvalidTransaction::OverflowPaymentInTransaction)?;
}

let account_balance_slot: U256 = keccak256((caller, U256::from(3)).abi_encode()).into();
let account_balance = token_account
.storage
.get(&account_balance_slot)
.expect("Balance slot not found")
.present_value();

if account_balance < balance_check && !context.cfg().is_balance_check_disabled() {
return Err(InvalidTransaction::LackOfFundForMaxFee {
fee: Box::new(balance_check),
balance: Box::new(account_balance),
}
.into());
};

Ok(())
}

fn validate_initial_tx_gas(&self, context: &Self::Context) -> Result<u64, Self::Error> {
self.inner.validate_initial_tx_gas(context)
}
}
Loading

0 comments on commit 60ce865

Please sign in to comment.