Forta for real-time monitoring and security of your smart contract

Table of contents

  • Introduction
  • Prerequisites
  • What are we building?
  • Building the Forta bot:
    • Setting up the dev environment.
    • Configure your bot to the Mumbai network.
    • Network manager: specify the smart contract addresses for different networks we want to monitor.
    • Emit custom findings through your Forta Bot.
  • Starting up our bot.
  • Conclusion.

Introduction

Smart contracts are autonomous pieces of code that exist on a blockchain. As such, anyone on the blockchain could interact with your smart contract if the chain itself is up and running. This makes it essential to keep a steady pair of eyes on your contract, especially if you are dealing with large sums of money as is the case with DeFi smart contracts for example.
Forta is an open-source project that helps you in writing bots to keep track of your smart contracts. You can configure the bot to your requirements and track your smart contract for the exact activities that you may want to look out for.

Prerequisites

  • Solidity basics to understand the basics of how events and payable functions work. You can refer solidity-by-example for a quick overview.
  • TypeScript — we will be using TypeScript to build our bot. If you need to install TypeScript in your system, refer to the official website here.
  • ethers.js — you need to understand the concept of providers in ethers.js.

What are we building?

We have a Deposit.sol smart contract deployed on the Mumbai testnet. Any account can deposit MATIC on it. Our Forta bot will:

  • Trigger a critical alert if any blacklisted addresses deposit ether into our smart contract.
  • Trigger an info alert if any random addresses deposit ether into our smart contract.
  • Detect if a deposit is less than 0.01MATIC: this will trigger a suspicious activity alert.

Deposit.sol has a single payable function. The contract will emit an event to signify the amount of ether deposited into the contract whenever some ether is sent to it.
This is the smart contract code:

pragma solidity =0.8.14;
contract Deposit{ 
     event Deposited(uint256 amount); 
     function deposit() external payable{      
       emit Deposited(msg.value); 
     } 
}

This contract is already deployed and verified at the Mumbai network. So you can focus only on the bot building!

Building the Forta Bot

Building a Forta Bot requires the following steps:

1. Setting up the dev environment.

2. Configure your bot to the Mumbai network.

3. Network manager: specify the smart contract addresses for different networks we want to monitor.

4. Emit custom findings through your Forta Bot.

Setting up our dev environment

Create a new folder by the name of deposit-detector. Open the folder in VS code (or any code editor you like), and run the command:

npm init -y
  • This command creates an empty package.json file that contains a list of the packages we install in our directory. Now run the following command to initiate a basic TypeScript project using the forta-agent CLI tool:
npx [email protected] init --typescript

We recommend using npm instead of yarn when setting up the project to avoid some known issues that can arise when deploying the bot.

This is what your terminal should look like:

  • Type yes through all prompts, and set a new password for your detector. npm will now download all the required packages to set up a basic Forta bot project.

After installation, your directory should look something like this:

That’s a ready template of a working bot. Run the npm start command in your terminal to check if everything was installed correctly. The bot should start emitting findings in all transfer events on the Tether token deployed on the Ethereum blockchain.

So how does a Forta Bot really listen to transactions and blocks on a blockchain in real time?

The forta-agent CLI creates an agent.ts file inside the src folder by default. The file contains a simple Forta bot that listens to transactions on the Tether token as described previously. Go to src/agent.ts . It is the main file of the bot where you can add your bot logic. You will find two exports functions:

export default {
handleTransaction,
handleBlock
};

Here is a brief overview of both of these functions:

  • handleTransaction: This function is called each time a new transaction has been submitted to our contract. handleTransaction receives a newly generated TransactionEvent object, which contains all the events that occurred inside that particular transaction. You can check all TransactionEvent properties from the official documentation here.
  • handleBlock: handleBlock is called each time a new block is minted on the blockchain. handleBlock receives a BlockEvent object which contains all transactions that occurred inside a block. You can take a look at all the BlockEvent properties from the documentation.

We need the bot to detect deposits inside the Deposit.sol smart contract. So we really only need the handleTransaction function.

