NEW

Connect the world's APIs to Web3 with Chainlink Functions. Get started

Making flexible, secure, and low-cost contracts

In this guide, you will learn how the flexibility of Chainlink Automation enables important design patterns that reduce gas fees, enhance the resilience of dApps, and improve end-user experience. Smart contracts themselves cannot self-trigger their functions at arbitrary times or under arbitrary conditions. Transactions can only be initiated by another account.

Start by integrating an example contract to Chainlink Automation that has not yet been optimized. Then, deploy a comparison contract that shows you how to properly use the flexibility of Chainlink Automation to perform complex computations without paying high gas fees.

Prerequisites

This guide assumes you have a basic understanding of Chainlink Automation. If you are new to Keepers, complete the following guides first:

Chainlink Automation is supported on several networks.

Problem: On-chain computation leads to high gas fees

In the guide for Creating Compatible Contracts, you deployed a basic counter contract and verified that the counter increments every 30 seconds. However, more complex use cases can require looping over arrays or performing expensive computation. This leads to expensive gas fees and can increase the premium that end-users have to pay to use your dApp. To illustrate this, deploy an example contract that maintains internal balances.

The contract has the following components:

  • A fixed-size(1000) array balances with each element of the array starting with a balance of 1000.
  • The withdraw() function decreases the balance of one or more indexes in the balances array. Use this to simulate changes to the balance of each element in the array.
  • Automation Nodes are responsible for regularly re-balancing the elements using two functions:
    • The checkUpkeep() function checks if the contract requires work to be done. If one array element has a balance of less than LIMIT, the function returns upkeepNeeded == true.
    • The performUpkeep() function to re-balances the elements. To demonstrate how this computation can cause high gas fees, this example does all of the computation within the transaction. The function finds all of the elements that are less than LIMIT, decreases the contract liquidity, and increases every found element to equal LIMIT.
samples/Automation/BalancerOnChain.sol

Test this example using the following steps:

  1. Deploy the contract using Remix on the supported testnet of your choice.

  2. Before registering the upkeep for your contract, decrease the balances of some elements. This simulates a situation where upkeep is required. In Remix, Withdraw 100 at indexes 10,100,300,350,500,600,670,700,900. Pass 100,[10,100,300,350,500,600,670,700,900] to the withdraw function:

    Withdraw 100 at 10,100,300,350,500,600,670,700,900

    You can also perform this step after registering the upkeep if you need to.

  3. Register the upkeep for your contract as explained here. Because this example has high gas requirements, specify the maximum allowed gas limit of 2,500,000.

  4. After the registration is confirmed, Automation Nodes perform the upkeep.

    BalancerOnChain Upkeep History

  5. Click the transaction hash to see the transaction details in Etherscan. You can find how much gas was used in the upkeep transaction.

    BalancerOnChain Gas

In this example, the performUpkeep() function used 2,481,379 gas. This example has two main issues:

  • All computation is done in performUpkeep(). This is a state modifying function which leads to high gas consumption.
  • This example is simple, but looping over large arrays with state updates can cause the transaction to hit the gas limit of the network, which prevents performUpkeep from running successfully.

To reduce these gas fees and avoid running out of gas, you can make some simple changes to the contract.

Solution: Perform complex computations with no gas fees

Modify the contract and move the computation to the checkUpkeep() function. This computation doesn’t consume any gas and supports multiple upkeeps for the same contract to do the work in parallel. The main difference between this new contract and the previous contract are:

  • The checkUpkeep() function receives checkData, which passes arbitrary bytes to the function. Pass a lowerBound and an upperBound to scope the work to a sub-array of balances. This creates several upkeeps with different values of checkData. The function loops over the sub-array and looks for the indexes of the elements that require re-balancing and calculates the required increments. Then, it returns upkeepNeeded == true and performData, which is calculated by encoding indexes and increments. Note that checkUpkeep() is a view function, so computation does not consume any gas.
  • The performUpkeep() function takes performData as a parameter and decodes it to fetch the indexes and the increments.

This data should always be validated against the contract’s current state to ensure that performUpkeep() is idempotent. It also blocks malicious Automation Nodes from sending non-valid data. This example, tests that the state is correct after re-balancing: require(_balance == LIMIT, "Provided increment not correct");

samples/Automation/BalancerOffChain.sol

Run this example to compare the gas fees:

  1. Deploy the contract using Remix on the supported testnet of your choice.

  2. Withdraw 100 at 10,100,300,350,500,600,670,700,900. Pass 100,[10,100,300,350,500,600,670,700,900] to the withdraw function the same way that you did for the previous example.

  3. Register three upkeeps for your contract as explained here. Because the Automation Nodes handle much of the computation off-chain, a gas limit of 200,000 is sufficient. For each registration, pass the following checkData values to specify which balance indexes the registration will monitor. Note: You must remove any breaking line when copying the values.

    Upkeep NameCheckData(base16)Remark: calculated using abi.encode()
    balancerOffChainSubset10x000000000000000000000000
    00000000000000000000000000
    00000000000000000000000000
    00000000000000000000000000
    0000000000000000000000014c
    lowerBound: 0
    upperBound: 332
    balancerOffChainSubset20x000000000000000000000000
    00000000000000000000000000
    0000000000014d000000000000
    00000000000000000000000000
    0000000000000000000000029a
    lowerBound: 333
    upperBound: 666
    balancerOffChainSubset30x000000000000000000000000
    00000000000000000000000000
    0000000000029b000000000000
    00000000000000000000000000
    000000000000000000000003e7
    lowerBound: 667
    upperBound: 999
  4. After the registration is confirmed, the three upkeeps run:

    BalancerOffChain1 History

    BalancerOffChain2 History

    BalancerOffChain3 History

  5. Click each transaction hash to see the details of each transaction in Etherscan. Find the gas used by each of the upkeep transactions:

    BalancerOffChain1 Gas

    BalancerOffChain2 Gas

    BalancerOffChain3 Gas

In this example the total gas used by each performUpkeep() function was 133,464 + 133,488 + 133,488 = 400,440. This is an improvement of about 84% compared to the previous example, which used 2,481,379 gas.

Conclusion

Using Chainlink Automation efficiently not only allows you to reduce the gas fees, but also keeps them within predictable limits. That’s the reason why several Defi protocols outsource their maintenance tasks to Chainlink Automation.

What's next

Stay updated on the latest Chainlink news