Skip to content

Latest commit

 

History

History
215 lines (165 loc) · 9.63 KB

File metadata and controls

215 lines (165 loc) · 9.63 KB
VIP 251
Title Dynamic Fee Market
Description This VIP proposes implementing a dynamic fee mechanism for VeChainThor transactions, inspired by Ethereum's EIP-1559.
Author Neil Brett (@nwbrettski), Paolo Galli (@paologalligit), Pedro Gomes (@otherview), Tony Li (@libotony)
Discussions https://vechain.discourse.group/t/add-vip-dynamic-fee-market/311
Category Core
Status Draft
CreatedAt 2025-02-17

Overview

This VIP proposes implementing a dynamic fee mechanism for VeChainThor transactions, inspired by Ethereum's EIP-1559. It introduces a fluctuating base fee that is burned and a user-set priority fee that is paid directly to the validator proposing the block.

The goal is to enhance network security and user experience by transitioning from a fixed-fee model to a dynamic fee model, adjusting fees based on network congestion.

Motivation

The current fixed-fee model presents challenges in securing the network and managing congestion. By adopting a dynamic fee model, the VIP aims to:

  • Enhance Network Security: Dynamic fee adjustments mitigate the risk of denial-of-service attacks by increasing costs during high demand.
  • Optimize Network Congestion: A self-adjusting base fee better reflects network demand, leading to efficient resource allocation.
  • Increase Value Accrual: Burning the base fee reduces the circulating supply of VTHO, enhancing its value.

Rationale

Base Fee Burning

  • VTHO Utility: Reinforces VTHO as the sole transaction fee token on VeChainThor.
  • Inflation Counterbalance: Helps mitigate inflation by removing tokens from circulation.
  • Validator Neutrality: Discourages validators from manipulating the base fee for personal gain.
  • Market Integrity: Prevents off-chain markets from undermining the fee mechanism.

Specification

Transaction Fee Model

The dynamic fee model consists of two components:

  • Base Fee: A network-wide fee determined dynamically per block based on network utilization. It is burned, effectively removing VTHO from circulation. An InitialBaseFee is set to 10,000 gwei (0.00001 VTHO), which also serves as the minimum base fee.
  • Priority Fee: An optional incentive paid directly to the validator for transaction inclusion. Users specify a maximum priority fee per gas unit.

Users specify the total maximum fee (maxFeePerGas) they are willing to pay. This value must cover both the base fee (baseFeePerGas) and the maximum priority fee (maxPriorityFeePerGas), with any unused portion returned to the user.

Base Fee Adjustment

The base fee is adjusted dynamically based on the gas used in the previous block relative to a target gas limit:

  • Gas Used > Gas Target: Base fee increases.
  • Gas Used < Gas Target: Base fee decreases.
  • Gas Used = Gas Target: Base fee remains unchanged.

The magnitude of adjustment is determined by a predefined base fee change denominator.

Block Header Changes

  • Add baseFeePerGas to the block header.

Transaction Model Changes

  • Add maxFeePerGas and maxPriorityFeePerGas to the transaction model.

New Parameters

  • Elasticity Multiplier: On VeChainThor, set to 75% to achieve a gas target per block of 30 million gas units.
  • Base Fee Change Denominator: Set to 8, allowing a maximum increase of ~4.17% on a full block and a maximum decrease of 12.50% on an empty block.

Test Cases

New Fields:

  • Verify invalid scenarios, such as maxPriorityFeePerGas being greater than maxFeePerGas.

Fee Calculations:

  • Test the base fee calculation before, during, and after fork activation.
  • Validate the fee cap constraint: effectivePriorityPrice = min(maxPriorityFeePerGas, maxFeePerGas - block.baseFee).

Refund and Rewards:

  • Ensure that the baseFeePerGas is not refunded while the effectivePriorityPrice is correctly applied.
  • Verify that the validator's reward is calculated solely from the priority fee.

Backwards Compatibility:

  • Confirm that legacy transactions are interpreted as dynamic fee transactions by setting:

    • maxFeePerGas equal to gasPrice
    • maxPriorityFeePerGas equal to gasPrice

    This approach may lead to overpayments for legacy transactions, as users will not receive refunds for the difference between the maximum fee and the gas price.

Consensus:

  • Validate the block header before, during, and after fork activation to ensure compliance.

Reference Implementation

