Pablo Cibraro

My notes about software development, security and other stuff.

Chainlink Keepers - An alarm clock for your Smart Contracts

alt

Smart Contracts work in reactive mode. Once deployed in the Blockchain, they go to a hibernation state until some work needs to be done. When the work is completed, they go to bed again. They basically react to transactions by executing code.

On the contrary, Smart Contracts in proactive mode, which don't really exist in reality but can be emulated, watch for different conditions to wake up themselves and do their job without intervention. For example, a contract that runs automatically when it detects a price fluctuation or when a given date or time is reached.

These contracts require the intervention of an external agent or worker that is constantly pinging the contract to verify whether one of the conditions to run was met. Those conditions should be checked in one or more view methods to avoid paying gas to the network. That means they can be resolved locally without submitting transactions.

Running an agent or worker now means you require additional off-chain infrastructure, which is centralized.

What if you can also leverage some of the existing decentralized network of oracles (DONs) from Chainlink to run those agents. Enter in the scene the Chainlink Keepers.

Chainlink Keepers

Keepers is a feature recently added in Chainlink for hosting jobs that allow running Smart Contracts proactively. It uses DONs to run the jobs in a decentralized manner. Jobs are called Upkeeps, and the Smart Contracts must implement an interface with the following methods.

function checkUpkeep(
    bytes calldata checkData
  )
    external
    returns (
        bool upkeepNeeded,
        bytes memory performData
);

function performUpkeep(
    bytes calldata performData
) external;

checkUpKeep is the method called periodically by the jobs to check if some works need to be done. It's the alarm clock for your contract. It receives a parameter checkData, which could be helpful to check for the conditions in the method implementation. That data is a fixed value that you configure as part of the job. It must be implemented as a view to avoiding paying gas on each call. It returns two values, upKeepNeeded, which specifies if an action must be performed, and data to be passed to execute that action.

performUpKeep is the method where the Smart Contract does the work. It's called if upKeepNeeded returned a value equal to true.

Chainlink also offers an application where you can configure the jobs. As part of the configuration of the job, you must provide the following data.

  • An email address
  • Name of the job (unkeep)
  • Address of the deployed contract
  • Admin Address. This is the address of an owner that can withdraw funds associated to the contract.
  • Gas Limit. This gas limit is for executing the performUnKeep method.
  • Check Data, which is passed to the checkUpkeep method. This is an array of bytes (hex), which could result from using abi.encode.

This feature is still in beta and the registration must be done manually in the Keepers app. Not support for automation APIs for the moment

Anatomy of a Keeper contract

Let's say that you want to implement a Betting contract for a game with two participants. You would like to select a winner and distribute the gains after the game finishes.

//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;

interface KeeperCompatibleInterface {
    function checkUpkeep(bytes calldata checkData) external returns (bool upkeepNeeded, bytes memory performData);
    function performUpkeep(bytes calldata performData) external;
}

contract Betting is KeeperCompatibleInterface{

   uint public matchTimestamp;

    constructor(uint _matchTimestamp) {
      matchTimestamp = _matchTimestamp;
    }

    function checkUpkeep(bytes calldata checkData)  external view override returns (bool upkeepNeeded, bytes memory performData) {
         upkeepNeeded = (block.timestamp > matchTimestamp);
    }

    function performUpkeep(bytes calldata performData) external override {
        //calls an oracle to retrieve the result of the match

    }
}

The contract is set with a time lock through a timestamp received in the constructor. We can assume this represents the date and time after the game finishes, and we know who the winner is.

 uint public matchTimestamp;

    constructor(uint _matchTimestamp) {
      matchTimestamp = _matchTimestamp;
    }

The checkUpkeep method compares the current block timestamp with the timestamp assigned to the contract and returns a value indicating if the condition is satisfied or not.

function checkUpkeep(bytes calldata checkData)  external view override returns (bool upkeepNeeded, bytes memory performData) {
         upkeepNeeded = (block.timestamp > matchTimestamp);
    }

Finally, the performUpkeep emulates a call to an Oracle in Chainlink that might hit an API to return the game's result.

function performUpkeep(bytes calldata performData) external override {
        //calls an oracle to retrieve the result of the match

}