example_erc20_gas/
handler.rs

1use revm::{
2    context::Cfg,
3    context_interface::{
4        result::{HaltReason, InvalidTransaction},
5        Block, ContextTr, JournalTr, Transaction,
6    },
7    handler::{
8        pre_execution::validate_account_nonce_and_code, EvmTr, EvmTrError, FrameResult, FrameTr,
9        Handler,
10    },
11    interpreter::interpreter_action::FrameInit,
12    primitives::{hardfork::SpecId, U256},
13    state::EvmState,
14};
15
16use crate::{erc_address_storage, token_operation, TOKEN, TREASURY};
17
18/// Custom handler that implements ERC20 token gas payment.
19/// Instead of paying gas in ETH, transactions pay gas using ERC20 tokens.
20/// The tokens are transferred from the transaction sender to a treasury address.
21#[derive(Debug)]
22pub struct Erc20MainnetHandler<EVM, ERROR, FRAME> {
23    _phantom: core::marker::PhantomData<(EVM, ERROR, FRAME)>,
24}
25
26impl<CTX, ERROR, FRAME> Erc20MainnetHandler<CTX, ERROR, FRAME> {
27    /// Creates a new ERC20 gas payment handler
28    pub fn new() -> Self {
29        Self {
30            _phantom: core::marker::PhantomData,
31        }
32    }
33}
34
35impl<EVM, ERROR, FRAME> Default for Erc20MainnetHandler<EVM, ERROR, FRAME> {
36    fn default() -> Self {
37        Self::new()
38    }
39}
40
41impl<EVM, ERROR, FRAME> Handler for Erc20MainnetHandler<EVM, ERROR, FRAME>
42where
43    EVM: EvmTr<Context: ContextTr<Journal: JournalTr<State = EvmState>>, Frame = FRAME>,
44    FRAME: FrameTr<FrameResult = FrameResult, FrameInit = FrameInit>,
45    ERROR: EvmTrError<EVM>,
46{
47    type Evm = EVM;
48    type Error = ERROR;
49    type HaltReason = HaltReason;
50
51    fn validate_against_state_and_deduct_caller(&self, evm: &mut Self::Evm) -> Result<(), ERROR> {
52        let context = evm.ctx();
53        let basefee = context.block().basefee() as u128;
54        let blob_price = context.block().blob_gasprice().unwrap_or_default();
55        let is_balance_check_disabled = context.cfg().is_balance_check_disabled();
56        let is_eip3607_disabled = context.cfg().is_eip3607_disabled();
57        let is_nonce_check_disabled = context.cfg().is_nonce_check_disabled();
58        let caller = context.tx().caller();
59        let value = context.tx().value();
60
61        let (tx, journal) = context.tx_journal_mut();
62
63        // Load caller's account.
64        let caller_account = journal.load_account_code(tx.caller())?.data;
65
66        validate_account_nonce_and_code(
67            &mut caller_account.info,
68            tx.nonce(),
69            is_eip3607_disabled,
70            is_nonce_check_disabled,
71        )?;
72
73        if tx.kind().is_call() {
74            caller_account.info.nonce = caller_account.info.nonce.saturating_add(1);
75        }
76
77        // Touch account so we know it is changed.
78        caller_account.mark_touch();
79
80        let max_balance_spending = tx.max_balance_spending()?;
81        let effective_balance_spending = tx
82            .effective_balance_spending(basefee, blob_price)
83            .expect("effective balance is always smaller than max balance so it can't overflow");
84
85        let account_balance_slot = erc_address_storage(tx.caller());
86        let account_balance = context
87            .journal_mut()
88            .sload(TOKEN, account_balance_slot)
89            .map(|v| v.data)
90            .unwrap_or_default();
91
92        if account_balance < max_balance_spending && !is_balance_check_disabled {
93            return Err(InvalidTransaction::LackOfFundForMaxFee {
94                fee: Box::new(max_balance_spending),
95                balance: Box::new(account_balance),
96            }
97            .into());
98        };
99
100        // Check if account has enough balance for `gas_limit * max_fee`` and value transfer.
101        // Transfer will be done inside `*_inner` functions.
102        if is_balance_check_disabled {
103            // ignore balance check.
104            // TODO add transfer value to the erc20 slot.
105        } else if max_balance_spending > account_balance {
106            return Err(InvalidTransaction::LackOfFundForMaxFee {
107                fee: Box::new(max_balance_spending),
108                balance: Box::new(account_balance),
109            }
110            .into());
111        } else {
112            // subtracting max balance spending with value that is going to be deducted later in the call.
113            let gas_balance_spending = effective_balance_spending - value;
114
115            token_operation::<EVM::Context, ERROR>(
116                context,
117                caller,
118                TREASURY,
119                gas_balance_spending,
120            )?;
121        }
122
123        Ok(())
124    }
125
126    fn reimburse_caller(
127        &self,
128        evm: &mut Self::Evm,
129        exec_result: &mut <<Self::Evm as EvmTr>::Frame as FrameTr>::FrameResult,
130    ) -> Result<(), Self::Error> {
131        let context = evm.ctx();
132        let basefee = context.block().basefee() as u128;
133        let caller = context.tx().caller();
134        let effective_gas_price = context.tx().effective_gas_price(basefee);
135        let gas = exec_result.gas();
136
137        let reimbursement =
138            effective_gas_price.saturating_mul((gas.remaining() + gas.refunded() as u64) as u128);
139        token_operation::<EVM::Context, ERROR>(
140            context,
141            TREASURY,
142            caller,
143            U256::from(reimbursement),
144        )?;
145
146        Ok(())
147    }
148
149    fn reward_beneficiary(
150        &self,
151        evm: &mut Self::Evm,
152        exec_result: &mut <<Self::Evm as EvmTr>::Frame as FrameTr>::FrameResult,
153    ) -> Result<(), Self::Error> {
154        let context = evm.ctx();
155        let tx = context.tx();
156        let beneficiary = context.block().beneficiary();
157        let basefee = context.block().basefee() as u128;
158        let effective_gas_price = tx.effective_gas_price(basefee);
159        let gas = exec_result.gas();
160
161        let coinbase_gas_price = if context.cfg().spec().into().is_enabled_in(SpecId::LONDON) {
162            effective_gas_price.saturating_sub(basefee)
163        } else {
164            effective_gas_price
165        };
166
167        let reward =
168            coinbase_gas_price.saturating_mul((gas.spent() - gas.refunded() as u64) as u128);
169        token_operation::<EVM::Context, ERROR>(context, TREASURY, beneficiary, U256::from(reward))?;
170
171        Ok(())
172    }
173}