func ValidateBlock(block *Block) error {
    parentBlock := GetParent(block)
    parentGasTarget := parentBlock.GasLimit / ElasticityMultiplier
    parentGasLimit := parentBlock.GasLimit

    parentBaseFeePerGas := parentBlock.BaseFeePerGas
    parentGasUsed := parentBlock.GasUsed

    // check if the block used too much gas
    if block.GasUsed > block.GasLimit {
        return fmt.Errorf("invalid block: too much gas used")
    }

    // check if the gas limit is at least the minimum gas limit
    if block.GasLimit < 1_000_000 {
        return fmt.Errorf("invalid block: gas limit too low")
    }

    // check if the block changed the gas limit too much
    if block.GasLimit >= parentGasLimit + parentGasLimit / 1024 {
        return fmt.Errorf("invalid block: gas limit increased too much")
    }
    if block.GasLimit <= parentGasLimit - parentGasLimit / 1024 {
        return fmt.Errorf("invalid block: gas limit decreased too much")
    }


    // check if the base fee is correct
    var expectedBaseFeePerGas int64
    if InitialForkBlockNumber == block.Number {
        expectedBaseFeePerGas = InitialBaseFee
    } else if parentGasUsed == parentGasTarget {
        expectedBaseFeePerGas = parentBaseFeePerGas
    } else if parentGasUsed > parentGasTarget {
        gasUsedDelta := parentGasUsed - parentGasTarget
        baseFeePerGasDelta := max(parentBaseFeePerGas * gasUsedDelta / parentGasTarget / BaseFeeMaxChangeDenominator, 1)
        expectedBaseFeePerGas = parentBaseFeePerGas + baseFeePerGasDelta
    } else {
        gasUsedDelta := parentGasTarget - parentGasUsed
        baseFeePerGasDelta := parentBaseFeePerGas * gasUsedDelta / parentGasTarget / BaseFeeMaxChangeDenominator
        expectedBaseFeePerGas = max(parentBaseFeePerGas - baseFeePerGasDelta, InitialBaseFee)
    }

    if expectedBaseFeePerGas != block.BaseFeePerGas {
        return fmt.Errorf("invalid block: base fee not correct")
    }

    // execute transactions and do gas accounting
    cumulativeTransactionGasUsed := int64(0)
    for _, transaction := range transactions {
        // Validate transaction
        if err := ValidateTransaction(transaction); err != nil {
            return err
        }

        signer := state.GetEnergy(transaction.From)

        signer.Energy -= transaction.Amount
        if signer.Balance < 0 {
            return fmt.Errorf("invalid transaction: signer does not have enough ETH to cover attached value")
        }

        // the signer must be able to afford the transaction
        if signer.Balance < transaction.GasLimit * transaction.MaxFeePerGas {
            return fmt.Errorf("invalid transaction: signer cannot afford transaction")
        }

        // ensure that the user was willing to at least pay the base fee
        if transaction.MaxFeePerGas < block.BaseFeePerGas {
            return fmt.Errorf("invalid transaction: max fee lower than base fee")
        }

        // Prevent impossibly large numbers
        maxBig := new(big.Int).Exp(big.NewInt(2), big.NewInt(256), nil)
        if big.NewInt(transaction.MaxFeePerGas).Cmp(maxBig) >= 0 {
            return fmt.Errorf("invalid transaction: max fee per gas too large")
        }
        if big.NewInt(transaction.MaxPriorityFeePerGas).Cmp(maxBig) >= 0 {
            return fmt.Errorf("invalid transaction: max priority fee per gas too large")
        }

        // The total must be the larger of the two
        if transaction.MaxFeePerGas < transaction.MaxPriorityFeePerGas {
            return fmt.Errorf("invalid transaction: max fee lower than priority fee")
        }

        // priority fee is capped because the base fee is filled first
        priorityFeePerGas := min(transaction.MaxPriorityFeePerGas, transaction.MaxFeePerGas - block.BaseFeePerGas)
        
        // signer pays both the priority fee and the base fee
        effectiveGasPrice := priorityFeePerGas + block.BaseFeePerGas
        signer.Balance -= transaction.GasLimit * effectiveGasPrice
        
        if signer.Balance < 0 {
            return fmt.Errorf("invalid transaction: signer does not have enough ETH to cover gas")
        }

        gasUsed := state.ExecuteTransaction(transaction, effectiveGasPrice)
        gasRefund := transaction.GasLimit - gasUsed
        cumulativeTransactionGasUsed += gasUsed
        
        // signer gets refunded for unused gas
        signer.Balance += gasRefund * effectiveGasPrice
        
        // validator only receives the priority fee; note that the base fee is not given to anyone (it is burned)
        validatorAccount := state.GetEnergy(block.Author)
        validatorAccount.Balance += gasUsed * priorityFeePerGas
    }

    // check if the block spent too much gas transactions
    if cumulativeTransactionGasUsed != block.GasUsed {
        return fmt.Errorf("invalid block: gas_used does not equal total gas used in all transactions")
    }

    // validate the rest of the block...

    return nil
}

Security Considerations

  • Economic Attacks: Analyze potential attack vectors related to base fee manipulation and priority fee bidding wars.
  • Consensus Stability: Assess the impact of the dynamic fee model on the stability of the VeChainThor consensus mechanism.

Sources

Copyright

Copyright and related rights waived via CC0.