Before getting down to the code, let’s think about the remaining steps to build a bot:

  1. Configuring our Forta Bot to a blockchain: Which blockchain network do we want to use? For this particular article, we will be configuring our bot to monitor a smart contract deployed on the Mumbai testnet.
  2. Network manager: we need to specify the addresses for all the smart contracts we want to monitor.
  3. Emit findings (send notification): decide which events we want to listen to. In our case, it is only a Deposited event:
    • If the Deposited event is called.
    • If the deposited amount is less than 0.01.
    • If the deposited address is blacklisted.

This was a brief outline of what we need to keep in mind while building a Forta Bot. Let us now move ahead to actually implementing these steps into our project.

Configure your Forta bot to the Mumbai testnet

The Forta bot currently is listening to the events of a single contract on the Ethereum mainnet. To configure the bot to listen to the Mumbai testnet:

  • First, go to package.json, and inside the chainIds object, replace 1 (which is the Ethereum Mainnet’s chain ID ) with “80001”, (Mumbai network’s chain ID).
"chainIds": [
   80001
],
  • Now go to the forta.config.json file. The file contains configuration parameters for your bot. Inside the file, replace the existing code with this:
{
"jsonRpcUrl": "<Your Chainstack RPC URL>"
}

This configuration will allow forta-agent to use your RPC URL to connect to any blockchain you want. The chain IDs and RPC URLs must be set up correctly to avoid conflicts while connecting to the blockchain.

You can use a public RPC URL, or alternatively, you could use a high-speed, reliable RPC connection to the blockchain by creating a node on Chainstack. If you are not familiar with setting up a node on Chainstack, feel free to check out this guide on our blog.

Now you have your bot listening for events on the Mumbai network. To test this out run npm start in the terminal.

You should have an output that looks like this:

If you searched for any transaction hash, you should be able to find it on Mumbai testnet explorer.

So what’s happening here? Our bot is looking for events emitted from the Tether token contract. But that contract was deployed on the Ethereum mainnet. Hence, looking for events on that contract address on the Mumbai testnet does not yield anything. You can however check for the block numbers being returned in the terminal. These block numbers should correspond to the latest blocks being published on the Mumbai testnet explorer website.
Stop the bot currently running in your terminal by pressing Ctrl+C (on Linux). Let us now add our own smart contract to the code.

Network manager: specify the smart contract addresses for different networks we want to monitor

The network manager has two main purposes. The first one is to map data to its equivalent blockchain network. So if we have a contract address 0xa… deployed on Ethereum and a contract address 0x0b… deployed on Polygon, you can monitor both addresses without changing the code.

  • Create a file named network.ts inside the src folder. Inside the file, add this code:
export interface NetworkData {
  address: string; // The smart contract address
  minimumDepositAmount: number; 
  blacklistedAddresses: string[];
}
export const networkData: Record <number, NetworkData> = {
  80001: {
    address: "0xa87db9fe057cff6e296586bec6a6982a5a9b44b0",
    minimumDepositAmount: 0.01,
    blacklistedAddresses: [
      "0x7c71a3d85a8d620eeab9339cce776ddc14a8129c",
      "0x17156c0cf9701b09114cb3619d9f3fd937caa3a8",
    ],
  },
};

In this file, we are exporting networkData which basically stores our contract address and an array of blacklisted addresses that will trigger an alert if they send us any ether.
The second purpose of the network manager is to initialize the provider object for the network connection (if you don’t know what a network provider is, check out Ethers documentation).

npm i forta-agent-tools
  • Now go to src/agent.ts and delete everything inside it.

Import NetworkManager from the installed package:

import { NetworkManager } from "forta-agent-tools";
  • Now create a new networkManager object with NetworkData by importing the network.ts file:
import { networkData, NetworkData } from "./network";
const networkManager = new NetworkManager(networkData); //global var
  • To add the provider, we need to call networkManager.init() function, but we need this to be called only one time when the bot is initialized. Forta has a built-in function to initialize. First import ethers from “forta-agent”:
import ethers from "forta-agent";
  • Now add the following code inside the agent.ts file:
export const provideInitialize = (
  provider: ethers.providers.JsonRpcProvider
) => {
   // should return a function that will be used by the Bot
  return async () => {
    await networkManager.init(provider);
  };
};
  • Finally, we will export provideInitialize():
export default {
  initialize: provideInitialize(utils.provider), // The utils object will be added in the next section
};

Your agent.ts should now look something like this:

