Segmented Transaction Fees

This guide will explain why transaction fees are important, how they are calculated, and how you can handle fees within your implementation. Specifically, it lays out how you can estimate the costs of a transaction, how you can set the limit for costs, and how you can optimize your Cadence code to reduce transaction costs where possible.

The guide will conclude with information on how to educate your users about fees and how to learn more about the implementation of transaction fees.

Note: Transaction cost implementation is based on a community-involved FLIP process. The work is currently ongoing. Jump to the “Learn More” section to participate in the process.

Understanding the need for transaction fees

Segmented transaction fees are essential to ensure fair pricing based on the impact on the network. For instance, more heavy operations will require more resources to process and propagate transactions. Common operations, however, will stay reasonably priced.

Fees will improve the overall security of the network by making malicious actions (eg spam) on the network less viable.

The unique Flow architecture is targeted at high throughput. It makes it easier to have slack in the system, so short spikes can be handled more gracefully.

Understanding fee structure

Fees are calculated based on three components: execution fee, inclusion fee, and network surge factor.

Inclusion and execution fees can be expressed as inclusion or execution effort and an associated multiplier to reflect the costs of the inclusion and execution effort. The final transaction fee calculation looks like this:

1
inclusionFee = inclusionEffort * inclusionEffortCost;
2
3
executionFee = executionEffort * executionEffortCost;
4
5
totalFee = (inclusionFee + executionFee) * surgeFactor;

Note: If you want to learn more about the cost function, take a look at FLIP 753.

Execution costs

The execution effort for a transaction is determined by the code path the transaction takes and the actions it does. The actions that have an associated execution effort cost can be separated into four broad buckets:

  • Normal lines of cadence, loops, or function calls
  • Reading data from storage, charged per byte read
  • Writing data to storage, charged per byte written
  • Account creation

Cost overview

To provide you a better understanding of the cost ranges, here are some common transaction types and their associated execution costs, given the current executionEffortCost and inclusionEffortCost parameters:

Transaction TypeEstimated cost (FLOW)Relative cost to FT transfer
FT transfer0.000001851 (baseline)
Mint a small NFT (heavily depends on the NFT size)0.00000191
Empty Transaction0.0000010.5
Add key to an account0.0000010.5
Create 1 Account0.000003151.7
Create 10 accounts0.0000226112.2
Deploying a contract that is ~50kb0.0000296516

Inclusion costs

The inclusion effort of a transaction represents the work needed for:

  • Including the transaction in a block
  • Transporting the transaction information from node to node
  • Verifying transaction signatures

Right now, the inclusion effort is always 1.0 and the inclusion effort cost is fixed to 0.000001.

Note: Inclusion effort will always be calculable without executing the transaction code.

In the future, costs for inclusion will be impacted by the byte size of the transaction and the number of signatures required.

Note: The changes to variable inclusion costs will be updated in one of the upcoming sporks.

Network surge

In the future, a network surge will be applied when the network is busy due to an increased influx of transactions required to be processed or a decrease in the ability to process transactions. Right now, the network surge is fixed to 1.0.

Storage fees

Storage fees are implemented differently from transaction fees. Read the Storing Data on Flow guide for more details. In summary, storage fees are a cost associated with storing data on-chain.

Estimating transaction costs

Cost estimation is a two-step process. First, you need to gather the execution effort with either the emulator, on testnet, or on mainnet. Second, you use the execution effort for a transaction to calculate the final fees using one of the JavaScript or Go FCL SDKs.

Understanding execution effort

Execution effort is best determined by running a transaction and reviewing the emitted event details.

Using Flow Emulator

You can start the emulator using the Flow CLI. Run your transaction and take a look at the events emitted:

1
0|emulator | time="2022-04-06T17:13:22-07:00" level=info msg="⭐ Transaction executed" computationUsed=3 txID=a782c2210c0c1f2a6637b20604d37353346bd5389005e4bff6ec7bcf507fac06

