Creating Compatible Contracts
Use custom logic to allow Chainlink Automation to determine when to execute your smart contract functions.
Learn how to make smart contracts that are compatible with the AutomationCompatibleInterface
contract and its functions.
Example contract
To use Chainlink Automation, contracts must meet the following requirements:
- Import
AutomationCompatible.sol
. You can refer to the Chainlink Contracts on GitHub to find the latest version. - Use the
AutomationCompatibleInterface
from the library to ensure yourcheckUpkeep
andperformUpkeep
function definitions match the definitions expected by the Chainlink Automation Network. - Include a
checkUpkeep
function that contains the logic that will be executed off-chain to see ifperformUpkeep
should be executed.checkUpkeep
can use on-chain data and a specifiedcheckData
parameter to perform complex calculations off-chain and then send the result toperformUpkeep
asperformData
. - Include a
performUpkeep
function that will be executed on-chain whencheckUpkeep
returnstrue
. BecauseperformUpkeep
is external, users are advised to revalidate conditions and performData.
Use these elements to create a compatible contract that will automatically increment a counter after every updateInterval
seconds. After you register the contract as an upkeep, the Chainlink Automation Network simulates our checkUpkeep
off-chain during every block to determine if the updateInterval
time has passed since the last increment (timestamp). When checkUpkeep
returns true, the Chainlink Automation Network calls performUpkeep
on-chain and increments the counter. This cycle repeats until the upkeep is cancelled or runs out of funding.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
// AutomationCompatible.sol imports the functions from both ./AutomationBase.sol and
// ./interfaces/AutomationCompatibleInterface.sol
import "@chainlink/contracts/src/v0.8/AutomationCompatible.sol";
/**
* THIS IS AN EXAMPLE CONTRACT THAT USES HARDCODED VALUES FOR CLARITY.
* THIS IS AN EXAMPLE CONTRACT THAT USES UN-AUDITED CODE.
* DO NOT USE THIS CODE IN PRODUCTION.
*/
contract Counter is AutomationCompatibleInterface {
/**
* Public counter variable
*/
uint public counter;
/**
* Use an interval in seconds and a timestamp to slow execution of Upkeep
*/
uint public immutable interval;
uint public lastTimeStamp;
constructor(uint updateInterval) {
interval = updateInterval;
lastTimeStamp = block.timestamp;
counter = 0;
}
function checkUpkeep(
bytes calldata /* checkData */
)
external
view
override
returns (bool upkeepNeeded, bytes memory /* performData */)
{
upkeepNeeded = (block.timestamp - lastTimeStamp) > interval;
// We don't use the checkData in this example. The checkData is defined when the Upkeep was registered.
}
function performUpkeep(bytes calldata /* performData */) external override {
//We highly recommend revalidating the upkeep in the performUpkeep function
if ((block.timestamp - lastTimeStamp) > interval) {
lastTimeStamp = block.timestamp;
counter = counter + 1;
}
// We don't use the performData in this example. The performData is generated by the Automation Node's call to your checkUpkeep function
}
}
Compile and deploy your own Automation Counter onto a supported Testnet.
- In the Remix example, select the compile tab on the left and press the compile button. Make sure that your contract compiles without any errors. Note that the Warning messages in this example are acceptable and will not block the deployment.
- Select the Deploy tab and deploy the
Counter
smart contract in theinjected web3
environment. When deploying the contract, specify theupdateInterval
value. For this example, set a short interval of 60. This is the interval at which theperformUpkeep
function will be called. - After deployment is complete, copy the address of the deployed contract. This address is required to register your upkeep.
To see more complex examples, go to the utility contracts page.
We will now look at each function in a compatible contract in detail.
Functions
Function Name | Description |
---|---|
checkUpkeep | Runs off-chain at every block to determine if the performUpkeep function should be called on-chain. |
performUpkeep | Contains the logic that should be executed on-chain when checkUpkeep returns true. |
checkUpkeep
function
This function contains the logic that runs off-chain during every block as an eth_call
(link) to determine if performUpkeep
should be executed on-chain. To reduce on-chain gas usage, attempt to do your gas intensive calculations off-chain in checkUpkeep
and pass the result to performUpkeep
on-chain.
Because checkUpkeep
is only off-chain in simulation it is best to treat this as a view
function and not modify any state. This might not always be possible if you want to use more advanced Solidity features like DelegateCall
(link). It is a best practice to import the AutomationCompatible.sol
(link) contract and use the cannotExecute
modifier to ensure that the method can be used only for simulation purposes.
function checkUpkeep(
bytes calldata checkData
) external view override returns (bool upkeepNeeded, bytes memory performData);
Below are the parameters and return values of the checkUpkeep
function. Click each value to learn more about its design patterns and best practices:
Parameters:
checkData
: Fixed and specified at upkeep registration and used in everycheckUpkeep
. Can be empty (0x).
Return Values:
upkeepNeeded
: Boolean that when True will trigger the on-chainperformUpkeep
call.performData
: Bytes that will be used as input parameter when callingperformUpkeep
. If you would like to encode data to decode later, tryabi.encode
.
checkData
You can pass information into your checkUpkeep
function from your upkeep registration to execute different code paths. For example, to check the balance on an specific address, set the checkData
to abi encode of the address. To learn how to create flexible upkeeps with checkData, please see out flexible upkeeps page.
function checkUpkeep(
bytes calldata checkData
) public view returns (bool, bytes memory) {
address wallet = abi.decode(checkData, (address));
return (wallet.balance < 1 ether, bytes(""));
}
Tips on using checkData
:
-
Managing unbounded upkeeps: Limit the problem set of your on-chain execution by creating a range bound for your upkeep to check and perform. This allows you to keep within predefined gas limits, which creates a predictable upper bound gas cost on your transactions. Break apart your problem into multiple upkeep registrations to limit the scope of work.
Example: You could create an upkeep for each subset of addresses that you want to service. The ranges could be 0 to 49, 50 to 99, and 100 to 149.
-
Managing code paths: Pass in data to your
checkUpkeep
to make your contract logic go down different code paths. This can be used in creative ways based on your use case needs.Example: You could support multiple types of upkeep within a single contract and pass a function selector through the
checkData
function.
performData
The response from checkUpkeep
is passed to the performUpkeep
function as performData
. This allows you to perform complex and gas intensive calculations as a simulation off-chain and only pass the needed data on-chain.
You can create a highly flexible off-chain computation infrastructure that can perform precise actions on-chain by using checkData
and performData
. Both of these computations are entirely programmable.
performUpkeep
function
When checkUpkeep
returns upkeepNeeded == true
, the Automation node broadcasts a transaction to the blockchain to execute your performUpkeep
function on-chain with performData
as an input.
Ensure that your performUpkeep
is idempotent. Your performUpkeep
function should change state such that checkUpkeep
will not return true
for the same subset of work once said work is complete. Otherwise the Upkeep will remain eligible and result in multiple performances by the Chainlink Automation Network on the exactly same subset of work. As a best practice, always revalidate conditions for your upkeep at the start of your performUpkeep
function.
function performUpkeep(bytes calldata performData) external override;
Parameters:
performData
: Data which was passed back from thecheckData
simulation. If it is encoded, it can easily be decoded into other types by callingabi.decode
. This data should always be validated against the contract’s current state.
performData
You can perform complex and broad off-chain computation, then execute on-chain state changes on a subset that meet your conditions. This can be done by passing the appropriate inputs within performData
based on the results from your checkUpkeep
. This pattern can greatly reduce your on-chain gas usage by narrowing the scope of work intelligently in your own Solidity code.
-
Identify a list of addresses that require work: You might have a number of addresses that you are validating for conditions before your contract takes an action. Doing this on-chain can be expensive. Filter the list of addresses by validating the necessary conditions within your
checkUpkeep
function. Then, pass the addresses that meets the condition through theperformData
function.For example, if you have a “top up” contract that ensures several hundred account balances never decrease below a threshold, pass the list of accounts that meet the conditions so that the
performUpkeep
function validates and tops up only a small subset of the accounts. -
Identify the subset of states that must be updated: If your contract maintains complicated objects such as arrays and structs, or stores a lot of data, you should read through your storage objects within your
checkUpkeep
and run your proprietary logic to determine if they require updates or maintenance. After that is complete, you can pass the known list of objects that require updates through theperformData
function.
Vyper example
You can find a KeepersConsumer
example here. Read the apeworx-starter-kit README to learn how to run the example.
Best practices
Revalidate performUpkeep
We recommend that you revalidate the conditions and data in performUpkeep
before work is performed. By default the performUpkeep
is external
and thus any party can call it, so revalidation is recommended. If you send data from your checkUpkeep
to your performUpkeep
and this data drives a critical function, please ensure you put adequate checks in place.
Perform ONLY when conditions are met
Some actions must be performed only when specific conditions are met. Check all of the preconditions within performUpkeep
to ensure that state change occurs only when necessary.
In this pattern, it is undesirable for the state change to occur until the next time the Upkeep is checked by the network and the conditions are met. It is a best practice to stop any state change or effects by performing the same checks or similar checks that you use in checkUpkeep
. These checks validate the conditions before doing the work.
For example, if you have a contract where you create a timer in checkUpkeep
that is designed to start a game at a specific time, validate the condition to ensure third-party calls to your performUpkeep
function do not start the game at a different time.
Perform ONLY when data is verified
Some actions must be performed using data you intend to use. Revalidate that the performData
is allowed before execution.
For example, if you have a performUpkeep
that funds a wallet and the address of the wallet is received via the performData
parameter, ensure you have a list of permissable addresses to compare against to prevent third-party calling your function to send money to their address.
When performing is not harmful
Sometimes actions must be performed when conditions are met, but performing actions when conditions are not met is still acceptable. Condition checks within performUpkeep
might not be required, but it can still be a good practice to short circuit expensive and unnecessary on-chain processing when it is not required.
It might be desirable to call performUpkeep
when the checkUpkeep
conditions haven’t yet been tested by Chainlink Automation, so any specific checks that you perform are entirely use case specific.
Test your contract
As with all smart contract testing, it is important to test the boundaries of your smart contract in order to ensure it operates as intended. Similarly, it is important to make sure the compatible contract operates within the parameters of the KeeperRegistry
.
Test all of your mission-critical contracts, and stress-test the contract to confirm the performance and correct operation of your use case under load and adversarial conditions. The Chainlink Automation Network will continue to operate under stress, but so should your contract. For a list of supported Testnet blockchains, please review the supported networks page.