Create compatible contracts
Contract that are compatible with the IAutomateCompatible
Ankr interface allow you to create custom logic tasks and automate contract function execution based on that logic.
A compatible contract contains:
- A checker, that repeatedly checks on an event.
- A follow-up executor, that contains the custom logic and executes it when the checker returns
true
.
IAutomateCompatible
interface
The interface contains a checker checkTask()
and an executor with the custom logic performTask()
.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;
interface IAutomateCompatible {
/**
* @notice method that is simulated by the automators to see if any work actually
* needs to be performed. This method does does not actually need to be
* executable, and since it is only ever simulated it can consume lots of gas.
* @dev To ensure that it is never called, you may want to add the
* cannotExecute modifier from AutomateBase to your implementation of this
* method.
* @param checkData specified in the task registration so it is always the
* same for a registered task. This can easily be broken down into specific
* arguments using `abi.decode`, so multiple tasks can be registered on the
* same contract and easily differentiated by the contract.
* @return taskNeeded boolean to indicate whether the automator should call
* performtask or not.
* @return performData bytes that the automator should call performtask with, if
* task is needed. If you would like to encode data to decode later, try
* `abi.encode`.
*/
function checkTask(bytes calldata checkData) external returns (bool taskNeeded, bytes memory performData);
/**
* @notice method that is actually executed by the automators, via the registry.
* The data returned by the checkTask simulation will be passed into
* this method to actually be executed.
* @dev The input to this method should not be trusted, and the caller of the
* method should not even be restricted to any single registry. Anyone should
* be able call it, and the input should be validated, there is no guarantee
* that the data passed in is the performData returned from checkTask. This
* could happen due to malicious automators, racing automators, or simply a state
* change while the performTask transaction is waiting for confirmation.
* Always validate the data passed in.
* @param performData is the data which was passed back from the checkData
* simulation. If it is encoded, it can easily be decoded into other types by
* calling `abi.decode`. This data should not be trusted, and should be
* validated against the contract's current state.
*/
function performTask(bytes calldata performData) external;
}
Example contract
To use the custom logic option in Ankr Automation, your contract must meet the following requirements:
- Inherit the
IAutomateCompatible
interface. - Use the
IAutomateCompatible
interface in the way that is compatible with Ankr Automation. - Implement the function
checkTask()
that will be executed off-chain to see if the logic inperformTask()
needs to be executed. - Implement the
performTask()
that will be executed on-chain whencheckTask()
returnstrue
.
Following these requirements, our example contract will increase a counter after every interval
seconds.
If you register this contract as a Task in Ankr Automation, the Automation simulates checkTask
off-chain during every block to determine if the interval
seconds have passed since the lastTimeStamp
.
When checkTask
returns true
, the Automation calls performTask()
on-chain and increments the counter. This cycle repeats until the Task 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
interface IAutomateCompatible {
/**
* @notice method that is simulated by the automators to see if any work actually
* needs to be performed. This method does does not actually need to be
* executable, and since it is only ever simulated it can consume lots of gas.
* @dev To ensure that it is never called, you may want to add the
* cannotExecute modifier from AutomateBase to your implementation of this
* method.
* @param checkData specified in the task registration so it is always the
* same for a registered task. This can easily be broken down into specific
* arguments using `abi.decode`, so multiple tasks can be registered on the
* same contract and easily differentiated by the contract.
* @return taskNeeded boolean to indicate whether the automator should call
* performtask or not.
* @return performData bytes that the automator should call performtask with, if
* task is needed. If you would like to encode data to decode later, try
* `abi.encode`.
*/
function checkTask(bytes calldata checkData) external returns (bool taskNeeded, bytes memory performData);
/**
* @notice method that is actually executed by the automators, via the registry.
* The data returned by the checkTask simulation will be passed into
* this method to actually be executed.
* @dev The input to this method should not be trusted, and the caller of the
* method should not even be restricted to any single registry. Anyone should
* be able call it, and the input should be validated, there is no guarantee
* that the data passed in is the performData returned from checkTask. This
* could happen due to malicious automators, racing automators, or simply a state
* change while the performTask transaction is waiting for confirmation.
* Always validate the data passed in.
* @param performData is the data which was passed back from the checkData
* simulation. If it is encoded, it can easily be decoded into other types by
* calling `abi.decode`. This data should not be trusted, and should be
* validated against the contract's current state.
*/
function performTask(bytes calldata performData) external;
}
/**
* 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 SampleCompatible is IAutomateCompatible {
/**
* Public counter variable
*/
uint public counter;
address public owner;
/**
* Use an interval in seconds and a timestamp to slow execution of Task
*/
uint public interval;
uint public lastTimeStamp;
constructor() {
interval = 3600;
lastTimeStamp = block.timestamp;
owner = msg.sender;
counter = 0;
}
function checkTask(
bytes calldata /* checkData */
)
external
view
override
returns (bool taskNeeded, bytes memory /* performData */)
{
taskNeeded = (block.timestamp - lastTimeStamp) > interval;
// We don't use the checkData in this example. The checkData is defined when the Task was registered.
}
function performTask(bytes calldata /* performData */) external override {
//We highly recommend revalidating the task in the performTask() 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 checkTask() function
}
function setInterval(uint256 _interval) external {
require(msg.sender == owner);
interval = _interval;
}
}
Test it in Remix
Try out this example contract in Remix (opens in a new tab)
Compile and deploy your own Automation Counter onto a supported Testnet (currently, BNB Smart Chain).
- In the Remix example, select the Compile tab on the left and press the Compile button. Make sure the compilation goes without errors; warnings in this example are acceptable and will not block the deployment.
- Select the Deploy tab, set the environment to Injected Provider — MetaMask, confirm connecting to MetaMask, and deploy the AutomatedCounter.sol smart contract to Binance Smart Chain Testnet. When deploying the contract, specify the interval value; use a short value, e.g., 3s. This is the interval at which the
performTask()
function will be called. - After deployment is complete, copy the address of the deployed contract and use it to create and register a Task.
Let's now look closer at the functions of the contract.
Functions
There are two main functions, which are inherited from the IAutomateCompatible
interface:
checkTask(bytes calldata checkData)
thatreturns (bool taskNeeded, bytes memory performData)
;performTask(bytes calldata performData)
;
checkTask()
Repeatedly checks if the performTask()
should be executed.
checkTask()
executes off-chain.
Parameters
checkData
(bytes) — information passed at the Task registration phase to execute different code paths. For example, to check the balance of a specific address, set thecheckData
to abi encode of the address.checkData
can be empty (0x).
Returns
taskNeeded
(bool) — triggers an on-chainperformTask()
call whentrue
.performData
(bytes) — passed to theperformTask()
function asperformData
, which allows you to perform complex and gas intensive calculations as a simulation off-chain and only pass the needed data on-chain. Useabi.encode
for encoding/decoding.
You can create a highly flexible off-chain computation infrastructure that can perform logic on-chain by using checkData
and performData
. Both computations are entirely programmable.
performTask()
When checkTask()
returns taskNeeded == true
, the Automation broadcasts a transaction to the blockchain to execute your performTask()
function on-chain with performData
as an input.
During the Task registration, you have to specify the max gas limit for the contract.
You can learn the best gas value for your Task by simulating the performTask()
function and adding enough overhead to take into account increases that might happen due to changes in performData
or on-chain data.
The limit you set cannot exceed the callGasLimit
in the configuration of the Ankr Automation registry.
Parameters
performData
(bytes) — passed bycheckTask()
to theperformTask()
function. Useabi.encode
for encoding/decoding. The data should always be validated against the current state of your deployed contract. Before usingperformData
on-chain inperformTask()
and paying gas fees, use it off-chain incheckTask()
to run extensive computations such as a high number of addresses that you are validating for conditions or identifying the subset of states that must be updated.
Best practices
Revalidate performTask()
We recommend that you revalidate the conditions and data in performTask() before work is performed. By default, performTask() is external and thus any party can call it, so revalidation is recommended. If you send data from your checkTask() to your performTask() 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 performTask() 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 Task 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 checkTask(). These checks validate the conditions before doing the work.
For example, if you have a contract where you create a timer in checkTask() that is designed to start a game at a specific time, validate the condition to ensure third-party calls to your performTask() 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 performTask() that funds a wallet and the address of the wallet is received via the performData parameter, ensure you have a list of permissible 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 performTask() 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 performTask() when the checkTask() conditions haven’t yet been tested by Ankr 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 TaskRegistry.
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 Ankr Automation Network will continue to operate under stress, but so should your contract.