import ethers from "forta-agent";
import { NetworkManager } from "forta-agent-tools";
import {NetworkData, networkData } from "./network";
const networkManager = new NetworkManager(networkData);
export const provideInitialize = (
  provider: ethers.providers.JsonRpcProvider
) => {
   // should return a function that will be used by the Bot
  return async () => {
    await networkManager.init(provider);
  };
};
export default {
  initialize: provideInitialize(utils.provider), // The utils object will be added on the next section
};
  • Inside the bot, we have the network manager and all the data that we need. The only missing thing is the handleTransaction function and the utils object. Let us start with the utils object.
    Create a new file inside the src folder with the name utils.ts.
  • Add the provider from forta-agent inside the utils file. We will export the provider object from the utils file:
import { getEthersProvider } from "forta-agent";
const provider = getEthersProvider();
  • Let us now add the event ABI to our utils file. We need the event ABI to monitor our payable function whenever it’s included event is triggered:
import { Interface } from "@ethersproject/abi";
const EVENT_ABI: string[] = ["event Deposited(uint256 amount)"];
const EVENTS_IFACE: Interface = new Interface(EVENT_ABI);

Emit custom findings through your Forta bot

A Forta bot creates findings that flag transactions and blocks that meet the required conditions. A finding mainly contains the types and severity of the flagged block/transaction. Forta gives us 4 types and 5 severities to configure our bot’s findings:

  • Finding types: Exploit, Suspicious, Degraded, Info.
  • Finding severity: Info, Low, Medium, High, Critical.

In our bot, we will use the following types: Info, Suspicious, and Exploit, and the following severities: Low, Medium, and Critical.

Let’s create the interface for our custom finding function.

  • We need to first make some additional imports from “forta-agent”. On the top of your utils file, replace the first import with this:
import {
  Finding,
  FindingSeverity,
  FindingType,
  getEthersProvider,
} from "forta-agent";
  • Next, let us create an interface for our finding function:
interface FindingParams {
  // deposit, minimum deposit and blacklisted address
  alertId: "001" | "002" | "003"; 
  account: string;
  depositedAmount: string;
  description: string;
  severity: FindingSeverity;
  type: FindingType;
}
  • Finally, let us create a finding function named createFinding. The createFinding function is just an object initializer.
const createFinding = ({
  alertId,
  account,
  depositedAmount,
  description,
  severity,
  type,
}: FindingParams): Finding => {
  return Finding.fromObject({
    name: "Detects all deposit transactions",
    description,
    alertId: `deposit-${alertId}`,
    severity,
    type,
    protocol: "Depositor",
    metadata: {
      account,
      depositedAmount,
    },
  });

  export default {
      provider,
      EVENT_ABI,
      EVENTS_IFACE,
      createFinding, // add create finding
  };
};

The utils the file is ready. It should look something like this:

import {
    Finding,
    FindingSeverity,
    FindingType,
    getEthersProvider,
  } from "forta-agent";
const provider = getEthersProvider();
import { Interface } from "@ethersproject/abi";
const EVENT_ABI: string[] = ["event Deposited(uint256 amount)"];
const EVENTS_IFACE: Interface = new Interface(EVENT_ABI);
interface FindingParams {
    // deposit, minimum deposit and blacklisted address
    alertId: "001" | "002" | "003"; 
    account: string;
    depositedAmount: string;
    description: string;
    severity: FindingSeverity;
    type: FindingType;
  }
  const createFinding = ({
    alertId,
    account,
    depositedAmount,
    description,
    severity,
    type,
  }: FindingParams): Finding => {
    return Finding.fromObject({
      name: "Detects all deposit transactions",
      description,
      alertId: `deposit-${alertId}`,
      severity,
      type,
      protocol: "Depositor",
      metadata: {
        account,
        depositedAmount,
      },
    });
};
  
  export default {
        provider,
        EVENT_ABI,
        EVENTS_IFACE,
        createFinding, 
    };
  • The last step to complete our bot is to implement the handleTransaction function. Save your utils file and open agent.ts.
    Below the provideInitialize function, add the provideHandleTransaction function by pasting this code:
