Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EIP1559 strict balance validation #857

Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 40 additions & 31 deletions frame/evm/src/runner/stack.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,27 +65,42 @@ where
) -> (ExitReason, R),
{
let (base_fee, weight) = T::FeeCalculator::min_gas_price();
let max_fee_per_gas = match (max_fee_per_gas, is_transactional) {
(Some(max_fee_per_gas), _) => max_fee_per_gas,
// Gas price check is skipped for non-transactional calls that don't
// define a `max_fee_per_gas` input.
(None, false) => Default::default(),
// Unreachable, previously validated. Handle gracefully.
_ => {
return Err(RunnerError {
error: Error::<T>::GasPriceTooLow,
weight,
})
}
};
let (total_fee_per_gas, actual_priority_fee_per_gas) =
match (max_fee_per_gas, max_priority_fee_per_gas, is_transactional) {
// With no tip, we pay exactly the base_fee
// TODO: should this fn explicitly check that base_fee <= max_fee_per_gas?
(Some(_), None, _) => (base_fee, U256::zero()),
// With tip, we include as much of the tip on top of base_fee that we can, never
// exceeding max_fee_per_gas
(Some(max_fee_per_gas), Some(max_priority_fee_per_gas), _) => {
let actual_priority_fee_per_gas = max_fee_per_gas
.saturating_sub(base_fee)
.min(max_priority_fee_per_gas);
(
base_fee.saturating_add(actual_priority_fee_per_gas),
actual_priority_fee_per_gas,
)
}
// Gas price check is skipped for non-transactional calls that don't
// define a `max_fee_per_gas` input.
(None, _, false) => (Default::default(), U256::zero()),
// Unreachable, previously validated. Handle gracefully.
_ => {
return Err(RunnerError {
error: Error::<T>::GasPriceTooLow,
weight,
})
}
};

// After eip-1559 we make sure the account can pay both the evm execution and priority fees.
let total_fee = max_fee_per_gas
.checked_mul(U256::from(gas_limit))
.ok_or(RunnerError {
error: Error::<T>::FeeOverflow,
weight,
})?;
let total_fee =
total_fee_per_gas
.checked_mul(U256::from(gas_limit))
.ok_or(RunnerError {
error: Error::<T>::FeeOverflow,
weight,
})?;

// Deduct fee from the `source` account. Returns `None` if `total_fee` is Zero.
let fee = T::OnChargeTransaction::withdraw_fee(&source, total_fee)
Expand All @@ -105,18 +120,12 @@ where

// Post execution.
let used_gas = U256::from(executor.used_gas());
let actual_fee = if let Some(max_priority_fee) = max_priority_fee_per_gas {
let actual_priority_fee = max_fee_per_gas
.saturating_sub(base_fee)
.min(max_priority_fee)
.saturating_mul(used_gas);
executor
.fee(base_fee)
.checked_add(actual_priority_fee)
.unwrap_or_else(U256::max_value)
} else {
executor.fee(base_fee)
};
// TODO: after simplifying the logic here, we no longer care about the specified priority
// fee except for calculating the total fee paid.
// When paying out the priority fee, we let correct_and_deposit_fee() come up with an
// arbitrary value that is used as the tip to pay.
// Assuming this is ok, actual_priority_fee_per_gas does not need to be returned above.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see three possibilities:

  1. do nothing (remove actual_priority_fee_per_gas and this comment)
  2. don't let correct_and_deposit_fee() return (or even know about...?) priority fee
  3. leave it as is but use actual_priority_fee_per_gas for sanity checks

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 or 3 would be appropriate for this PR, 2 would make sense as a follow-up.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd slightly prefer option 3. This is only executed once per transaction so the performance impact is minimum.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After another review, this was really always the case. Previously, we only calculated priority fee as an incremental step to get to total fee, and that's no longer necessary in this PR.

It's correct_and_deposit_fee() which is responsible for coming up with the tip to be paid.

We could add a sanity check that correct_and_deposit_fee() returns something equal to (...or less than?) the correct priority_fee, but that would require additional trait bounds for LiquidityInfo to at least compare it.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went with 1 for now. If 3 is worth the extra trait bounds in your opinion, I'm happy to do it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah that's okay. Please resolve the conflicts.

let actual_fee = executor.fee(total_fee_per_gas);
log::debug!(
target: "evm",
"Execution {:?} [source: {:?}, value: {}, gas_limit: {}, actual_fee: {}, is_transactional: {}]",
Expand Down
30 changes: 15 additions & 15 deletions primitives/evm/src/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -116,23 +116,18 @@ impl<'config, E: From<InvalidEvmTransactionError>> CheckEvmTransaction<'config,

pub fn with_balance_for(&self, who: &Account) -> Result<&Self, E> {
// Get fee data from either a legacy or typed transaction input.
let (_, effective_gas_price) = self.transaction_fee_input()?;
let (max_fee_per_gas, _) = self.transaction_fee_input()?;

// Account has enough funds to pay for the transaction.
// Check is skipped on non-transactional calls that don't provide
// a gas price input.
//
// Fee for EIP-1559 transaction **with** tip is calculated using
// the effective gas price.
//
// Fee for EIP-1559 transaction **without** tip is calculated using
// the base fee.
// Validation for EIP-1559 is done using the max_fee_per_gas, which is
// the most a txn could possibly pay.
//
// Fee for Legacy or EIP-2930 transaction is calculated using
// the provided `gas_price`.
let fee = effective_gas_price
.unwrap_or_default()
.saturating_mul(self.transaction.gas_limit);
let fee = max_fee_per_gas.saturating_mul(self.transaction.gas_limit);
if self.config.is_transactional || fee > U256::zero() {
let total_payment = self.transaction.value.saturating_add(fee);
if who.balance < total_payment {
Expand All @@ -142,6 +137,9 @@ impl<'config, E: From<InvalidEvmTransactionError>> CheckEvmTransaction<'config,
Ok(self)
}

// Returns the max_fee_per_gas (or gas_price for legacy txns) as well as an optional
// effective_gas_price for EIP-1559 transactions. effective_gas_price represents
// the total (fee + tip) that would be paid given the current base_fee.
fn transaction_fee_input(&self) -> Result<(U256, Option<U256>), E> {
match (
self.transaction.gas_price,
Expand Down Expand Up @@ -659,29 +657,31 @@ mod tests {
}

#[test]
// Account balance is matched against the base fee without tip.
fn validate_balance_using_base_fee() {
// Account balance is matched against max_fee_per_gas (without txn tip)
fn validate_balance_regardless_of_base_fee() {
let who = Account {
// sufficient for base_fee, but not for max_fee_per_gas
balance: U256::from(21_000_000_000_001u128),
nonce: U256::zero(),
};
let with_tip = false;
let test = transaction_max_fee_high(with_tip);
let res = test.with_balance_for(&who);
assert!(res.is_ok());
assert!(res.is_err());
}

#[test]
// Account balance is matched against the effective gas price with tip.
fn validate_balance_using_effective_gas_price() {
// Account balance is matched against max_fee_per_gas (with txn tip)
fn validate_balance_regardless_of_effective_gas_price() {
let who = Account {
// sufficient for (base_fee + tip), but not for max_fee_per_gas
balance: U256::from(42_000_000_000_001u128),
nonce: U256::zero(),
};
let with_tip = true;
let test = transaction_max_fee_high(with_tip);
let res = test.with_balance_for(&who);
assert!(res.is_ok());
assert!(res.is_err());
}

#[test]
Expand Down