Automate ERC20 Token Balance Maintenance Using A Forta Bot and Defender Autotask

This guide serves as an A to Z walkthrough for integrating a custom Forta bot with Defender. You’ll connect that bot to Defender via a Sentinel. Whenever the bot fires an alert, the Sentinel will send you a notification and trigger an Autotask to run custom logic to send a transaction (via Relayer) to automatically top up the monitored account. In this example, we’ll be monitoring for LINK on the Polygon network, although any ERC20 token can easily be substituted.

Component diagram

Install Dependencies

Although this guide makes use of the defender-client packages to create a Relayer, Sentinel, and Autotask, the very same functionality is also available via the Defender web interface.

You’ll need to install the relevant Defender NPM packages. Note that Forta bot creation also requires that you also have Docker installed.

$ mkdir minimum-balance && cd minimum-balance
$ npm init -y
$ npm i --save-dev @openzeppelin/defender-relay-client @openzeppelin/defender-autotask-client @openzeppelin/defender-sentinel-client dotenv

Create Relayer

From the Defender web interface, open the hamburger menu at the top right. Grab your Team API key and secret and save them to your local .env file.

Using @openzeppelin/defender-relay-client, create a new Relayer on the Polygon network and save the Relayer’s ID to your .env file.

const { RelayClient } = require('@openzeppelin/defender-relay-client');
const { appendFileSync } = require('fs');

async function run() {
  require('dotenv').config();
  const { API_KEY: apiKey, API_SECRET: apiSecret } = process.env;
  const relayClient = new RelayClient({ apiKey, apiSecret });

  // create relay using defender client
  const requestParams = {
    name: 'LINK Low Balance Relayer',
    network: 'matic',
    minBalance: BigInt(1e17).toString(),
  };
  const relayer = await relayClient.create(requestParams);

  // store relayer info in file (optional)
  appendFileSync('.env', relayer.relayerId)
  console.log('Relayer created: ', relayer);
}

run().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});

Save the file and run the script. Having the Relayer’s ID gives you all that you need to run transactions via an Autotask.

Create Autotask

Next, you’ll need to create an Autotask that makes use of ethers.js to transfer LINK from the integrated Relayer to the account you would like to be monitored by the Forta bot.

$ mkdir autotasks && touch autotasks/index.js

In the index.js file, include logic to run the transfer function:

// ...
async function handler(event) {
  const provider = new DefenderRelayProvider(event)
  const signer = new DefenderRelaySigner(event, provider, { speed: 'fast' })

  const contract = new ethers.Contract(LINK_CONTRACT, ABI, signer)
  const amount = ethers.utils.parseUnits("1.0", 18);
  const tx = await contract.connect(signer).transfer(MONITORED_ADDRESS, amount);
}

// ...

The Autotask is able to connect to the Relayer by specifying its ID during Autotask creation. Credential passing is handled automatically and securely.

Using @openzeppelin/defender-autotask-client, write a script in a separate file that creates a new Autotask and uploads the code from autotasks/index.js:

const { AutotaskClient } = require('@openzeppelin/defender-autotask-client')
const { appendFileSync } = require('fs')

async function main() {
  require('dotenv').config()
  const credentials = {
    apiKey: process.env.API_KEY,
    apiSecret: process.env.API_SECRET,
  }
  const autotaskClient = new AutotaskClient(credentials)

  const params = {
    name: 'LINK low balance transfer',
    encodedZippedCode: await autotaskClient.getEncodedZippedCodeFromFolder(
      './autotasks'
    ),
    trigger: {
      type: 'webhook',
    },
    paused: false,
    relayerId: process.env.RELAYER_ID,
  }

  const createdAutotask = await autotaskClient.create(params)
  console.log('Created Autotask with ID: ', createdAutotask.autotaskId)

  appendFileSync('.env', `\nAUTOTASK_ID="${createdAutotask.autotaskId}"`)
}

if (require.main === module) {
  main()
    .then(() => process.exit(0))
    .catch((error) => {
      console.error(error)
      process.exit(1)
    })
}

Save the script and run it.

Now it’s time to build the Forta bot.

Install Forta CLI

In this demo, you’ll use the command line package to work with Forta bot development.

$ mkdir forta-bot && cd forta-bot $ npx forta-agent@latest init --typescript

A keyfile will be generated in ~/.forta that you’ll encrypt with a password.

Create Bot

First, the bignumber package needs to be installed:

$ npm install --save-dev bignumber

In the /src directory, open the agent.ts file, replacing the starter code.

Export a handler method that checks whether the account balance has fallen below 0.1 LINK:

import BigNumber from 'bignumber.js'
import {
  BlockEvent,
  Finding,
  HandleBlock,
  FindingSeverity,
  FindingType,
  getEthersProvider,
  ethers
} from 'forta-agent'

