Forta for real-time monitoring and security of your smart contract
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 forta-agent@latest 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 generatedTransactionEvent
object, which contains all the events that occurred inside that particular transaction. You can check allTransactionEvent
properties from the official documentation here.
handleBlock
:handleBlock
is called each time a new block is minted on the blockchain.handleBlock
receives aBlockEvent
object which contains all transactions that occurred inside a block. You can take a look at all theBlockEvent
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:
- 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.
- Network manager: we need to specify the addresses for all the smart contracts we want to monitor.
- 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 thechainIds
object, replace1
(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 thesrc
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).
- To do so, we need to install forta-agent-tools. Open the terminal and run:
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 withNetworkData
by importing thenetwork.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 thesrc
folder with the nameutils.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
. ThecreateFinding
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 openagent.ts
.
Below theprovideInitialize
function, add theprovideHandleTransaction
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.
- Discover how you can save thousands in infra costs every month with our unbeatable pricing on the most complete Web3 development platform.
- Input your workload and see how affordable Chainstack is compared to other RPC providers.
- Connect to Ethereum, Solana, BNB Smart Chain, Polygon, Arbitrum, Base, Optimism, Avalanche, TON, Ronin, zkSync Era, Starknet, Scroll, Aptos, Fantom, Cronos, Gnosis Chain, Klaytn, Moonbeam, Celo, Aurora, Oasis Sapphire, Polygon zkEVM, Bitcoin and Harmony mainnet or testnets through an interface designed to help you get the job done.
- To learn more about Chainstack, visit our Developer Portal or join our Discord server and Telegram group.
- Are you in need of testnet tokens? Request some from our faucets. Multi-chain faucet, Sepolia faucet, Holesky faucet, BNB faucet, zkSync faucet, Scroll faucet.
Have you already explored what you can achieve with Chainstack? Get started for free today.