Intermediate

 

Gas and Fees

In this tutorial, we will dive into the world of gas and fees in the context of the MultiversX blockchain platform. Gas and fees play a crucial role in the functioning of smart contracts and transactions on the blockchain. Understanding how they work is essential for developers looking to build dApps and execute transactions on the MultiversX network. This tutorial will provide a comprehensive guide to the gas and fees system in MultiversX and help you understand how to optimize your transactions for cost-effectiveness and speed. Get ready to learn everything you need to know about gas and fees in MultiversX!

Cost of processing (gas units)

Each MultiversX transaction has a processing cost, expressed as an amount of gas units. At the broadcast time, each transaction must be provided a gas limit (gasLimit), which acts as an upper limit of the processing cost.

Constraints

For any transaction, the gasLimit must be greater or equal to erd_min_gas_limit but smaller or equal to erd_max_gas_per_transaction, these two being parameters of the Network:

networkConfig.erd_min_gas_limit <= tx.gasLimit <= networkConfig.erd_max_gas_per_transaction

Cost components

The actual gas consumption – also known as used gas – is the consumed amount from the provided gas limit – the amount of gas units actually required by the Network in order to process the transaction. The unconsumed amount is called remaining gas.

At processing time, the Network breaks the used gas down into two components:

  • gas used by value movement and data handling

  • gas used by contract execution (for executing System or User-Defined Smart Contract)

NOTE : Simple transfers of value (EGLD transfers) only require the value movement and data handling component of the gas usage (that is, no execution gas), while Smart Contract calls require both components of the gas consumption. This includes ESDT and NFT transfers as well, because they are in fact calls to a System Smart Contract.

The value movement and data handling cost components are easily computable, using the following formula:

tx.gasLimit =
    networkConfig.erd_min_gas_limit +
    networkConfig.erd_gas_per_data_byte * lengthOf(tx.data)

The contract execution cost component is easily computable for System Smart Contract calls (based on formulas specific to each contract), but harder to determine a priori for user-defined Smart Contracts. This is where simulations and estimations are employed.

Processing fee (EGLD)

The processing fee, measured in EGLD, is computed with respect to the actual gas cost – broken down into its components – and the gas price per gas unit, which differs between the components.

The gas price per gas unit for the value movement and data handling must be specified by the transaction, and it must be equal to or greater than a Network parameter called erd_min_gas_price.

While the price of a gas unit for the value movement and data handling component equals the gas price provided in the transaction, the price of a gas unit for the contract execution component is computed with respect to another Network parameter called erd_gas_price_modifier:

value_movement_and_data_handling_price_per_unit = tx.GasPrice
contract_execution_price_per_unit = tx.GasPrice * networkConfig.erd_gas_price_modifier

NOTE : Generally speaking, the price of a gas unit for contract execution is lower than the price of a gas unit for value movement and data handling, due to the gas price modifier for contracts (erd_gas_price_modifier).

The processing fee formula looks like this:

processing_fee =
    value_movement_and_data_handling_cost * value_movement_and_data_handling_price_per_unit +
    contract_execution_cost * contract_execution_price_per_unit

After processing the transaction, the Network will send a value called gas refund back to the sender of the transaction, computed with respect to the unconsumed (component of the) gas, if applicable (if the paid fee is higher than the necessary fee).

EGLD transfers (move balance transactions)

Formula

For EGLD transfers, the actual gas cost of processing is easy to determine precisely, since it only contains the value movement and data handling component. The gas limit should be set to the actual gas cost, according to the previously depicted formula:

tx.gasLimit = 
    networkConfig.erd_min_gas_limit + 
    networkConfig.erd_gas_per_data_byte * lengthOf(tx.data)

Examples

Given:

networkConfig.erd_min_gas_limit is 50000
networkConfig.erd_gas_per_data_byte is 1500
networkConfig.erd_min_gas_price is 1000000000

tx1.data = ""
tx1.gasPrice = networkConfig.erd_min_gas_price

tx2.data = "Hello world!"
tx2.gasPrice = networkConfig.erd_min_gas_price

Then:

tx1.gasLimit = 50000

