Gas Mechanisms and Fee Markets

Understanding gas calculation, fee markets (EIP-1559), and priority fee mechanisms in blockchain.

Advanced⏱️ 50 min📚 Prerequisites: 2

Gas Mechanisms and Fee Markets

Gas is the unit that measures computational work in blockchain transactions.

Why Gas?

  • Prevent Spam: Costly to execute many transactions
  • Resource Limits: Prevent infinite loops
  • Fair Pricing: Pay for actual computation
  • Network Stability: Regulate transaction throughput

Gas Calculation

Gas Costs

Different operations cost different amounts:

  • Simple Transfer: 21,000 gas
  • Contract Creation: 32,000 + code size
  • Storage Write: 20,000 gas (first time), 5,000 (update)
  • Storage Read: 2,100 gas
  • SHA3: 30 gas + 6 per word

Gas Price

Gas Price is how much you pay per unit of gas (in wei/gas).

RUST
struct Transaction {
    gas_limit: u64,
    gas_price: u64, // wei per gas
    // ...
}

fn calculate_fee(tx: &Transaction) -> u64 {
    tx.gas_limit * tx.gas_price
}

EIP-1559 Fee Market

EIP-1559 introduced a new fee structure:

Components

  1. Base Fee: Burned, adjusts per block
  2. Priority Fee: Goes to miner/validator
  3. Max Fee: Maximum user willing to pay

Implementation

RUST
struct EIP1559Transaction {
    max_fee_per_gas: u64,
    max_priority_fee_per_gas: u64,
    gas_limit: u64,
}

struct Block {
    base_fee: u64,
    transactions: Vec<EIP1559Transaction>,
}

fn calculate_fee(tx: &EIP1559Transaction, base_fee: u64) -> (u64, u64) {
    // Effective gas price = min(max_fee, base_fee + priority_fee)
    let effective_gas_price = tx.max_fee_per_gas.min(base_fee + tx.max_priority_fee_per_gas);
    
    // Total fee = effective_gas_price * gas_limit
    let total_fee = effective_gas_price * tx.gas_limit;
    
    // Base fee portion (burned)
    let base_fee_portion = base_fee.min(effective_gas_price) * tx.gas_limit;
    
    // Priority fee portion (to miner)
    let priority_fee_portion = total_fee - base_fee_portion;
    
    (base_fee_portion, priority_fee_portion)
}

fn adjust_base_fee(previous_base_fee: u64, block_fullness: f64) -> u64 {
    // Target: 50% block fullness
    let target = 0.5;
    
    if block_fullness > target {
        // Increase base fee
        previous_base_fee + (previous_base_fee / 8)
    } else {
        // Decrease base fee
        previous_base_fee - (previous_base_fee / 8)
    }
}

Benefits of EIP-1559

  • Predictable Fees: Base fee adjusts smoothly
  • Fee Burning: Base fee is burned (deflationary)
  • Better UX: Users set max fee, not exact fee
  • MEV Reduction: Less front-running incentive

Gas Optimization

Best Practices

  • Cache Storage: Read once, use multiple times
  • Pack Data: Use smaller data types
  • Batch Operations: Combine multiple operations
  • Avoid Loops: Unroll when possible
  • Use Events: Cheaper than storage

Example

RUST
// Bad: Multiple storage writes
fn bad_function(&mut self) {
    self.value1 = 1;
    self.value2 = 2;
    self.value3 = 3;
}

// Good: Pack into struct
struct PackedValues {
    value1: u8,
    value2: u8,
    value3: u8,
}

fn good_function(&mut self) {
    self.packed = PackedValues { value1: 1, value2: 2, value3: 3 };
    // Single storage write instead of three
}

Real-World Examples

  • Ethereum: Uses gas for all operations
  • EIP-1559: Implemented in Ethereum London upgrade
  • Polygon: Lower gas costs, similar mechanism
  • Arbitrum: L2 with lower gas costs

Fee Estimation

RUST
fn estimate_gas(transaction: &Transaction, state: &State) -> u64 {
    let mut gas_used = 21_000; // Base cost
    
    // Add costs based on transaction type
    if transaction.is_contract_creation() {
        gas_used += 32_000;
        gas_used += transaction.data.len() as u64 * 200; // Code size
    }
    
    // Simulate execution to get accurate estimate
    // ...
    
    gas_used
}

Code Examples

Gas Calculation

Calculate transaction fees

RUST
struct Transaction {
    gas_limit: u64,
    gas_price: u64, // wei per gas
    to: String,
    value: u64,
    data: Vec<u8>,
}