You should see the computationUsed field. Take a note of the value, you will use it in the next step.

On testnet or mainnet

Once a transaction is completed, you can use an explorer like Flowscan to review the transaction details and events emitted. For Flowscan, you can open the transaction in question and look for the event FeesDeducted from the FlowFees contract:

flowscan-fees

In the event data on the right side, you will see a set of fields representing FeeParameters:

  • surgeFactor
  • inclusionEffort
  • executionEffort

Take a note of the last value in the list - the executionEffort value. You will use it in the next step.

Calculating final costs

The cost for transactions can be calculated using the following FCL scripts on mainnet/testnet respectively.

On mainnet

1
import FlowFees from 0xf919ee77447b7497
2
pub fun main(
3
inclusionEffort: UFix64,
4
executionEffort: UFix64
5
): UFix64 {
6
return FlowFees.computeFees(inclusionEffort: inclusionEffort, executionEffort: executionEffort)
7
}

On testnet

1
import FlowFees from 0x912d5440f7e3769e
2
pub fun main(
3
inclusionEffort: UFix64,
4
executionEffort: UFix64
5
): UFix64 {
6
return FlowFees.computeFees(inclusionEffort: inclusionEffort, executionEffort: executionEffort)
7
}

Configuring execution limits

FCL SDKs allow you to set the execution effort limit for each transaction. Based on the execution effort limit determined in the previous step, you should set a reasonable maximum to avoid unexpected behavior and protect your users. The final transaction fee is computed from the actual execution effort used up to this maximum.

Note: Keep in mind that the limits are not for the final fees that the user will have to pay. The limits are for the execution efforts specifically.

It is important to set a limit that isn’t too high or too low. If it is set too high, the payer needs to have more funds in their account before sending the transaction. If it is too low, the execution could fail and all state changes are dropped.

Using FCL JS SDK

You need to set the limit parameter for the mutate function, for example:

1
import * as fcl from "@onflow/fcl"
2
3
const transactionId = await fcl.mutate({
4
cadence: `
5
transaction {
6
execute {
7
log("Hello from execute")
8
}
9
}
10
`,
11
proposer: fcl.currentUser,
12
payer: fcl.currentUser,
13
limit: 100
14
})
15
16
const transaction = await fcl.tx(transactionId).onceSealed();
17
console.log(transaction;)

Using FCL Go SDK

You need to call the SetGasLimit method to set the fee limit, for example:

1
import (
2
"github.com/onflow/flow-go-sdk"
3
"github.com/onflow/flow-go-sdk/crypto"
4
)
5
6
var (
7
myAddress flow.Address
8
myAccountKey flow.AccountKey
9
myPrivateKey crypto.PrivateKey
10
)
11
12
tx := flow.NewTransaction().
13
SetScript([]byte("transaction { execute { log(\"Hello, World!\") } }")).
14
SetGasLimit(100).
15
SetProposalKey(myAddress, myAccountKey.Index, myAccountKey.SequenceNumber).
16
SetPayer(myAddress)

Optimizing Cadence code to reduce effort

Several optimizations can lead to reduced execution time of transactions. Below is a list of some practices. This list is not exhaustive but rather exemplary.

Limit functions calls

Whenever you make function calls, make sure these are absolutely required. In some cases, you might be able to check prerequisites and avoid additional calls:

1
for obj in sampleList {
2
/// check if call is required
3
if obj.id != nil {
4
functionCall(obj)
5
}
6
}

Limit loops and iterations

Whenever you want to iterate over a list, make sure it is necessary to iterate through all elements as opposed to a subset. Avoid loops to grow in size too much over time. Limit loops when possible.

1
// Iterating over long lists can be costly
2
pub fun sum(list: [Int]): Int {
3
var total = 0
4
var i = 0
5
// if list grows too large, this might not be possible anymore
6
while i < list.length {
7
total = total + list[i]
8
}
9
return total
10
}
11
12
// Consider designing transactions (and scripts) in a way where work can be "chunked" into smaller pieces
13
pub fun partialSum(list: [Int], start: Int, end: Int): Int {
14
var partialTotal = 0
15
var i = start
16
while i < end {
17
partialTotal = partialTotal + list[i]
18
}
19
return partialTotal
20
}