export const ABI = `[ { "constant": true, "inputs": [ { "name": "_owner", "type": "address" } ], "name": "balanceOf", "outputs": [ { "name": "balance", "type": "uint256" } ], "payable": false, "type": "function" } ]`
export const ACCOUNT = "[Your Account Address]" // The account you'd like to monitor
export const MIN_BALANCE = "100000000000000000" // 0.1 LINK
export const LINK = "0xb0897686c545045afc77cf20ec7a532e3120e0f1" //  LINK address on polygon

const ethersProvider = getEthersProvider()

function provideHandleBlock(ethersProvider: ethers.providers.JsonRpcProvider): HandleBlock {
  return async function handleBlock(blockEvent: BlockEvent) {
    // report finding if specified account balance falls below threshold
    const findings: Finding[] = []

    const erc20Contract = new ethers.Contract(LINK, ABI, ethersProvider)
    const accountBalance = new BigNumber((await erc20Contract.balanceOf(ACCOUNT, {blockTag:blockEvent.blockNumber})).toString())

    if (accountBalance.isGreaterThanOrEqualTo(MIN_BALANCE)) return findings

    findings.push(
      Finding.fromObject({
        name: "Minimum Account Balance",
        description: `Account balance (${accountBalance.toString()}) below threshold (${MIN_BALANCE})`,
        alertId: "FORTA-6",
        severity: FindingSeverity.Info,
        type: FindingType.Suspicious,
        metadata: {
          balance: accountBalance.toString()
        }
      }
    ))

    return findings
  }
}

export default {
  provideHandleBlock,
  handleBlock: provideHandleBlock(ethersProvider)
}

Edit package.json, giving your bot a unique name (in lowercase) and description, specifying the chainId.

{
  "name": "minimum-link-balance-polygon-example",
  "version": "0.0.1",
  "description": "Forta bot that reports whether an account has fallen below .1 LINK balance",
  "chainIds": [137],
  // ...

You can witness the bot’s functionality using live blockchain data by running it locally, ensuring that you specify an account in the code with no LINK.

$ npx hardhat forta:run

Deploy Bot

Bot deployment can happen via the CLI, the app, or the Hardhat plugin.

Keep in mind that the account you’re deploying it from needs to be funded with some MATIC.

$ npm run publish

This will build the agent image and push it to the remote repository. After entering the password you created when installing forta-agent, you’ll be given the agent ID and manifest.

❯ npm run publish

> minimum-link-balance-polygon-example@0.0.1 publish
> forta-agent publish

building agent image...
pushing agent image to repository...
✔ Enter password to decrypt keyfile UTC--2022-08-26T21:52:34.343Z--3c89fa18f6cb70585b5831970e6b0c067ae46598 … ********
pushing agent documentation to IPFS...
pushing agent manifest to IPFS...
adding agent to registry...
successfully added agent id 0xd6d29c1584801d5baa867c9edaf595e794be63d207758155f28bed8ffa98d472 with manifest QmSNSaNwbjcvi2SuX73pqzEUcTzb4zdXpjPRbiCzsBLKuo

Congratulations on deploying a Forta bot!

For convenience, save the agent ID to the .env file in your main project folder. You’ll need it when creating a Sentinel that subscribes to this bot.

Create Forta Sentinel

Using the sentinel-client package, write a script that creates a Forta Sentinel connected to your Relayer and Autotask.

require('dotenv').config()
const { SentinelClient } = require('@openzeppelin/defender-sentinel-client')

const BOT = process.env.BOT_ID

async function main() {
  require('dotenv').config()
  const client = new SentinelClient({
    apiKey: process.env.API_KEY,
    apiSecret: process.env.API_SECRET,
  })

  const notificationChannels = await client.listNotificationChannels();
  const { notificationId, type } = notificationChannels[0];

  const requestParams = {
    type: 'FORTA',
    name: 'Low balance alert - trigger refill',
    agentIDs: [BOT],
    fortaConditions: {
      minimumScannerCount: 2,
      severity: 1, // (unknown=0, info=1, low=2, medium=3, high=4, critical=5)
    },
    autotaskTrigger: process.env.AUTOTASK_ID,
    alertTimeoutMs: 120000,
    notificationChannels: [notificationChannels[0].notificationId],
  }

  const newSentinel = await client.create(requestParams)
  console.log(newSentinel)
}

main().catch((error) => {
  console.error(error)
  process.exitCode = 1
})

The Sentinel is configured to trigger a notification as well as an Autotask when the bot sends an alert. To prevent being triggered multiple times for the same low balance event, the alertTimeoutMs has been set.

Run the script to create the Sentinel.

Congratulations! You can now experiment with this integration further by transfering LINK from the monitored account so that the balance drops below 0.1. Detecting this, the Forta bot will fire, causing the Sentinel to trigger the Autotask which runs the transfer function on the Relayer, refilling the monitored account.