tx2.gasLimit 
    = 50000 + 1500 * len("Hello world!") 
    = 68000

Furthermore, the fee would be as follows:

fee(tx1) 
    = tx1.gasLimit * tx1.gasPrice 
    = 50000 * 1000000000
    = 50000000000000 atoms of EGLD
    = 0.00005 EGLD

fee(tx2) 
    = tx2.gasLimit * tx2.gasPrice 
    = 68000 * 1000000000
    = 68000000000000 atoms of EGLD
    = 0.000068 EGLD

User-defined Smart Contracts

For user-defined Smart Contract deployments and function calls, the actual gas consumption of processing contains both of the previously mentioned cost components – though, while the value movement and data handling component is easily computable (using the previously depicted formula), the contract execution component is hard to determine precisely a priori. Therefore, for this component, we have to rely on simulations and estimations.

For simulations, we will start a local testnet using mxpy (detailed setup instructions can be found here). Thus, before going further, make sure your local testnet is up and running.

Contract deployments

In order to get the required gasLimit (the actual gas cost) for a deployment transaction, one should use the well-known mxpy contract deploy command, but with the --simulate flag set.

At first, pass the maximum possible amount for gas-limit (no guessing).

$ mxpy --verbose contract deploy --bytecode=./counter.wasm \
 --recall-nonce --gas-limit=600000000 \
 --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem \
 --simulate

In the output, look for txGasUnits. For example:

"txSimulation": {
    ...
    "cost": {
        "txGasUnits": 1849711,
        ...
    }
}

NOTE : The simulated cost txGasUnits contains both components of the cost.

After that, check the cost simulation by running the simulation once again, but this time with the precisegas-limit:

$ mxpy --verbose contract deploy --bytecode=./counter.wasm \
 --recall-nonce --gas-limit=1849711 \
 --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem \
 --simulate

In the output, look for the status – it should be success:

