-
Notifications
You must be signed in to change notification settings - Fork 602
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(example): use custom handlers to pay for gas with erc-20 (#1901)
* 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
1 parent
543b4bb
commit 60ce865
Showing
8 changed files
with
579 additions
and
0 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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>>, | ||
>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(()) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
} |
Oops, something went wrong.