Skip to main content

Gas and value

This tutorial defines how =nil; treats gas and transaction value.

Definition

In nil;, every transaction has two fields responsible for handling gas and value:

  • FeeCredit
  • Value

FeeCredit contains the amount of =nil; base tokens to be paid for transaction execution. This is different from regular Ethereum in that there is no base fee or the premium fee. Instead, FeeCredit is the gas limit (the maximum possible amount of gas the transaction sender is willing to pay) multiplied by the possible gas price within a specific shard.

info

FeeCredit is an estimate created by the transaction sender rather than a precisely calculated transaction execution fee. To evaluate whether a given fee credit would be enough to make a call, use the nil smart-account estimate-fee ADDRESS FUNC_NAME [ARGS] command.

To estimate gas in a different way, use the eth_estimateFee JSON-RPC method or the nil smart-account call-readonly ADDRESS command while providing the --with-details flag. Note that this command may sometimes produce the "out of gas" response which should not be considered an error.

Value is the amount of tokens to be transferred to the address where the transaction is sent. These tokens are not used for transaction execution and, instead, are debited to the receiving address.

info

If a transaction is made to a non-payable function, and Value is not zero, execution will be reverted as the contract will not be able to accept the transferred tokens.

Basic example

This Nil.js example sends a transaction from the user's smart account to another contract. It estimates the gas price to be 100n and is willing to pay for a maximum of 1_000_000n units of gas.

const hash = await smartAccount.sendTransaction({
to: CONTRACT_ADDRESS,
value: 0n,
bounceTo: BOUNCE_ADDRESS,
feeCredit: 1_000_000n * 100n,
data: encodeFunctionData({
abi: CONTRACT_ABI,
functionName: "func_name()",
args: [ARGS],
}),
});

If the receiver contract has a payable function, the smart account would be able to send the following message.

const hash = await smartAccount.sendTransaction({
to: CONTRACT_ADDRESS,
value: 5_000_000n,
bounceTo: BOUNCE_ADDRESS,
feeCredit: 100_000n * 10n,
data: encodeFunctionData({
abi: CONTRACT_ABI,
functionName: "payable_func_name()",
args: [ARGS],
}),
});

Gas forwarding

Gas forwarding applies to cases where an async call to one contract triggers a chain of async calls to other contracts:

Consider this flow:

  • The smart account sets FeeCredit to 500_000 and asynchronously calls Contract A
  • The transaction is executed for 200_000, leaving 300_000 tokens unused
  • As part of executing the initial transaction from the smart account, Contract A must send async calls to Contract B and Contract C requiring at least 150_000 and 350_000 FeeCredit, respectively
  • Contract A must decide how it will pay for these calls

=nil; provides five possible options on how Contract A can handle forwarding tokens for executing subsequent calls. These options are set via the uint8 forwardKind argument when using asyncCall():

function asyncCall(
...
address bounceTo,
uint feeCredit,
uint8 forwardKind,
...
)

No gas forwarding

The smart account can set some FeeCredit and send some tokens to Contract A:

In the above flow:

  • The smart account sets FeeCredit to 200_000 and asynchronously calls Contract A. The smart account also sends 500_000 tokens as Value
  • The transaction is executed for 100_000, leaving another 100_000 of FeeCredit available
  • Instead of forwarding the leftover FeeCredit, Contract A pays for async calls to Contract B and Contract C from its balance

This is how Contract A calls Contract B:

function callContractB(
Nil.asyncCall(
CONTRACT_B_ADDRESS,
...
150000,
Nil.FORWARD_NONE,
...
);
)

And Contract C:

function callContractC(
Nil.asyncCall(
CONTRACT_C_ADDRESS,
...
350000,
Nil.FORWARD_NONE,
...
);
)

Forwarding by absolute value

The smart account only sets FeeCredit and does not send any tokens to Contract A:

In the above flow:

  • The smart account sets FeeCredit to 600_000 and asynchronously calls Contract A. No tokens are sent to Contract A directly
  • The transaction is executed for 100_000, leaving another 500_000 of FeeCredit available
  • Contract A forwards the leftover FeeCredit to pay for execution of Contract B (150_000) and Contract C (350_000)

Here is how Contract A calls Contract B:

function callContractB(
Nil.asyncCall(
CONTRACT_B_ADDRESS,
...
150000,
Nil.FORWARD_VALUE,
...
);
)

And Contract C:

