--- 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 ```go 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 * The original EIP-1559 https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1559.md ## Copyright Copyright and related rights waived via CC0.