export const provideHandleTransaction =
  (networkManager: NetworkManager<NetworkData>): HandleTransaction =>
  async (txEvent: TransactionEvent) => {
    const findings: Finding[] = [];
    const depositLogs = txEvent.filterLog(
      utils.EVENT_ABI,
      networkManager.get("address")
    );
    depositLogs.forEach((log: LogDescription) => {
      const amount = ethers.utils.formatEther(log.args.amount);
      // Emit finding for new deposit
      findings.push(
        utils.createFinding({
          alertId: "001",
          account: txEvent.transaction.from,
          depositedAmount: amount,
          description: "New deposit!",
          severity: FindingSeverity.Low,
          type: FindingType.Info,
        })
      );
      // Emit finding if deposit less than the minimum amount
      if (Number(amount) < networkManager.get("minimumDepositAmount")) {
        findings.push(
          utils.createFinding({
            alertId: "002",
            account: txEvent.transaction.from,
            depositedAmount: amount,
            description: "Someone deposited a very small amount",
            severity: FindingSeverity.Medium,
            type: FindingType.Suspicious,
          })
        );
      }
      // Emit finding for blacklisted addresses
      if (
        networkManager
          .get("blacklistedAddresses")
          .includes(txEvent.transaction.from)
      ) {
        findings.push(
          utils.createFinding({
            alertId: "003",
            account: txEvent.transaction.from,
            depositedAmount: amount,
            description: "Blacklisted addresses deposit!",
            severity: FindingSeverity.Critical,
            type: FindingType.Exploit,
          })
        );
      }
    });
    return findings;
  };

Let us go over this function briefly.

Whenever a transaction sends ether to our contract, a new event is emitted, and Forta Bot returns an array of findings to us. Based on our required conditions, we can classify the events into different types and severities.
If our contract for example receives a rather small amount of ether as a deposit, we may want to flag the transaction as suspicious. We are basically emitting the findings based on the fulfillment of certain conditions.

  • We now need to export this function as a handler to Forta Bot. Go to the bottom of agent.ts, and change the file export to this:
export default {
  initialize: provideInitialize(utils.provider),
  handleTransaction: provideHandleTransaction(networkManager),
};
  • You will probably still be seeing a bunch of errors in the agent.ts file at this point. This is because we still need to make some imports. Go to the top of the file and import additional modules from “forta-agent” with this:
import {ethers,
  Finding,
  FindingSeverity,
  FindingType,
  HandleTransaction,
  LogDescription,
  TransactionEvent,
} from "forta-agent";
  • and import the utils file into agent.ts:
import utils from "./utils";

Starting up our bot

The default project created by forta-agent CLI includes an agent.spec file that is used for testing our bot. We won’t be testing our bot this time, so you can just delete the file.

  • And that’s it. Now your bot is ready to use. To start up your bot inside the terminal, run:
npm start

This command will start the bot. Try going to the smart contract and depositing a small amount of MATIC tokens. See what findings you get back from your bot.

  • You can also ask your bot to listen to a specific transaction using this command:
npm run tx <transaction hash>
  • Or to listen to all transactions that happened on a block by using this command:
npm run block <block number>

You can get the values of the transaction hash or the block number from the readme file. And that’s it for the bot development. Now your bot is ready to use.

But in most cases, bots may be more complicated than this, so I recommend you always create a unit test with positive and negative scenarios for your bot.

Conclusion

In this article, you learned about the Forta network, and how to use the forta-agent CLI to build your own bot that can monitor threats and suspicious activities on your smart contracts in real time.
Forta-agent is a very powerful tool, and you can learn more about it from the official documentation.

Have you already explored what you can achieve with Chainstack? Get started for free today.

Yehia is a full-stack blockchain engineer at Nethermind. He likes to write technical articles as a hobby, and is always looking for new technologies and open-source projects to learn from!

Hyperledger Global Forum 2020 Recap

ConsenSys, Microsoft, and EY launch the Baseline protocol. ScanTrust and Unilever provide end-to-end traceability for millions of units. NTT Data, Hitachi, and Accenture move their use cases to production.

Ashlie Chin
Mar 20

Querying full and archive Ethereum nodes with JavaScript

Whenever we need to query data from the blockchain, we fetch it from a node. An archive node differs from a full node since the first holds the entire history of network transactions. Thus, some queries for older block transactions cannot be fetched easily on a full node. In this tutorial, we will programmatically fetch data from the blockchain, switching between full and archive nodes when necessary.

Bastian Simpertigue
Aug 24
Chainstack uses cookies to provide you with a secure and
personalized experience on its website. Learn more.