function callContractC(
Nil.asyncCall(
CONTRACT_C_ADDRESS,
...
350000,
Nil.FORWARD_VALUE,
...
);
)

Forwarding by percentage

Similarly to forwarding by value, the smart account only sets FeeCredit and does not send any tokens to Contract A:

The basic flow is also similar to forwarding by value:

  • The smart account sets FeeCredit to 600_000 and asynchronously calls Contract A. No tokens are sent to Contract A directly
  • The transaction is executed for 100_000, leaving another 500_000 of FeeCredit available
  • Contract A forwards the leftover FeeCredit to pay for execution of Contract B (150_000) and Contract C (500_000)

There is one major difference between forwarding by percentage and forwarding by value, and it is in how Contract A calls other contracts. Contract B:

function callContractB(
Nil.asyncCall(
CONTRACT_B_ADDRESS,
...
30,
Nil.FORWARD_PERCENTAGE,
...
);
)

Contract C:

function callContractC(
Nil.asyncCall(
CONTRACT_C_ADDRESS,
...
70,
Nil.FORWARD_PERCENTAGE,
...
);
)

Instead of specifying absolute values in the feeCredit argument, Contract A sets percentages of the leftover FeeCredit it sends to Contract B and Contract C.

Forwarding by equal split

info

Forwarding by equal split is the default option if the forwardKind argument is not specified when calling asyncCall().

The smart account only sets FeeCredit and does not send any tokens to Contract A:

In the above flow:

  • The smart account sets FeeCredit to 600_000 and asynchronously calls Contract A. No tokens are sent to Contract A directly
  • The transaction is executed for 100_000, leaving another 500_000 of FeeCredit available
  • Contract A forwards the leftover FeeCredit to pay for execution of Contract B (250_000) and Contract C (250_000)

Note that Contract A splits the leftover FeeCredit equally between Contract B and Contract C. Here is how it calls Contract B:

function callContractB(
Nil.asyncCall(
CONTRACT_B_ADDRESS,
...
Nil.FORWARD_REMAINING,
...
);
)

Contract C:

function callContractC(
Nil.asyncCall(
CONTRACT_C_ADDRESS,
...
Nil.FORWARD_PERCENTAGE,
...
);
)

The feeCredit argument is not specified in both uses of asyncCall(). As FORWARD_PERCENTAGE is the default option, it can be omitted as well.

Mixed forwarding

Contract A can also specify several different types of forwarding when calling other contracts:

In the above flow:

  • The smart account sets FeeCredit to 1_000_000 and asynchronously calls Contract A. The smart account also sends 300_000 to the balance of Contract A
  • The transaction is executed for 100_000, leaving another 900_000 of FeeCredit available
  • Contract A sends its balance to pay for execution of Contract B (300_000), no gas is forwarded
  • Contract A forwards some of its leftover gas to pay for execution of Contract C (350_000), gas is forwarded by absolute value
  • Contract A forwards some of its leftover gas to pay for the execution of Contract D (300_000), gas is forwarded by percentage
  • Contract A forwards the remaining gas by equal split to Contract E and Contract F
Order of forwarding

When value and percentage forwarding are used in mixed forwarding, value forwarding calculated first. For example, if there is 900_000 leftover FeeCredit and 300_000 is forwarded by value to other contracts while 30% is forwarded by percentage, thia 30% would equal 200_000 instead of 300_000.

Conversely, equal split forwarding is always calculated last. In the above example, only 400_000 leftover FeeCredit will be left after calculating value and percentage forwarding. This amount will be split equally among the remaining contracts to be called.

Here is how Contract A calls Contract B:

function callContractB(
Nil.asyncCall(
CONTRACT_B_ADDRESS,
...
300000,
Nil.FORWARD_NONE,
...
);
)

Contract C:

function callContractC(
Nil.asyncCall(
CONTRACT_C_ADDRESS,
...
350000,
Nil.FORWARD_VALUE,
...
);
)

Contract D:

function callContractD(
Nil.asyncCall(
CONTRACT_D_ADDRESS,
...
30,
Nil.FORWARD_PERCENTAGE,
...
);
)

Contract E:

function callContractE(
Nil.asyncCall(
CONTRACT_E_ADDRESS,
...
Nil.FORWARD_REMAINING,
...
);
)

Contract F:

function callContractF(
Nil.asyncCall(
CONTRACT_F_ADDRESS,
...
Nil.FORWARD_REMAINING,
...
);
)