Understand the impact of function calls

Some functions will require more execution efforts than others. You should carefully review what function calls are made and what execution they involve.

1
// be aware functions that call a lot of other functions
2
// (or call themselves) might cost a lot
3
pub fun fib(_ x: Int): Int {
4
if x == 1 || x== 0 {
5
return x
6
}
7
// + 2 function calls each recursion
8
return fib(x-1) + fib(x-2)
9
}
10
11
// consider inlining functions with single statements, to reduce costs
12
pub fun add(_ a: Int, _ b: Int): Int {
13
// single statement; worth inlining
14
return a + b
15
}

Avoid excessive load and save operations

Avoid costly loading and storage operations and borrow references where possible, for example:

1
transaction {
2
3
prepare(acct: AuthAccount) {
4
5
// Borrows a reference to the stored vault, much less costly operation that removing the vault from storage
6
let vault <- acct.borrow<&ExampleToken.Vault>(from: /storage/exampleToken)
7
8
let burnVault <- vault.withdraw(amount: 10)
9
10
destroy burnVault
11
12
// No `save` required because we only used a reference
13
}
14
}

Note: If the requested resource does not exist, no reading costs are charged.

Limit accounts created per transaction

Creating accounts and adding keys are associated with costs. Try to only create accounts and keys when necessary.

Check user’s balance before executing transactions

You should ensure that the user’s balance has enough balance to cover the highest possible fees. For FT transfers, you need to cover the amount to transfer in addition to the highest possible fees.

Educating users

Wallets will handle the presentation of the final transaction costs but you can still facilitate the user experience by educating them within your application.

If your user is using non-custodial wallets, they may have to pay the transaction and want to understand the fees. Here are some suggestions.

Explain that costs can vary depending on the network usage

Suggested message: “Fees improve the security of the network. They are flexible to ensure fair pricing based on the impact on the network.”

Explain that waiting for the network surge to pass is an option

Inevitably, network surges will cause higher fees. Users who might want to submit a transaction while the network usage is surging should consider sending the transaction at a later time to reduce costs.

Explain that the wallet might not allow the transaction due to a lack of funds

If dynamic fees increase to the highest possible level, the user’s fund might not be enough to execute the transaction. Let the users know that they should either add funds or try when the network is less busy.

How to learn more

There are several places to learn more about transaction fees:

Note: If you have thoughts on the implementation of transaction fees on Flow, you can leave feedback on this forum post.

FAQs

When will the fee update go into effect?

The updates were rolled out with the Spork on April 6, 2022, and were enabled on June 1st during the weekly epoch transition.

Why are fees collected even when transactions fail?

Broadcasting and verifying a transaction requires execution, so costs are deducted appropriately.

What execution costs are considered above average?

There is no average for execution costs. Every function will vary significantly based on the logic implemented. You should review the optimization best practices to determine if you could reduce your costs.

Do hardware wallets like Ledger support segmented fees?

Yes.

What is the lowest execution cost?

The lowest execution cost is 1. This means your transaction included one function call or loop that didn't read or write any date.

Can I determine how much a transaction will cost on mainnet without actually paying?

You can estimate the costs in a two-way process: 1) determine execution costs for transactions (emulator or testnet) and 2) use an FCL SDK method to calculate the final transaction fees.

How accurate will testnet fees be to mainnet fees?

Final fees are determined by the surge factor on the network. The surge factor for the testnet will be different from the factor for the mainnet, so you need to expect a variation between mainnet and testnet estimates.

I use Blocto and I haven't paid any fees yet. Why is that?

That is because Blocto is acting as the payer for transactions. Non-custodial wallets may have the user pay the transaction. Additionally, apps can sponsor the transaction if they choose.