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

Priority fees on EVMs (EIP-1559) #16342

Draft
wants to merge 3 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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
60 changes: 60 additions & 0 deletions docs/packages/suite/priority fees eip1559/eip1559.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# EIP-1559 - priority fees

More info in [notion](https://www.notion.so/satoshilabs/EIP-1559-Priority-fees-177dc5260606801fbc82e05e0db9cb3e)

- ✅ Ethereum Mainnet + Holesky + Sepolia
- ✅ Arbuirum
- ✅ Base
- ⚠ (Supported by infura, not yet impmemented in suite) BNB mainnet (Base fee is 0)
- ✅ Optimism
- ✅ Polygon mainnet
- ❌ Ethereum Classic (not supported)

# Blockbook

It is possible to send EIP-1559 transaction, although when we receive transaction data, eip1559 information is not present.

This has to be fixed on blockbook side. (Martin Böhm should be working on it right now)

This is a blocker for sending RBF and correctly displaying transaction fees data in transaction details.

# Passing eip1559 transaction to firmware

### In Firmware maximum fee is calculated:

https://github.com/trezor/trezor-firmware/blob/13c078f8af75135ac757423da3fc0c013186c32c/core/src/apps/ethereum/sign_tx_eip1559.py#L65

`maximum_fee = format_ethereum_amount(max_gas_fee * gas_limit, None, defs.network)`

Since we can’t provide baseFeePerGas to firmware, we should calculate **_effective gas price_** on the suite side and pass the selected price to maximum_fee. Priority fee is being passed just for the purpose of displaying it, in fact it's being already included into effective gas price.

### Misc

I have some thought processes documented [here](https://excalidraw.com/#json=0l28PPN7iSp-ekzch1igV,iQnn7yW7gpQ-6mWcK1nUAw), although it's not complete, it can help you to understand how I reasoned about the forms. Which is the most challenging part about implementing this feature.

While testing, I recommend having a list of each part of the app where you have the fees component and after you test it document the send transaction.

### Staking

#### Stake

#### Unstake/Claim

### Send

### Swap

#### Approval tx for tokens on DEX

#### The swap tx on DEX

#### CEX

### Sell

Each of the above scenarios has to be tested with:

- legacy fees with backend disconnected/returning only normal fee level (standard and custom)
- legacy fees when the network feature is turned off
- priority fees with a standard fee level
- priority fees with custom fee level
26 changes: 4 additions & 22 deletions packages/connect/src/api/ethereum/EthereumFees.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { BigNumber } from '@trezor/utils/src/bigNumber';

import { Blockchain } from '../../backend/BlockchainLink';
import type { EthereumNetworkInfo, FeeLevel } from '../../types';
import { Blocks, MiscFeeLevels, findBlocksForFee } from '../common/MiscFees';
import { Blocks, MiscFeeLevels } from '../common/MiscFees';

type EipResponse1559Level = 'low' | 'medium' | 'high';
type Eip1559Level = 'low' | 'normal' | 'high';
Expand Down Expand Up @@ -32,7 +32,7 @@ export class EthereumFeeLevels extends MiscFeeLevels {
const level = eip1559[levelKey];

// We can't pass BaseFeePerGas to firmware, so we calculate the effective gas price here
const calculatedMaxFeePerGas = BigNumber.minimum(
const calculatedMaxBaseFeePerGas = BigNumber.minimum(
new BigNumber(level?.maxFeePerGas || '0'),
new BigNumber(eip1559.baseFeePerGas || '0').plus(
level?.maxPriorityFeePerGas || '0',
Expand All @@ -46,8 +46,8 @@ export class EthereumFeeLevels extends MiscFeeLevels {

return {
label,
maxFeePerGas: level?.maxFeePerGas || '0',
effectiveGasPrice: calculatedMaxFeePerGas,
maxFeePerGas: level?.maxFeePerGas,
effectiveGasPrice: calculatedMaxBaseFeePerGas,
maxPriorityFeePerGas: level?.maxPriorityFeePerGas || '0',
baseFeePerGas: eip1559.baseFeePerGas,
minWaitTimeEstimate: level?.minWaitTimeEstimate
Expand All @@ -72,22 +72,4 @@ export class EthereumFeeLevels extends MiscFeeLevels {

return this.levels;
}

updateEthereumCustomFee(
feePerUnit: string,
effectiveGasPrice?: string,
maxPriorityFeePerGas?: string,
) {
// remove "custom" level from list
this.levels = this.levels.filter(l => l.label !== 'custom');
// recreate "custom" level
const blocks = findBlocksForFee(feePerUnit, this.blocks);
this.levels.push({
label: 'custom',
feePerUnit,
blocks,
maxPriorityFeePerGas,
effectiveGasPrice,
});
}
}
10 changes: 2 additions & 8 deletions packages/connect/src/types/fees.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,6 @@ export const FeeInfo = Type.Object({
dustLimit: Type.Number(),
});

export type PriorityFeeEstimationDetails = Static<typeof PriorityFeeEstimationDetails>;
export const PriorityFeeEstimationDetails = Type.Object({
maxFeePerGas: Type.String(),
maxPriorityFeePerGas: Type.String(),
maxWaitTimeEstimate: Type.Optional(Type.Number()),
minWaitTimeEstimate: Type.Optional(Type.Number()),
});

export type FeeLevel = Static<typeof FeeLevel>;
export const FeeLevel = Type.Object({
label: Type.Union([
Expand All @@ -32,6 +24,8 @@ export const FeeLevel = Type.Object({
baseFeePerGas: Type.Optional(Type.String()),
maxFeePerGas: Type.Optional(Type.String()),
effectiveGasPrice: Type.Optional(Type.String()),
customMaxBaseFeePerGas: Type.Optional(Type.String()),
customMaxPriorityFeePerGas: Type.Optional(Type.String()),
maxPriorityFeePerGas: Type.Optional(Type.String()),
maxWaitTimeEstimate: Type.Optional(Type.Number()),
minWaitTimeEstimate: Type.Optional(Type.Number()),
Expand Down
2 changes: 2 additions & 0 deletions packages/suite/src/actions/wallet/stake/stakeFormActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ export const calculate = (
max,
fee: feeInBaseUnits,
feePerByte: feeLevel.feePerUnit,
maxFeePerGas: feeLevel.effectiveGasPrice || undefined,
maxPriorityFeePerGas: feeLevel.maxPriorityFeePerGas || undefined,
feeLimit: feeLevel.feeLimit,
bytes: 0,
inputs: [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ import {
UNSTAKE_INTERCHANGES,
} from '@suite-common/wallet-constants';
import { ComposeActionContext, selectSelectedDevice } from '@suite-common/wallet-core';
import { calculateEffectiveGasPrice } from '@suite-common/wallet-core/src/send/sendFormEthereumUtils';
import {
AddressDisplayOptions,
ExternalOutput,
PrecomposedTransaction,
PrecomposedTransactionFinal,
StakeFormState,
} from '@suite-common/wallet-types';
import { calculateEthFee, getAccountIdentity, isPending } from '@suite-common/wallet-utils';
import { calculateMaxEthFee, getAccountIdentity, isPending } from '@suite-common/wallet-utils';
import TrezorConnect, { FeeLevel } from '@trezor/connect';
import { BigNumber } from '@trezor/utils/src/bigNumber';

Expand All @@ -31,14 +32,18 @@ import {

import { calculate, composeStakingTransaction } from './stakeFormActions';

const calculateTransaction = (
const calculateStakingTransaction = (
availableBalance: string,
output: ExternalOutput,
feeLevel: FeeLevel,
compareWithAmount = true,
symbol: NetworkSymbol,
): PrecomposedTransaction => {
const feeInWei = calculateEthFee(toWei(feeLevel.feePerUnit, 'gwei'), feeLevel.feeLimit || '0');
const isEip1559 = feeLevel.maxPriorityFeePerGas !== undefined;

const feeInWei = isEip1559
? calculateMaxEthFee(feeLevel.effectiveGasPrice, feeLevel.feeLimit)
: calculateMaxEthFee(toWei(feeLevel.feePerUnit, 'gwei'), feeLevel.feeLimit);

const stakingParams = {
feeInBaseUnits: feeInWei,
Expand Down Expand Up @@ -82,10 +87,18 @@ export const composeTransaction =
}
// in case when selectedFee is set to 'custom' construct this FeeLevel from values
if (formValues.selectedFee === 'custom') {
const calculatedEffectiveGasPrice = calculateEffectiveGasPrice({
maxFeePerGasGwei: formValues.customMaxBaseFeePerGas || '0',
maxPriorityFeePerGasGwei: formValues.customMaxPriorityFeePerGas,
});
predefinedLevels.push({
label: 'custom',
feePerUnit: formValues.feePerUnit,
feeLimit: formValues.feeLimit,
customMaxBaseFeePerGas: formValues.customMaxBaseFeePerGas,
customMaxPriorityFeePerGas: formValues.customMaxPriorityFeePerGas || '0',
effectiveGasPrice: calculatedEffectiveGasPrice,
maxPriorityFeePerGas: toWei(Number(formValues.customMaxPriorityFeePerGas), 'gwei'),
blocks: -1,
});
}
Expand All @@ -94,7 +107,7 @@ export const composeTransaction =
formValues,
formState,
predefinedLevels,
calculateTransaction,
calculateStakingTransaction,
undefined,
customFeeLimit,
);
Expand Down Expand Up @@ -150,6 +163,8 @@ export const signTransaction =
identity,
amount: formValues.outputs[0].amount,
gasPrice: transactionInfo.feePerByte,
maxFeePerGas: transactionInfo.maxFeePerGas,
maxPriorityFeePerGas: transactionInfo.maxPriorityFeePerGas,
nonce,
chainId: network.chainId,
});
Expand All @@ -161,6 +176,8 @@ export const signTransaction =
identity,
amount: formValues.outputs[0].amount,
gasPrice: transactionInfo.feePerByte,
maxFeePerGas: transactionInfo.maxFeePerGas,
maxPriorityFeePerGas: transactionInfo.maxPriorityFeePerGas,
nonce,
chainId: network.chainId,
interchanges: UNSTAKE_INTERCHANGES,
Expand All @@ -172,6 +189,8 @@ export const signTransaction =
from: account.descriptor,
identity,
gasPrice: transactionInfo.feePerByte,
maxFeePerGas: transactionInfo.maxFeePerGas,
maxPriorityFeePerGas: transactionInfo.maxPriorityFeePerGas,
nonce,
chainId: network.chainId,
});
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { formatDurationStrict } from '@suite-common/suite-utils';
import { NetworkType, networks } from '@suite-common/wallet-config';
import { NetworkType, getNetworkFeatures, networks } from '@suite-common/wallet-config';
import { FeeInfo, GeneralPrecomposedTransactionFinal, StakeType } from '@suite-common/wallet-types';
import { getFee } from '@suite-common/wallet-utils';
import { Box, IconButton, Note, Row, Text } from '@trezor/components';
Expand Down Expand Up @@ -48,13 +48,20 @@ export const TransactionReviewSummary = ({
const locale = useLocales();
const { symbol, accountType, index, networkType } = account;
const network = networks[symbol];
const fee = getFee(networkType, tx);

const baseFee = fees[symbol].levels[0].baseFeePerGas;
const hasEip1559Feature = getNetworkFeatures(symbol).includes('eip1559');
const shouldUsePriorityFees = !!tx.fee && hasEip1559Feature && !!baseFee;
const fee = getFee({ account, tx, shouldUsePriorityFees });

const estimateTime = getEstimatedTime(networkType, fees[account.symbol], tx);

const formFeeRate = drafts[currentAccountKey]?.feePerUnit;
const isFeeCustom = drafts[currentAccountKey]?.selectedFee === 'custom';
const isComposedFeeRateDifferent = isFeeCustom && formFeeRate !== fee;

const isEthereumNetworkType = networkType === 'ethereum';

return (
<Row columnGap={spacings.md} rowGap={spacings.xxs} flexWrap="wrap">
<Row gap={spacings.xxs}>
Expand Down Expand Up @@ -82,17 +89,15 @@ export const TransactionReviewSummary = ({
</Note>
)}

{networkType === 'ethereum' ? (
<Note iconName="gasPump">
<Translation id="TR_GAS_PRICE" />
{': '}
<FeeRate feeRate={fee} networkType={network.networkType} symbol={symbol} />
</Note>
) : (
<Note iconName="receipt">
<FeeRate feeRate={fee} networkType={network.networkType} symbol={symbol} />
</Note>
)}
<Note iconName={isEthereumNetworkType ? 'gasPump' : 'receipt'}>
{isEthereumNetworkType && (
<>
<Translation id="TR_GAS_PRICE" />
{': '}
</>
)}
<FeeRate feeRate={fee} networkType={network.networkType} symbol={symbol} />
</Note>

{isComposedFeeRateDifferent && network.networkType === 'bitcoin' && (
<Translation id="TR_FEE_RATE_CHANGED" />
Expand Down
12 changes: 10 additions & 2 deletions packages/suite/src/components/wallet/Fees/CustomFee/CurrentFee.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,20 @@ type CurrentFeeProps = {
feeIconName: IconName;
currentFee: string;
symbol: NetworkSymbol;
isEip1559?: boolean;
};

export const CurrentFee = ({ networkType, feeIconName, currentFee, symbol }: CurrentFeeProps) => (
// For priority fees it should show current base fee
export const CurrentFee = ({
networkType,
feeIconName,
currentFee,
symbol,
isEip1559 = false,
}: CurrentFeeProps) => (
<Row justifyContent="space-between">
<Text variant="tertiary" typographyStyle="hint">
<Translation id="TR_CURRENT_FEE_CUSTOM_FEES" />
<Translation id={isEip1559 ? 'TR_CURRENT_BASE_FEE' : 'TR_CURRENT_FEE_CUSTOM_FEES'} />
</Text>
<Text variant="default" typographyStyle="hint">
<Row alignItems="center" gap={spacings.xxs}>
Expand Down
15 changes: 15 additions & 0 deletions packages/suite/src/components/wallet/Fees/CustomFee/CustomFee.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import {
UseFormSetValue,
} from 'react-hook-form';

import { fromWei } from 'web3-utils';

import { NetworkSymbol, NetworkType } from '@suite-common/wallet-config';
import { FeeInfo, FormState } from '@suite-common/wallet-types';
import { getFeeUnits, isInteger } from '@suite-common/wallet-utils';
Expand Down Expand Up @@ -75,7 +77,17 @@ export const CustomFee = <TFieldValues extends FormState>({

const locale = useSelector(selectLanguage);

const normalLevel = feeInfo.levels.filter(level => level.label === 'normal')[0];

const isEip1559 = normalLevel.maxPriorityFeePerGas !== undefined;

const currentBaseFee = fromWei(Number(normalLevel.baseFeePerGas), 'Gwei');

const getCurrentFee = () => {
if (isEip1559) {
return `${currentBaseFee}`;
}

const { levels } = feeInfo;
const middleIndex = Math.floor((levels.length - 1) / 2);

Expand All @@ -92,11 +104,14 @@ export const CustomFee = <TFieldValues extends FormState>({
feeIconName={feeIconName}
currentFee={getCurrentFee()}
symbol={symbol}
isEip1559={isEip1559}
/>
{networkType === 'ethereum' ? (
<CustomFeeEthereum
{...props}
networkType={networkType}
isEip1559={isEip1559}
currentBaseFee={currentBaseFee}
feeInfo={feeInfo}
register={register}
control={control}
Expand Down
Loading
Loading