impl Transaction {
    fn calculate_fee(&self) -> u64 {
        self.gas_limit * self.gas_price
    }
    
    fn estimate_gas(&self) -> u64 {
        let mut gas = 21_000; // Base transaction cost
        
        // Contract creation costs more
        if self.to.is_empty() {
            gas += 32_000;
            gas += self.data.len() as u64 * 200; // Code deployment
        }
        
        // Data costs gas
        if !self.data.is_empty() {
            gas += self.data.len() as u64 * 16; // Data cost
        }
        
        gas
    }
}

fn main() {
    // Simple transfer
    let simple_tx = Transaction {
        gas_limit: 21_000,
        gas_price: 20_000_000_000, // 20 gwei
        to: String::from("0xBob"),
        value: 1_000_000_000_000_000_000, // 1 ETH
        data: vec![],
    };
    
    let fee = simple_tx.calculate_fee();
    println!("Transaction fee: {} wei", fee);
    println!("Transaction fee: {} ETH", fee as f64 / 1e18);
    
    // Contract creation
    let contract_tx = Transaction {
        gas_limit: 100_000,
        gas_price: 20_000_000_000,
        to: String::new(), // Empty = contract creation
        value: 0,
        data: vec![0; 1000], // Contract code
    };
    
    let estimated = contract_tx.estimate_gas();
    println!("Estimated gas: {}", estimated);
}

Explanation:

Gas calculation determines transaction fees. Simple transfers cost 21,000 gas. Contract creation costs more. The total fee is gas_limit * gas_price. This ensures users pay for the computational resources they use.

EIP-1559 Fee Structure

EIP-1559 fee calculation

RUST
struct EIP1559Tx {
    max_fee_per_gas: u64,
    max_priority_fee_per_gas: u64,
    gas_limit: u64,
}

struct Block {
    base_fee: u64,
    number: u64,
}

fn calculate_fees(tx: &EIP1559Tx, block: &Block) -> (u64, u64) {
    // Effective gas price = min(max_fee, base_fee + priority_fee)
    let effective_gas_price = tx.max_fee_per_gas.min(
        block.base_fee + tx.max_priority_fee_per_gas
    );
    
    // Total fee paid
    let total_fee = effective_gas_price * tx.gas_limit;
    
    // Base fee portion (burned)
    let base_fee_portion = block.base_fee.min(effective_gas_price) * tx.gas_limit;
    
    // Priority fee portion (to miner/validator)
    let priority_fee_portion = total_fee - base_fee_portion;
    
    (base_fee_portion, priority_fee_portion)
}

fn adjust_base_fee(previous_base_fee: u64, block_fullness: f64) -> u64 {
    // Target: 50% block fullness
    let target = 0.5;
    
    if block_fullness > target {
        // Increase by 12.5%
        previous_base_fee + (previous_base_fee / 8)
    } else if block_fullness < target {
        // Decrease by 12.5%
        previous_base_fee.saturating_sub(previous_base_fee / 8)
    } else {
        previous_base_fee
    }
}

fn main() {
    let mut block = Block {
        base_fee: 20_000_000_000, // 20 gwei
        number: 1,
    };
    
    let tx = EIP1559Tx {
        max_fee_per_gas: 50_000_000_000, // 50 gwei max
        max_priority_fee_per_gas: 2_000_000_000, // 2 gwei priority
        gas_limit: 21_000,
    };
    
    let (base_fee_burned, priority_fee) = calculate_fees(&tx, &block);
    
    println!("Base fee burned: {} wei", base_fee_burned);
    println!("Priority fee to miner: {} wei", priority_fee);
    println!("Total fee: {} wei", base_fee_burned + priority_fee);
    
    // Adjust base fee for next block
    let new_base_fee = adjust_base_fee(block.base_fee, 0.75); // 75% full
    println!("New base fee: {} wei", new_base_fee);
}

Explanation:

EIP-1559 splits fees into base fee (burned, adjusts per block) and priority fee (to miner). The base fee adjusts based on block fullness, making fees more predictable. Users set max_fee and max_priority_fee, and pay the minimum needed.

Exercises

Calculate Transaction Fee

Calculate the fee for a transaction!

Easy

Starter Code:

RUST
struct Transaction {
    gas_limit: u64,
    gas_price: u64,
}

fn main() {
    let tx = Transaction {
        gas_limit: 21_000,
        gas_price: 20_000_000_000,
    };
    
    // Calculate fee
}