forta-agent-tools v3.2.12
General Agent Module
Description
This module contains some common approaches for building Forta Bots. You will also find some tools for writing tests for these bots. These approaches can be composed for creating more complex bots or used only for checking without returning their findings.
Installation
- Using npm:
npm i forta-agent-tools
or
- Clone the repo
- npm install
- npm run test
Handlers
Handlers are approaches for dealing with block and transaction events. They can either be integrated into a bot's logic to make it easier to get specific data based on transactions or blocks or used like Forta bot generators through.
Each handler gets specific data, which is called metadata (related to what would be relevant for the alert metadata),
from a transaction or block event. This data can be returned to be processed externally by calling
handler.metadata(event), but the handler can also receive a callback that creates a finding from its metadata. In
this case, it can both return findings through handler.handle(event) and also generate the Forta bot handlers through
handler.getHandleBlock() and handler.getHandleTransaction().
Handler is an abstract base class and each specific handler extends it. The common interface is:
Handler(options): Each handler has a specific set of options. The only common field between all the specific options is the optionalonFinding, that defines how a finding will be generated based on the metadata.Handler, in this case, is a specific handler, not theHandlerbase class.metadata(event): This method returns a promise to an array of metadata objects related to a transaction or block or, if there's no implementation for a specific event (e.g. a handler only gets information from transactions, not blocks) it will return a promise that resolves tonull.handle(event, onFinding?): This method handles a transaction or block event and returns a list of findings.onFindingwill overrideoptions.onFindingif both were specified.getHandleBlock(onFinding?): This method returns aforta-agentHandleBlockhandle callback.onFindingwill overrideoptions.onFindingif both were specified.getHandleTransaction(onFinding?): This method returns aforta-agentHandleTransactionhandle callback.onFindingwill overrideoptions.onFindingif both were specified.
Each handler's options and metadata interfaces can be accessed through Handler.Options and Handler.Metadata
(Handler, again, in this case, being a specific handler, not the Handler base class).
BlacklistedAddresses
This handler detects transactions that contain addresses from a list provided by the user.
How to use it
import { Finding, FindingSeverity, FindingType, TransactionEvent } from "forta-agent";
import { handlers, createAddress } from "forta-agent-tools";
const blacklistedAddressesHandler = new handlers.BlacklistedAddresses({
addresses: [createAddress("0x0")],
onFinding(metadata) {
return Finding.from({
name: "Blacklisted Address",
description: "A transaction involving a blacklisted address was found",
severity: FindingSeverity.Info,
type: FindingType.Info,
metadata: {},
addresses: metadata.addresses,
});
},
});
const handleTransaction = blacklistedAddressesHandler.getHandleTransaction();
const handleBlock = blacklistedAddressesHandler.getHandleBlock();
// or
async function handleTransaction(txEvent: TransactionEvent): Promise<Finding[]> {
const findings = await blacklistedAddressesHandler.handle(txEvent);
// or
const metadataList = await blacklistedAddressesHandler.metadata(txEvent);
const findingsFromMetadata = (metadataList || []).map((metadata) => {
return Finding.from({
name: "Blacklisted Address",
description: "A transaction involving a blacklisted address was found",
severity: FindingSeverity.Info,
type: FindingType.Info,
metadata: {},
addresses: metadata.addresses,
});
});
// ...
}Options
addresses: Blacklisted addresses. A finding should be generated if any of them is involved in a transaction.
Metadata
addresses: Blacklisted addresses that were involved in a transaction.
Erc20Transfers
This handler detects ERC20 token transfers in transactions.
How to use it
import { Finding, FindingSeverity, FindingType, TransactionEvent } from "forta-agent";
import { handlers, createAddress } from "forta-agent-tools";
const erc20TransfersHandler = new handlers.Erc20Transfers({
emitter: createAddress("0x0"),
from: createAddress("0x1"),
to: createAddress("0x2"),
amountThreshold: "10000", // or (amount) => amount.gte("100")
onFinding(metadata) {
return Finding.from({
name: "Large ERC20 transfer",
description: "A large ERC20 transfer was detected",
severity: FindingSeverity.Info,
type: FindingType.Info,
metadata: {
from: metadata.from,
to: metadata.to,
amount: metadata.amount.toString(),
},
});
},
});
const handleTransaction = erc20TransfersHandler.getHandleTransaction();
const handleBlock = erc20TransfersHandler.getHandleBlock();
// or
async function handleTransaction(txEvent: TransactionEvent): Promise<Finding[]> {
const findings = await erc20TransfersHandler.handle(txEvent);
// or
const metadataList = await erc20TransfersHandler.metadata(txEvent);
const findingsFromMetadata = (metadataList || []).map((metadata) => {
return Finding.from({
name: "Large ERC20 transfer",
description: "A large ERC20 transfer was detected",
severity: FindingSeverity.Info,
type: FindingType.Info,
metadata: {
token: metadata.emitter,
from: metadata.from,
to: metadata.to,
amount: metadata.amount.toString(),
},
});
});
// ...
}Options
emitter: Token address, emitter of theTransferevents that will be listened.from: Transfer sender.to: Transfer receiver.amountThreshold: Determines a filter based on the transfer amount. Can be either a value, like"1000"(doesn't consider the token's decimal places, sameuint256representation as in the contract), case in which the transfer event will be filtered out when it amount less than that value, or a callback that defines whether the amount should lead to a finding or not.
Metadata
emitter: Token address.from: Transfer sender.to: Transfer receiver.amount: Transfer amount.
EthTransfers
This handler detects ether transfers in transactions.
How to use it
import { Finding, FindingSeverity, FindingType, TransactionEvent } from "forta-agent";
import { handlers, createAddress } from "forta-agent-tools";
const ethTransfersHandler = new handlers.EthTransfers({
from: createAddress("0x0"),
to: createAddress("0x1"),
valueThreshold: "10000", // or (value) => value.gte("100")
onFinding(metadata) {
return Finding.from({
name: "Large ether transfer",
description: "A large ether transfer was detected",
severity: FindingSeverity.Info,
type: FindingType.Info,
metadata: {
from: metadata.from,
to: metadata.to,
value: metadata.value.toString(),
},
});
},
});
const handleTransaction = ethTransfersHandler.getHandleTransaction();
const handleBlock = ethTransfersHandler.getHandleBlock();
// or
async function handleTransaction(txEvent: TransactionEvent): Promise<Finding[]> {
const findings = await ethTransfersHandler.handle(txEvent);
// or
const metadataList = await ethTransfersHandler.metadata(txEvent);
const findingsFromMetadata = (metadataList || []).map((metadata) => {
return Finding.from({
name: "Large ether transfer",
description: "A large ether transfer was detected",
severity: FindingSeverity.Info,
type: FindingType.Info,
metadata: {
from: metadata.from,
to: metadata.to,
value: metadata.value.toString(),
},
});
});
// ...
}Options
from: Transfer sender.to: Transfer receiver.valueThreshold: Determines a filter based on the transfer amount. Can be either a value, like"1000"(in wei), case in which the transfer event will be filtered out when it amount less than that value, or a callback that defines whether the amount should lead to a finding or not.
Metadata
from: Transfer sender.to: Transfer receiver.value: Transferred value in wei.
TraceCalls
This handler parses and detects specific calls in transactions traces.
How to use it
import { Finding, FindingSeverity, FindingType, TransactionEvent } from "forta-agent";
import { handlers, createAddress } from "forta-agent-tools";
const traceCallsHandler = new handlers.TraceCalls({
signatures: ["function func(uint256 param) returns (uint256 resp)"],
from: createAddress("0x0"),
to: createAddress("0x1"),
includeErrors: false,
filterByArguments(args) {
return args.param.eq(0);
},
filterByOutput(output) {
return output.resp.eq(1);
},
filter(metadata) {
return metadata.trace.traceAddress.length === 1;
},
onFinding(metadata) {
return Finding.from({
name: "Func called in traces",
description: "A func call was detected in the transaction's traces",
severity: FindingSeverity.Info,
type: FindingType.Info,
metadata: {
from: metadata.from,
to: metadata.to,
error: metadata.error,
param: metadata.args.param.toString(),
resp: metadata.output.resp.toString(),
},
});
},
});
const handleTransaction = traceCallsHandler.getHandleTransaction();
const handleBlock = traceCallsHandler.getHandleBlock();
// or
async function handleTransaction(txEvent: TransactionEvent): Promise<Finding[]> {
const findings = await traceCallsHandler.handle(txEvent);
// or
const metadataList = await traceCallsHandler.metadata(txEvent);
const findingsFromMetadata = (metadataList || []).map((metadata) => {
return Finding.from({
name: "Func called in traces",
description: "A func call was detected in the transaction's traces",
severity: FindingSeverity.Info,
type: FindingType.Info,
metadata: {
from: metadata.from,
to: metadata.to,
error: metadata.error,
param: metadata.args.param.toString(),
resp: metadata.output.resp.toString(),
},
});
});
// ...
}Options
signatures: Function signatures to be monitored. Also used in decoding.from: Call sender.to: Call receiver.includeErrors: Whether calls that had an error should be included or not (by default, falsy).filterByArguments: Callback (same signature as aarray.filter(cb)callback) to filter calls by arguments.filterByOutput: Callback (same signature as aarray.filter(cb)callback) to filter calls by returned values.filter: Callback (same signature as aarray.filter(cb)callback) to filter calls by metadata.
Metadata
from: Call sender.to: Call receiver.trace: Trace object.error: Whether there was an error during the call or not.output: Call result. Will benulliferroristrue.
As well as ethers.utils.TransactionDescription's fields:
functionFragment: Function fragment from the signature.name: Function name.args: Function arguments.signature: Function signature.sighash: Function sighash.value: Transaction value in wei.
Utils
Address Handling
These are utility functions to create and manipulate addresses.
padAddress(address): Simply pads left a hex string with zeroes so it fits the expected length.createAddress(address): Pads the provided address and ensures it is lowercase.createChecksumAddress(address): Pads the provided address and ensures it is in checksum format.toChecksumAddress(address): Formats a valid address (case-insensitive) in checksum format.
TestTransactionEvent
This is a helper class for creating TransactionEvents using the fluent interface pattern.
How to use it
import { TestTransactionEvent } from "forta-agent-tools/lib/test";
const txEvent: TransactionEvent = new TestTransactionEvent().setFrom(address1).setTo(address2);There are multiple methods you can use for creating the exact TransactionEvent you want:
setFrom(address)This method sets thetransaction.fromfield in the event.setTo(address)This method sets thetransaction.tofield in the event.setGas(value)This method sets thetransaction.gasfield in the event.setGasPrice(value)This method sets thetransaction.gasPricefield in the event.setValue(value)This method sets thetransaction.valuefield in the event.setData(data)This method sets thetransaction.datafield in the event.setGasUsed(value)This method sets thereceipt.gasUsedfield in the event.setTimestamp(timestamp)This method sets theblock.timestampfield in the event.setBlock(block)This method sets theblock.numberfield in the event.addEventLog(eventSignature, address, inputs)This method adds a log to thereceipt.logsfield. The only mandatory argument is theeventSignature.addressargument is the zero address by default,inputscorresponds to the event arguments values, it is an empty list by default.The
keccak256hash of the signature is added at the beginning of thetopicslist automatically.addInvolvedAddresses(addresses)This method adds a spread list of addresses toaddressesfield.addTraces(traceProps)This method adds a list ofTraceobjects at the end oftracesfield in the event. The traces are created from thetracePropsspread list.TracePropsis a TS object with the following optional fields{ function, to, from, arguments, output, value, traceAddress }.
TestBlockEvent
This is a helper class for creating BlockEvents using the fluent interface pattern.
How to use it
import { TestBlockEvent } from "forta-agent-tools/lib/test";
const blockEvent: BlockEvent = new TestBlockEvent().setHash(blockHash).setNumber(blockNumber);There are multiple methods you can use for creating the exact BlockEvent you want:
setHash(blockHash)This method sets theblock.hashfield in the event.setParentHash(blockHash)This method sets theblock.parentHashfield in the event.setNumber(blockNumber)This method sets theblock.numberfield in the event.addTransactions(txns)This method adds the hashes of a spread list of transaction events at the end ofblock.transactionsfield in the event.addTransactionsHashes(hashes)This method adds a hashes spread list to the end ofblock.transactionsfield in the event.
TestAlertEvent
The concept of a TestAlertEvent class does not actually exist. It was not implemented because the forta-agent library provides built-in static methods that serve the purpose that TestAlertEvent would have fulfilled. Below we are providing some instructions on how to use forta-agent to create an AlertEvent you could use for testing.
Creating Alert
In the forta-agent SDK, you'll find a class called Alert, which essentially represents an Alert within the context of the AlertEvent in Forta.
- You cannot directly instantiate an
Alertobject through its constructor because it has a private constructor. Instead, there's a static method namedfromObject. - To use it, call
Alert.fromObject(alertInput: AlertInput), whereAlertInputis an interface containing all the properties anAlertcan have. Each property inAlertInputis optional, allowing you to create anAlertwith only the properties you need for your use case or testing. - For more details about
Alertand its properties, refer to the official forta-docs, and for more details aboutAlertInput's properties, refer to our implementation in alert.type.ts.
Creating AlertEvent
AlertEvent is a class with a constructor that takes a single argument of type Alert.
- You can use the
Alertobject created using the above method to obtain anAlertEventobject. - All other properties in
AlertEventare simply getters, serving as aliases to the property methods of theAlertclass. - To learn more about
AlertEventand its properties, consult the official forta-docs.
Refer to the code snippet below for an example.
Basic Usage:
import { Alert, AlertEvent, EntityType, Label } from "forta-agent";
import { AlertInput } from "../utils/alert.type";
import { createAddress, createTransactionHash } from "../utils";
let alert: Alert;
let alertEvent: AlertEvent;
let alertInput: AlertInput;
const createAlert = (alertInput: AlertInput): Alert => {
return Alert.fromObject(alertInput);
};
const getLabel = (name: string, value: string): Label => {
return Label.fromObject({
entityType: EntityType.Transaction,
entity: createTransactionHash({ to: createAddress("0x1234") }),
label: name,
confidence: 1,
metadata: { value: value },
});
};
const getAlertInput = (): AlertInput => {
let alertInput: AlertInput = {
addresses: [createAddress("0x1234"), createAddress("0x5678"), createAddress("0x9abc")],
alertId: createTransactionHash({ to: createAddress("0x1234") }),
hash: createTransactionHash({ to: createAddress("0x45678987654") }),
contracts: [
{ address: createAddress("0x1234"), name: "Contract1" },
{ address: createAddress("0x5678"), name: "Contract2" },
{ address: createAddress("0x9abc"), name: "Contract3" },
],
createdAt: "2021-01-01T00:00:00.000Z",
description: "Test Alert",
findingType: "Info",
name: "Test Alert",
protocol: "Test",
scanNodeCount: 1,
severity: "Info",
alertDocumentType: "Alert",
relatedAlerts: [createTransactionHash({ to: createAddress("0x1234") })],
chainId: 1,
labels: [getLabel("label1", "value1"), getLabel("label2", "value2")],
source: {
transactionHash: createTransactionHash({ to: createAddress("0x1234") }),
block: {
timestamp: "2021-01-01T00:00:00.000Z",
chainId: 1,
hash: createTransactionHash({ to: createAddress("0x1234") }),
number: 1,
},
bot: {
id: "botId",
reference: "botReference",
image: "botImage",
},
sourceAlert: {
hash: createTransactionHash({ to: createAddress("0x1234") }),
botId: "botId",
timestamp: "2021-01-01T00:00:00.000Z",
chainId: 1,
},
},
metadata: {
metadata1: "value1",
metadata2: "value2",
},
projects: [
{
id: "projectId",
name: "projectName",
contacts: {
securityEmailAddress: "securityEmailAddress",
generalEmailAddress: "generalEmailAddress",
},
website: "website",
token: {
symbol: "symbol",
name: "name",
decimals: 1,
chainId: 1,
address: createAddress("0x1234"),
},
social: {
twitter: "twitter",
github: "github",
everest: "everest",
coingecko: "coingecko",
},
},
],
addressBloomFilter: {
bitset: "bitset",
k: "k",
m: "m",
},
};
return alertInput;
};
alertInput = getAlertInput();
alert = createAlert(alertInput);
alertEvent = new AlertEvent(alert);runBlock
This is a helper function to simulate the execution of run block cli command when the bot has implemented a handleTransaction and a handleBlock.
How to use it
import { runBlock } from "forta-agent-tools/lib/test";
async myFunction(params) => {
...
const findings: Findings[] = await runBlock(bot, block, tx1, tx2, tx3, ..., txN);
...
};Parameters description:
bot: It is a JS object with two properties,handleTransactionandhandleBlock.block: It is theBlockEventthat the bot will handle.tx#: These are theTransactionEventobjects asociated with theBlockEventthat the bot will handle.
MockTransactionData
This is a helper class for mocking the interfaces ethers.providers.TransactionResponse and ethers.providers.TransactionReceipt by implementing them.
Since this class implements both of these interfaces, the instance of this class can be used for ethers TransactionResponse and TransactionReceipt.
The class is instantiated with default values for all the fields and has set functions to set the values for each of these fields.
Basic Usage:
import { MockTransactionData } from "forta-agent-tools/lib/test";
import { utils, BigNumber, providers } from "ethers";
const mockTransactionData: MockTransactionData = new MockTransactionData();
mockTransactionData.setValue(utils.parseEther("1.0"))
.setGasPrice(BigNumber.from(1000000))
.setGasLimit(BigNumber.from(21000))
mockTransactionData.setHash("0x1234567890987654345678987654...");
// or can generate the hash based on the current transaction config
mockTransactionData.generateHash();
const transactionResponse: Partial<providers.TransactionResponse> = { // Add the fields that you want to set for the TransactionResponse.
hash: '0x3fda39a81c47dc37d84c761c3cbbea375866c1fbdfcf91566eaa4c4ef62c70ad',
type: 2,
accessList: [],
blockHash: '0x25e44bfb2c3a47703c86884110a6d5c5a7a655b02fe1ca3a9d135e7459efab95',
blockNumber: 18095175,
transactionIndex: 109,
confirmations: 22,
from: '0xDAFEA492D9c6733ae3d56b7Ed1ADB60692c98Bc5',
gasPrice: BigNumber.from("9999762606"),
maxPriorityFeePerGas: BigNumber.from("0"),
maxFeePerGas: BigNumber.from("9999762606"),
gasLimit: BigNumber.from(0x5208),
to: '0x4675C7e5BaAFBFFbca748158bEcBA61ef3b0a263',
value: BigNumber.from("63002772804144528"),
nonce: 436110,
data: '0x',
r: '0xa62a979dd4713a8c12de05167f68ddbfab947886d44eb0806f9b4f8c0b7d4ca5',
s: '0x72058708dfe24365969eff435447a92cd9ee0348e898b5ea1d26211f77241a6d',
v: 1,
creates: null,
chainId: 1
}
mockTransaction.setTransactionResponse(transactionResponse) // if hash is not set, the hash will be generated.
const transactionReceipt: Partial<providers.TransactionReceipt> = { // Add the fields that you want to set for the TransactionReceipt.
to: '0x4675C7e5BaAFBFFbca748158bEcBA61ef3b0a263',
from: '0xDAFEA492D9c6733ae3d56b7Ed1ADB60692c98Bc5',
contractAddress: null,
transactionIndex: 109,
gasUsed: BigNumber.from(21000),
logsBloom: '0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
blockHash: '0x25e44bfb2c3a47703c86884110a6d5c5a7a655b02fe1ca3a9d135e7459efab95',
transactionHash: '0x3fda39a81c47dc37d84c761c3cbbea375866c1fbdfcf91566eaa4c4ef62c70ad',
logs: [],
blockNumber: 18095175,
confirmations: 33,
cumulativeGasUsed: BigNumber.from(12276572),
effectiveGasPrice: BigNumber.from("9999762606"),
status: 1,
type: 2,
byzantium: true
}
mockTransaction.setTransactionReceipt(transactionReceipt) // if hash is not set, the hash will be generated.
...In this way the class can be used to shape the MockTransactionData into ethers.providers.TransactionResponse or/and ethers.providers.TransactionReceipt.
You can get only the TransactionResponse or TransactionReceipt by the calling the methods:
...
const txResponse: providers.TransactionResponse = mockTransactionData.getTransactionResponse();
const txReceipt: providers.TransactionReceipt = mockTransactionData.getTransactionReceipt();All of the set methods in the MockTransactionData will return the type MockTransactionData. So these set methods can be chained.
Some of the methods that the MockTransactionData provides to set the transaction field values:
setHash(hash: string): The method accepts a string and sets it as the txn hash for theMockTransactionData.generateHash(): The method generates the txn hash based on the currentMockTransactionDataconfig and sets it as the txn hash.setFrom(address: string): Sets the from AddresssetTo(address: string): Sets the to AddresssetNonce(value: number): Sets the Nonce.setContractAddress(address: string): Sets the contract Address field of the transaction.setGasPrice(value: string): Sets the Gas price for theMockTransactionDatasetGasLimit(value: string): Sets the gas limit for theMockTransactionDatasetData(data: string): Sets the data of the transaction.setLogs(logs: ethers.providers.Log[]): Sets the logs field in the transaction receipt field of theMockTransactionDatasetLogsBloom(value: string): Sets the logsBloom value.setTimestamp(timestamp: number): Sets the timestamp of the transaction.setStatus: Sets the Transaction Status.setConfirmations(confirmations: number): Sets the number of confirmations for the transaction.setTransactionResponse(transaction: Partial<ethers.providers.TransactionResponse>): Sets all the values for the ethersTransactionResponsebased on the given optional/partial fields. Generates the transaction hash if not given.setTransactionReceipt(receipt: Partial<ethers.providers.TransactionReceipt>): Sets all the values for the ethersTransactionReceiptbased on the given optional/partial fields. Generates the transaction hash if not given.setBlockHash(hash: string): Sets the Block hash.setBlockNumber(blockNumber: number): Sets the block number.setMaxPriorityFeePerGas(value: string): Sets the Max Priority fee for the transaction.setMaxFeePerGas(value: string): Sets the Max Fee field of the transaction.setTransactionType(type: number): Sets the value of type in the transaction. The type refers to the "Typed-Transaction features".setTransactionIndex(index: number): Sets the "transactionIndex" field in the transaction.setChainId(chainId: number): Sets the chainId of the transaction.setGasUsed(value: string): Sets the gasUsed field in the transaction.setCumulativeGasUsed(value: string): Sets the cumulativeGasUsed field in the transaction.setEffectiveGasPrice(value: string): Sets the effectiveGasPrice field in the transaction.
MockEthersProvider
This is a helper class for mocking the ethers.providers.Provider class.
Basic usage:
import { MockEthersProvider } from "forta-agent-tools/lib/test";
import { createAddress } from "forta-agent-tools";
import { utils, Contract } from "ethers";
const iface: utils.Interface = new utils.Interface([
"function myAwesomeFunction(uint256 param1, string param2) extenal view returns (unit8 id, uint256 val)",
]);
const address: string = createAddress("0xf00");
const data: string = createAddress("0xda7a");
const mockProvider: MockEthersProvider = new MockEthersProvider()
.addCallTo(address, 20, iface, "myAwesomeFunction", { inputs: [10, "tests"], outputs: [1, 2000] })
.addStorage(address, 5, 15, utils.defaultAbiCoder.encode(["address"], [data]));How to use it
This mock provides some methods to set up the values that the provider should return:
addCallTo(contract, block, iface, id, { inputs, outputs }). This method prepares a call to thecontractaddress at the specifiedblock, whereifaceis theethers.utils.Interfaceobject relative to the contract,idis the identifier of the function to call,inputsare the parameters passed in the call andoutputsare the values the call should return.addCallFrom(contract, from, block, iface, id, { inputs, outputs }). Similar toaddCallTobut only thefromwill be able to call the function.addStorage(contract, slot, block, result). This method prepares the value stored in the specificslotofcontractaddress in the givenblockto beresult.addBlock(blockNumber, block). This method prepares the block with numberblockNumberto beblock.setLatestBlock(block). This method allows you to set up what the number of the latest block in the provider is.addSigner(addr). This function prepares a valid signer for the given address that uses the provider being used.addLogs(logs). This method allows you to add entries to the logs record that will be filtered ingetLogsif the filter specified wasn't yet added inaddFilteredLogs.setNetwork(chainId, ensAddress?, name?). This method allows you to set up the network information (chainId,ensAddressandname) that will be returned when there's a call togetNetwork.setTransaction(transaction: MockTransactionData): This method accepts the transaction parameter of typeMockTransactionDataand allows you to set the return value for bothethers.providers.getTransaction(hash: string)andethers.providers.getTransactionReceipt(hash: string)clear(). This function clears all the mocked data.
All the data you set in the provider will be used until the clear function is called.
MockEthersSigner
This is a helper class for mocking the ethers.providers.JsonRpcSigner class. This class extends MockEthersProvider.
Basic usage:
import { MockEthersProvider, MockEthersSigner } from "forta-agent-tools/lib/test";
import { createAddress } from "forta-agent-tools";
import { utils, Contract } from "ethers";
const iface: utils.Interface = new utils.Interface([
"function myAwesomeFunction(uint256 param1, string param2)"
]);
const address: string = createAddress("0xf00");
const contract: string = createAddress("0xda0");
const mockProvider: MockEthersProvider = new MockEthersProvider();
const mockSigner: MockEthersSigner = new MockEthersSigner(mockProvider)
.setAddress(from)
.allowTransaction(
address, contract, iface,
"myAwesomeFunction", [20, "twenty"]
{ confirmations: 42 }, // receipt data
)How to use it
This mock provides some methods to set up the values that the signer should return:
setAddress(address). This method sets the address that the signer can sign.allowTransaction(from, to, iface, id, inputs). This method prepares a txn sent totoand signed fromfrom. The transaction is meant to call the methodidtaken from theifaceof thetocontract passing theinputsas parameters.receiptwill be the receipt returned by the transaction.denyTransaction(from, to, iface, id, inputs, msg). Same conditions ofallowTransactionbut in this case the transaction will be reverted withmsgmessage.
All the data you set in the signer will be used until the clear function is called.
NetworkManager
This is a tool to help with storing data relative to the network the bot will be running at.
Basic usage:
import { NetworkManager } from "forta-agent-tools";
interface NetworkData {
address: string;
num: number;
}
const data: Record<number, NetworkData> = {
// ChainID 1
1: {
address: "address1",
num: 1,
},
42: {
address: "address42",
num: 2,
},
};
const provider = getEthersProvider();
const networkManager = new NetworkManager(data);
await networkManager.init(provider);
networkManager.get("address"); // "address1" if the ChainID is 1, "address42" if the ChainID is 42How to use it
NetworkManager(networkData, chainId?): Sets the network data and creates aNetworkManagerinstance. IfchainIdis specified, it won't needNetworkManager.init()to be initialized. Throws an error if there is no entry forchainIdinnetworkData.getNetworkMap(): Gets the network map passed as argument in the constructor as read-only.getNetwork(): Gets the instance's active ChainID.setNetwork(chainId): Sets the instance's active ChainID. Throws an error if there is no entry forchainIdinnetworkData.init(provider): Retrieves network data from the provider and sets the active ChainID. Throws an error if there is no entry for that ChainID innetworkData.get(key): Gets the value of the fieldkeyin the active network's data record. Throws an error ifNetworkManagerwas not yet initialized, i.e. the ChainID was not specified in the constructor andNetworkManager.init()orNetworkManager.setNetwork()were not called.
ProviderCache
This is a class that can create a proxy to a provider which then caches call results and avoids cached calls being repeated both later and in the same block or transaction.
Basic usage:
import { ProviderCache, createAddress } from "forta-agent-tools";
import { ethers, getEthersProvider } from "forta-agent";
const provider = getEthersProvider();
const cachedProvider = ProviderCache.createProxy(provider);
const address = createAddress("0x0");
const iface: ethers.ContractInterface = [];
// the cached provider can be used as a regular provider
const contract = new ethers.Contract(address, iface, cachedProvider);How to use it
ProviderCache.createProxy(provider, cacheByBlockTag?): Creates a proxy to a provider that caches call results. IfcacheByBlockTagis set tofalse, then the call is cached without taking the block tag into account, useful for cases where some data can't change between blocks. By default,cacheByBlockTagis set totrue, thus the call result cache takes into account the block tag in which it's called.ProviderCache.clear(): Clears the internal cache.ProviderCache.set(options): Sets options specified byoptions.options.blockDataCacheSize?: If it's defined, the block data cache (used whencacheByBlockTagistrue) is cleared if it exists and its max size is changed to the value specified.options.immutableDataCacheSize?: if it's defined, the immutable data cache (used whencacheByBlockTagisfalse) is cleared if it exists and its max size is changed to the value specified.
CachedContract
This is a shortcut class that extends ethers.Contract but uses a cached provider from ProviderCache. Creating a CachedContract by calling new CachedContract(address, iface, provider, cacheByBlockTag?) is equivalent to creating an ethers.Contract by calling new ethers.Contract(address, iface, ProviderCache.createProxy(provider, cacheByBlockTag?)). There's also some utility methods.
Basic usage:
import { CachedContract, createAddress } from "forta-agent-tools";
import { getEthersProvider } from "forta-agent";
const provider = getEthersProvider();
const address = createAddress("0x0");
const iface: ethers.ContractInterface = [];
const cachedContract = new CachedContract(address, iface, provider, true);
// it can also be created from an existing ethers.Contract
const contract = new ethers.Contract(address, iface, provider);
const cachedContractfromContract = CachedContract.from(contract, true);How to use it
CachedContract(addressOrName, contractInterface, signerOrProvider, cacheByBlockTag?): Creates a newCachedContractinstance with addressaddressOrName, interfacecontractInterfaceand aProviderCacheproxy to the providerproviderwith the specifiedcacheByBlockTagoption. By default,cacheByBlockTagis set totrue. Throws ifprovidertype is not an extension ofethers.providers.BaseProvider.from(contract, cacheByBlockTag?): Creates a newCachedContractinstance fromcontract, anethers.Contractinstance by collecting its fields and calling the constructor. A wrapper tonew CachedContract(contract.address, contract.interface, contract.provider, cacheByBlockTag?). By default,cacheByBlockTagis set totrue. Throws ifcontracthas a signer.clear(): A shortcut toProviderCache.clear(). Clears theProviderCacheglobal cache.
MulticallProvider
This is an ethers provider-like interface built on top of ethers-multicall, but it also supports specifying a block
tag for a call, using Multicall2 features and making grouped calls.
The calls are decoded using ethers, so each return data has the same structure as a call made by itself through an ethers.Contract.
Supported chains (by default):
- Ethereum Mainnet
- Ropsten Testnet
- Rinkeby Testnet
- Görli Testnet
- Kovan Testnet
- BNB Smart Chain
- BNB Smart Chain Testnet
- Gnosis
- Huobi ECO Chain Mainnet
- Polygon Mainnet
- Fantom Opera
- Arbitrum One
- Avalanche
- Mumbai Testnet
Other chains can also be supported by finding a deployed Multicall2 contract address and calling
MulticallProvider.setMulticall2Addresses({ [chainId]: multicall2Address }). Default addresses can also be overriden.
Basic usage:
import { getEthersProvider } from "forta-agent";
import { MulticallProvider, MulticallContract, createAddress } from "forta-agent-tools";
const provider = getEthersProvider();
const multicallProvider = new MulticallProvider(provider);
const token = new MulticallContract(createAddress("0x0"), [
"function balanceOf(address account) external view returns (uint256)",
"function allowance(address owner, address spender) external view returns (uint256)",
]);
async function initialize() {
// fetches the provider network and loads an appropriate Multicall2 address
// throws if the network is not supported
await multicallProvider.init();
}
async function getBalances() {
const addresses = [createAddress("0x1"), createAddress("0x2"), createAddress("0x3")];
const calls = addresses.map((address) => token.balanceOf(address));
const blockTag = 1;
const [success, balancesAll] = await multicallProvider.all(calls, blockTag); // [success, [balance0, balance1, balance2]]
// or
const balancesTryAll = await multicallProvider.tryAll(calls, blockTag); // [{ success, returnData: balance0 }, { success, returnData: balance1 }, { success, returnData: balance2 }]
// or
const [successGrouped, balancesGrouped] = await multicallProvider.groupAll(
addresses.map((address) => [token.balanceOf(address), token.allowance(address, createAddress("0x4"))])
); // [success, [[balance0, allowance0], [balance1, allowance1], [balance2, allowance2]]]
// or
const balancesGroupTryAll = await multicallProvider.groupTryAll(
addresses.map((address) => [token.balanceOf(address), token.allowance(address, createAddress("0x4"))])
); // [
// [{ success, returnData: balance0 }, { success, returnData: allowance0 }],
// [{ success, returnData: balance1 }, { success, returnData: allowance1 }],
// [{ success, returnData: balance2 }, { success, returnData: allowance2 }],
//]
}How to use it
MulticallProvider(provider, chainId?): Creates aMulticallProviderinstance through an ethers providerprovider. IfchainIdis specified, it's not necessary to callinit()before making calls.MulticallProvider.setMulticall2Addresses(addresses): Allows overriding and adding support to more networks by specifying a validMulticall2contract address to it.init(): Fetches the provider's chain ID and and loads aMulticall2contract address. If there's no known address for that network, it throws an error.all(calls, blockTag?, batchSize?): Performs the calls inblockTagwithbatchSizesized multicalls and requires success of all of them. By default,batchSizeis50.tryAll(calls, blockTag?, batchSize?): Performs the calls inblockTagwithbatchSizesized multicalls and doesn't require their success, returning a flag for each of them that indicates whether they were successful or not. By default,batchSizeis50.groupAll(calls, blockTag?, batchSize?): Works in the same way asall(), but allows specifying groups of calls (e.g.[[call0, call1], [call2, call3]]) and keeps that same structure in the returned data.groupTryAll(calls, blockTag?, batchSize?): Works in the same way astryAll(), but allows specifying groups of calls (e.g.[[call0, call1], [call2, call3]]) and keeps that same structure in the returned data.
VictimIdentifier
This is a class library that identifies protocol victims:
- during the preparation stage of an attack, where victims are contained in a newly deployed contract's code
- during the exploitation stage of an attack, in transactions where the victim protocol's balance is reduced:
- more than $100, when denominated in USD, or
- more than 5% of the token's total supply.
The library also calculates the Confidence Level (0-1) for each of the victims:
- Preparation stage:
- The
Confidence Levelis determined based on the number of occurrences of the victim address in previously deployed contracts code.
- The
- Exploitation stage:
- The
Confidence Levelis determined either based on the USD value (with $500000 or more being the CL: 1 and by then splitting the CL into 10 parts) or based on the percentage of the token's total supply in which case there are 4 levels of confidence (5%-9%: CL 0.7, 10%-19%: CL 0.8, 20-29%%: CL 0.9, >30%: CL 1)
- The
Supported chains:
- Ethereum Mainnet
- BNB Smart Chain
- Polygon Mainnet
- Fantom Opera
- Arbitrum One
- Optimism Mainnet
- Avalanche
Basic usage:
import { Finding, HandleTransaction, TransactionEvent, ethers, getEthersProvider } from "forta-agent";
import { VictimIdentifier } from "forta-agent-tools";
const keys = {
ethplorerApiKey: "...",
moralisApiKey: "...",
etherscanApiKey: "...",
optimisticEtherscanApiKey: "...",
bscscanApiKey: "...",
polygonscanApiKey: "...",
fantomscanApiKey: "...",
arbiscanApiKey: "...",
snowtraceApiKey: "...",
};
export const provideHandleTransaction =
(victimsIdentifier: VictimIdentifier): HandleTransaction =>
async (txEvent: TransactionEvent) => {
const findings: Finding[] = [];
const victims = await victimsIdentifier.getIdentifiedVictims(txEvent);
/*Returns an object of type:
{
exploitationStage: Record<string, {
protocolUrl: string;
protocolTwitter: string;
tag: string;
holders: string[];
confidence: number;
}>;
preparationStage: Record<string, {
protocolUrl: string;
protocolTwitter: string;
tag: string;
holders: string[];
confidence: number;
}>;
}
*/
// Rest of the logic
return findings;
};
export default {
provideHandleTransaction,
handleTransaction: provideHandleTransaction(new VictimIdentifier(getEthersProvider(), keys)),
};How to use it
- Create a config file with any of the following optional API keys:
- Ethplorer API (Fetches the addresses of pool tokens holders)
- Moralis API (Fetches token prices when CoinGecko calls fail)
- Block Explorer APIs (Fetches the address of a contract's creator / a contract name)
- Etherscan
- Optimistic Etherscan
- Bscscan
- Polygonscan
- Fantomscan
- Arbiscan
- Snowtrace
- Initialize a
VictimIdentifierinstance that takes as parameters: 1) an ethers provider and 2) the API keys. - Call
VictimIdentifier's methodgetIdentifiedVictims()which takes as an input aTransactionEvent.
2 years ago
2 years ago
2 years ago
2 years ago
2 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
3 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago
4 years ago