"txSimulation": {
    "execution": {
        "result": {
            "status": "success",
            ...
        },
        ...
    }

In the end, let’s actually deploy the contract:

$ mxpy --verbose contract deploy --bytecode=./counter.wasm \
 --recall-nonce --gas-limit=1849711 \
 --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem \
 --send --wait-result

INFO : For deployments, the execution component of the cost is associated with instantiating the Smart Contract and calling its init() function.

If the flow of init() is dependent on input arguments or it references blockchain data, then the cost will vary as well, depending on these variables. Make sure you simulate sufficient deployment scenarios and increase (decrease) the gas-limit.

Contract calls

In order to get the required gasLimit (the actual gas cost) for a contract call, one should first deploy the contract, then use the mxpy contract call command, with the --simulate flag set.

INFO : If the contract makes further calls to other contracts, please read the next section.

Assuming we’ve already deployed the contract (see above) let’s get the cost for calling one of its endpoints:

$ mxpy --verbose contract call erd1qqqqqqqqqqqqqpgqygvvtlty3v7cad507v5z793duw9jjmlxd8sszs8a2y \
 --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem \
 --function=increment\
 --recall-nonce --gas-limit=600000000\
 --simulate

In the output, look for txGasUnits. For example:

"txSimulation": {
    ...
    "cost": {
        "txGasUnits": 1225515,
        ...
    }
}

In the end, let’s actually call the contract:

$ mxpy --verbose contract call erd1qqqqqqqqqqqqqpgqygvvtlty3v7cad507v5z793duw9jjmlxd8sszs8a2y \
 --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem \
 --function=increment\
 --recall-nonce --gas-limit=1225515\
 --send --wait-result

INFO : If the flow of the called function is dependent on input arguments or it references blockchain data, then the cost will vary as well, depending on these variables. Make sure you simulate sufficient scenarios for the contract call and increase (decrease) the gas-limit.

Contracts calling (asynchronously) other contracts

Suppose we have two contracts: A and B, where A::foo(addressOfB) asynchronously calls B::bar() (e.g. using asyncCall()).

Let’s deploy contracts A and B:

$ mxpy --verbose contract deploy --bytecode=./a.wasm \
 --recall-nonce --gas-limit=5000000 \
 --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem \
 --send --wait-result --outfile=a.json

$ mxpy --verbose contract deploy --bytecode=./b.wasm \
 --recall-nonce --gas-limit=5000000 \
 --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem \
 --send --wait-result --outfile=b.json

Assuming A is deployed at erd1qqqqqqqqqqqqqpgqfzydqmdw7m2vazsp6u5p95yxz76t2p9rd8ss0zp9ts, and B is deployed at erd1qqqqqqqqqqqqqpgqj5zftf3ef3gqm3gklcetpmxwg43rh8z2d8ss2e49aq, let’s simulate A::foo(addressOfB) (at first, pass a large-enough or maximum gas-limit):

$ export hexAddressOfB=0x$(mxpy wallet bech32 --decode erd1qqqqqqqqqqqqqpgqj5zftf3ef3gqm3gklcetpmxwg43rh8z2d8ss2e49aq)

$ mxpy --verbose contract call erd1qqqqqqqqqqqqqpgqfzydqmdw7m2vazsp6u5p95yxz76t2p9rd8ss0zp9ts \
 --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem \
 --function=foo\
 --recall-nonce --gas-limit=50000000\
 --arguments ${hexAddressOfB}\
 --simulate

In the output, look for the simulated cost (as above):

"txSimulation": {
    ...
    "cost": {
        "txGasUnits": 3473900,
        ...
    }
}

The simulated cost represents the actual gas cost for invoking A::foo(), B::bar() and A::callBack().

However, the simulated cost above isn’t the value we are going to use as gasLimit. If we were to do so, we would be presented the error not enough gas.

Upon reaching the call to B::bar() inside A::foo(), the MultiversX VM inspects the remaining gas at runtime and temporarily locks (reserves) a portion of it, to allow for the execution of A::callBack() once the call to B::bar() returns.

With respect to the VM Gas Schedule, the aforementioned remaining gas at runtime has to satisfy the following conditions in order for the temporary gas lock reservation to succeed:

onTheSpotRemainingGas > gasToLockForCallback

gasToLockForCallback =
    costOf(AsyncCallStep) +
    costOf(AsyncCallbackGasLock) +
    codeSizeOf(callingContract) * costOf(AoTPreparePerByte)

NOTE : Subsequent asynchronous calls (asynchronous calls performed by an asynchronously-called contract) will require temporary gas locks as well.

For our example, where A has 453 bytes, the gasToLockForCallback would be (as of February 2022):

gasToLockForCallback = 100000 + 4000000 + 100 * 453 = 4145300

It follows that the value of gasLimit should be:

simulatedCost < gasLimit < simulatedCost + gasToLockForCallback

For our example, that would be:

3473900 < gasLimit < 7619200

For our example, let’s simulate using the following values for gasLimit: 7619200, 7000000, 6000000:

$ mxpy --verbose contract call erd1qqqqqqqqqqqqqpgqfzydqmdw7m2vazsp6u5p95yxz76t2p9rd8ss0zp9ts \
 --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem \
 --function=foo\
 --recall-nonce --gas-limit=7619200\
 --arguments ${hexAddressOfB}\
 --simulate

... inspect output (possibly testnet logs); execution is successful

mxpy --verbose contract call erd1qqqqqqqqqqqqqpgqfzydqmdw7m2vazsp6u5p95yxz76t2p9rd8ss0zp9ts \
 --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem \
 --function=foo\
 --recall-nonce --gas-limit=7000000\
 --arguments ${hexAddressOfB}\
 --simulate

... inspect output (possibly testnet logs); execution is successful

mxpy --verbose contract call erd1qqqqqqqqqqqqqpgqfzydqmdw7m2vazsp6u5p95yxz76t2p9rd8ss0zp9ts \
 --pem=~/multiversx-sdk/testwallets/latest/users/alice.pem \
 --function=foo\
 --recall-nonce --gas-limit=6000000\
 --arguments ${hexAddressOfB}\
 --simulate

... inspect output (possibly testnet logs); ERROR: out of gas when executing B::bar()

Therefore, in our case, a reasonable value for gasLimit would be 7000000.

With this, you complete this workshop successfully!!