Skip to main content

smart-contracts: gas forwarding

The Nil.sol contract in the smart-contracts package provides the means to handle 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,
...
);
)