Sentinel

The Defender Sentinel service offers 2 types of Sentinels, Contract Sentinels and Forta Sentinels. Contract Sentinels allow you to monitor transactions to a contract by defining conditions on events, functions, transaction parameters. Forta Sentinels allow you to monitor Forta Alerts by defining conditions on Forta Agents, contract addresses, alert IDs and severity. If a Sentinel matches a transaction or a Forta Alert based on your defined conditions it will notify you via email, slack, telegram, discord, Autotasks, and more.

Use cases

Use a Sentinel when you need to know about or respond to transactions and Forta Alerts involving your smart contracts, other smart contracts you need to monitor or Forta Agents. Your Sentinel will watch every transaction and Forta Alert and send the ones you care about to your notification methods of choice.

  • Monitor your sensitive functions like transferOwnership, pause, or upgrade

  • Alert on potentially dangerous transactions on your contracts

  • Respond by executing logic when key events happen

  • Integrate with your existing tools through slack, telegram, discord, email or custom Autotask integration

  • Know when an unexpected volume of transactions or alerts occur

When to use a Contract vs a Forta Sentinel

Contract Sentinels are particularly useful for monitoring transactions to a single contract. With the Contract Conditions you can filter transactions by various transaction attributes i.e events, functions, params, gasPrice, value, etc.

Forta Sentinels are useful for monitoring one or more of your smart contracts using a more complex criteria, by hooking into the events and conditions defined by Forta Agent Developers. Forta Agents have greater flexibility for defining transaction conditions than a Contract Sentinel.

What’s in a Contract Sentinel?

A Contract Sentinel watches transactions that affect a given address, filters those transactions by any conditions that you specify, then notifies you when those transactions occur.

At this time, Sentinels are supported across all networks except Fantom. Furthermore, Sentinels can only monitor internal function calls on Mainnet, Ropsten, and Kovan. If the internal call emits an event, consider monitoring for that event instead, as events emitted from internal calls are detected on all supported networks.

Specifying an Address

  • Address: Choose either a smart contract or an externally owned account to monitor.

  • ABI: Specify the ABI for the contract. Defender will automatically load the ABI if the source code is verified.

  • Confirmation Blocks: If you want to be notified only after a certain level of confidence that the transaction is accepted, choose a higher confirmation block level. If you want to be notified as soon as possible and are tolerant to re-orgs, then choose Latest

Matching Rules

For a Transaction to trigger a notification, it must satisfy ALL of the following:

  • Transaction MUST have a To, From, or Address (from log) that matches the configured address

  • If a Transaction Condition is specified

    • The Transaction MUST match the Transaction Condition.

  • If Events are selected

    • The Transaction MUST emit any of the selected Events and match the Event Condition (if any)

  • If Functions are selected

    • The Transaction MUST directly invoke any of the selected Functions (contract calls are not detected at the moment) and match the Function Condition (if any)

Events and Functions

Select any Events and Function calls that you wish to monitor. Selecting Events and Functions acts as an OR clause, in that if a transaction matches ANY of the selected Events or Functions, it will trigger notification.

An ABI is required to configure Events and Function detection
If no Events or Functions are specified, then the Sentinel will monitor ALL transactions to or from your monitored address.
At this time, Sentinels can only monitor internal function calls on Mainnet, Ropsten, and Kovan. If the internal call emits an event, consider monitoring for that event instead. Events emitted from internal calls are detected on all networks.

What’s in a Forta Sentinel?

A Forta Sentinel watches Forta Alerts that come from a given Forta Agent or affect a given address, and filters those alerts by any conditions that you specify, then notifies you when those alerts occur.

Specifying Forta Agents and Addresses

A Forta Sentinel filters on Agent IDs or Addresses and requires that at least one of these filters are set. It is possible to sepcify multiple Agent IDs and Addresses to filter on.

  • Agent IDs: Specify the Agent IDs to filter on. These can be comma separated to specify multiple.

  • Address: Choose either smart contracts or externally owned accounts to filter on.

Forta Matching Rules

For a Forta Alert to trigger a notification, it must satisfy ALL of the following:

  • Forta Alert MUST come from one of the Agent IDs specified and/or one of the Addresses specified

  • If the Alert ID Condition is specified

    • The Alert ID from the alert MUST match one of the Alert IDs specified

  • If the Severity Condition is specified

    • The Alert Severity MUST match or be of greater severity than the one specified

Severity and Alert IDs

You can specify Alert Severity and Alert IDs to monitor. Specifying both Severity and Alert IDs acts as an OR clause, in that if a alert matches ANY of the selected Alert IDs or matches the selected Severity, it will trigger notification.

If no Severity or Alert IDs are specified, then the Sentinel will monitor ALL alerts matching your specified Agent IDs and/or Adresses.

What are Contract Conditions?

Conditions act as filters that allow you to narrow the transactions even further. These are entered as expressions and offer a great deal of flexibility. Conditions are very much like Javascript expressions. To accomodate comparisons for checksum and non-checksum addresses, comparisons are case-insensitive.

If you want to receive ALL transactions that involve your selected events/functions, then do not specify any conditions.
  • Conditions can use AND, OR, NOT and ()

  • Conditions can use ==, <, >, >=, <= to compare

  • Number values can be referred to by Hex (0xabc123) or Decimal (10000000000)

  • String values can only be compared via ==

  • Includes basic math operators: +, -, *, /, ^

Transaction Conditions

If a transaction condition is specified, then a transaction MUST meet this condition in order to trigger a notification.

Transaction Conditions can refer to the following properties

  • to is the to address for the transaction

  • from is the from address for the transaction

  • gasPrice is the price of gas sent in the transaction

  • gasLimit is the gas limit sent in the transaction

  • gasUsed is the amount of gas used in the transaction

  • value is the value sent in the transaction

  • nonce is the nonce for the specific transaction

  • status is a derived value and can be compared with "success" or "failed"

Example Conditions

Transactions that are reverted

status == "failed"

Transactions excluding those from 0xd5180d374b6d1961ba24d0a4dbf26d696fda4cad

from != "0xd5180d374b6d1961ba24d0a4dbf26d696fda4cad"

Transactions that have BOTH a gasPrice higher than 50 gwei AND a gasUsed higher than 20000

gasPrice > 50000000000 and gasUsed > 20000

Event and Function Conditions

Event and Function conditions further narrow the set of transactions that trigger notification. These can refer to arguments in the signature either by name (if the argument is named) or by index (e.g. $0, $1…​). The variables that are available to you are indicated in the user interface as you specify these functions.

Example Conditions

Transactions that emit a Transfer(…​) event with a value between 1 and 100 ETH (in hex)

// Event Signature: Transfer(address to, address from, uint256 value)
value > 0xde0b6b3a7640000 and value < 0x56bc75e2d63100000

Transactions that emit a ValsEvent(…​) event with an array with a first element equal to 5

// Event Signature: ValsEvent(uint256[3] vals)
vals[0] == 5

Transactions that invoke a greet(…​) function with an unnamed string of "hello"

// Function Signature: greet(address, string)
$1 == "hello"

Autotask Conditions

If an autotask condition is specified, then it will be called with a list of matches found for a given block. This allows the sentinel to use other datasources and custom logic to evaluate whether a transaction is a match.

Only transactions that match other conditions (event, function, transaction) will invoke the autotask condition.
Each invocation can contain up to 25 transactions.

Request Schema

The request body will contain the following structure. You can use the SentinelConditionRequest type from the defender-autotask-utils package if you are coding your Autotasks in Typescript.

{
  "events": [
  {
    "transaction": {                // eth_getTransactionReceipt response body
      ...                           // see https://eips.ethereum.org/EIPS/eip-1474
    },
    "blockHash": "0xab..123",       // block hash from where this transaction was seen
    "matchReasons": [               // the reasons why sentinel triggered
      {
        "type": "event",            // event, function, or transaction
        "signature": "...",         // signature of your event/function
        "condition": "value > 5",   // condition expression (if any)
        "args": ["5"],              // parameters by index (unnamed are present)
        "params": { "value": "5" }  // parameters by name (unnamed are not present)
      }
    ],
    "sentinel": {
      "id": "44a7d5...31df5",       // internal ID of your sentinel
      "name": "Sentinel Name",      // name of your sentinel
      "abi": [...],                 // abi of your address (or undefined)
      "address": "0x000..000",      // address your sentinel is watching
      "confirmBlocks": 0,           // number of blocks sentinel waits
      "network": "rinkeby"          // network of your address
      "chainId": 4                  // chain Id of the network
    }
  }
  ]
}

Response Schema

The autotask must return a structure containing all matches. Returning an empty object indicates no match occurred. The type for this object is SentinelConditionResponse.

Errors will be treated as a non-match. All executions can be found on the Autotask’s run page.
{
  "matches": [
    {
      "hash": "0xabc...123",   // transaction hash
      "metadata": {
        "foo": true            // any object to be shared with notifications
      }
    },
    {
      "hash": "0xabc...123"    // example with no metadata specified
    }
  ]
}

Example Autotask Condition

exports.handler = async function(payload) {
  const conditionRequest = payload.request.body;
  const matches = [];
  const events = conditionRequest.events;
  for(const evt of events) {

    // add custom logic for matching here

    // metadata can be any JSON-marshalable object (or undefined)
    matches.push({
       hash: evt.hash,
       metadata: {
        "id": "customId",
        "timestamp": new Date().getTime(),
        "numberVal": 5,
        "nested": { "example": { "here": true } }
       }
    });
  }
  return { matches }
}

Testing Conditions

On the right side of the conditions form, there is a "Test Sentinel conditions" tool. This tool searches for transactions that match the Sentinel’s conditions across a range of blocks. Testing also invokes an autotask condition if one is specified

Options

  • Recent Blocks searches a range of blocks prior to the network’s latest block

  • Specific Block will search the specified block

  • Specific Transaction will attempt to match a transaction hash (0xabc…​def)

The search uses the conditions that are in the form at the current moment.

Note: Running a Test will not trigger a notification.

What are Forta Conditions?

Forta Conditions act as filters that allow you to narrow Forta Alerts down even further.

Severity Condition

The Severity Condition allows you to only get notified about alerts which are greater than a certain impact level. You will be notified of any alerts which match or have a greater impact level than your chosen severity value.

Forta Alerts may have 1 of the following 5 severity values which indicate different impact levels:

  • Critical - Exploitable vulnerabilities, massive impact on users/funds

  • High - Exploitable under more specific conditions, significant impact on users/funds

  • Medium - Notable unexpected behaviours, moderate to low impact on users/funds

  • Low - Minor oversights, negligible impact on users/funds

  • Info - Miscellaneous behaviours worth describing

Alert IDs Condition

The Alert IDs Condition allows you to filter alerts and only get notified about a specific class of finding. One or more Alert IDs may be specified.

Example Conditions

FORTA-1, NETHFORTA-1

Autotask Conditions

If an autotask condition is specified, then it will be called with a list of matches. This allows the sentinel to use other datasources and custom logic to evaluate whether a transaction is a match.

Only alerts that match other conditions (Severity, Alert IDs) will invoke the autotask condition.

Request Schema

The request body will contain the following structure.

{
  "events": [
    {
      "alert": {                            // Forta Alert
        "addresses": [ "0xab..123" ],       // map of addresses involved in the transaction
        "alert_id": "NETHFORTA-1",          // unique string to identify this class of finding
        "name": "High Gas Used",            // human-readable name of finding
        "description": "Gas Used: 999999",  // brief description
        "hash": "0xab..123",                // Forta Alert transaction hash
        "protocol": "ethereum",             // specifies which network the transaction was mined
        "scanner_count": 1,
        "severity": "MEDIUM",               // indicates impact level of finding
        "type": "SUSPICIOUS",               // indicates type of finding: Exploit, Suspicious, Degraded, Info
        "source": {
          "tx_hash": "0xab..123",           // network transaction hash  e.g ethereum transaction hash
          "agent": {
            "id": "0xab..123",              // Agent ID
            "name": ""                      // Agent name
          },
          "block": {
            "chain_id": 1,                  // Chain ID of the originating network
            "hash": "0xab..123",            // network block hash  e.g ethereum block hash
          }
        }
      },
      "matchReasons": [                     // the reasons why sentinel triggered
        {
          "type": "alert-id",               // Alert ID or Severity
          "value": "NETHFORTA-1"            // Condition Value
        }
      ],
      "sentinel": {
        "id": "forta_id",                   // internal ID of your sentinel
        "name": "forta sentinel",           // name of your sentinel
        "addresses": [ "0xab..123" ],       // addresses your sentinel is monitoring
        "agents": [ "0xab..123" ]           // Agent IDs your sentinel is monitoring
        "network": "mainnet"                // network your sentinel is monitoring
        "chainId": 1                        // chain Id of the network
      }
    }
  ]
}

Response Schema

The autotask must return a structure containing all matches. Returning an empty object indicates no match occurred. The type for this object is SentinelConditionResponse.

Errors will be treated as a non-match. All executions can be found on the Autotask’s run page.
{
  "matches": [
    {
      "hash": "0xabc...123",   // Forta Alert hash i.e events[0].alert.hash
      "metadata": {
        "foo": true            // any object to be shared with notifications
      }
    },
    {
      "hash": "0xabc...123"    // example with no metadata specified
    }
  ]
}

Example Autotask Condition

exports.handler = async function(payload) {
  const conditionRequest = payload.request.body;
  const matches = [];
  const events = conditionRequest.events;
  for(const evt of events) {

    // add custom logic for matching here
    // metadata can be any JSON-marshalable object (or undefined)
    matches.push({
       hash: evt.hash,
       metadata: {
        "id": "customId",
        "timestamp": new Date().getTime(),
        "numberVal": 5,
        "nested": { "example": { "here": true } }
       }
    });
  }
  return { matches }
}

Notifications

When triggered, a Sentinel can notify one or more slack webhooks, telegram bots, discord webhooks, email lists, datadog metrics, custom webhooks, or execute an autotask.

Slack Configuration

Please see https://api.slack.com/messaging/webhooks to configure a Slack webhook. Once Slack is configured, enter the webhook URL in Defender.

  • Alias is the name for this slack configuration. For instance, you might name it after the name of the channel.

  • Webhook URL is the URL from your slack management console to use for notification.

Email Configuration

  • Alias is the name for this email list. (e.g., Developers)

  • Emails is the list of emails you wish to notify. These can be comma or semicolon-delimited.

Discord Configuration

Please see https://support.discord.com/hc/en-us/articles/228383668-Intro-to-Webhooks to configure a webhook for your Discord channel.

  • Alias is the name for this discord configuration.

  • Webhook URL is the URL from your discord channel to use for notification.

Datadog Configuration

Datadog configurations let Defender forward custom metrics to your Datadog account. For more information about custom metrics, please see https://docs.datadoghq.com/developers/metrics/

The metric we send is a COUNT metric, which represents the number of transactions that triggered the sentinel. We do not send zeros, so a lack of data should be expected if the sentinel does not trigger. With each metric, we send two tags: network (rinkeby, mainnet,…​) & sentinel (name of sentinel)

It can take several minutes for a new custom metric to show up in the Datadog console
  • Alias is the name for this Datadog configuration.

  • Api Key is the API key from your Datadog management.

  • Metric Prefix will precede all metric names. For instance, with a prefix of defender., sentinels will send a metric called defender.sentinel.

Telegram Configuration

Please see https://core.telegram.org/bots#6-botfather to configure a Telegram Bot using the BotFather

The Telegram Bot must be added to your channel and have the rights to post messages.

To find the Chat ID of the channel, execute the following curl (with your bot token value) and extract the id value of the chat. If you do not receive any entries in the response, send a test message to your chat first.

$ curl https://api.telegram.org/bot$BOT_TOKEN/getUpdates
{
  "ok": true,
  "result": [
    {
      "update_id": 98xxxx98,
      "channel_post": {
        "message_id": 26,
        "sender_chat": {
          "id": -100xxxxxx5976,
          "title": "Defender Sentinel Test",
          "type": "channel"
        },
        "chat": {
          "id": -100xxxxxx5976, // <--- This is your chat ID
          "title": "Defender Sentinel Test",
          "type": "channel"
        },
        "date": 1612809138,
        "text": "test"
      }
    }
  ]
}
  • Alias is the name for this Telegram configuration.

  • Chat ID is the ID of the Telegram Chat.

  • Bot Token is the token you receive from the BotFather when creating the Telegram Bot.

Custom webhook Configuration

To configure a custom webhook notification channel, you just need to provide the webhook endpoint URL and an alias for display purposes.

  • Alias is the name for this webhook endpoint.

  • Webhook URL is the URL where Sentinel will send matching events.

To avoid overwhelming the receiving webhook with many concurrent requests under a high number of matches, Sentinel sends a JSON object with an events containing an array with all the matching events found in a block.

{
  events: [...] // See Event Schema for details on the contents of this array
}

The event schema is exactly the same as the one laid out in Event Schema. You can also use the test notification feature to send a test notification to your webhook.

Autotask

If an autotask is selected, then the autotask will receive a body property containing the deails for the triggering event, either the transaction details for the triggering transaction or Forta Alert details from the triggerting alert. The autotask can then perform custom logic and reach out to external APIs as needed.

Autotask executions are subject to quotas. After a quota is exhausted, the autotask will no longer execute. If you need to raise your Autotask execution quotas, please let us know at defender@openzeppelin.com with a description of your use case.

Autotask Events

The sentinel will pass information about the transaction to your autotask. If you are writing your Autotasks in typescript you can use the BlockTriggerEvent type for contract sentinels and the FortaTriggerEvent type for Forta sentinels, from the defender-autotask-utils package.

Example Autotask

exports.handler = async function(params) {
  const payload = params.request.body;
  const matchReasons = payload.matchReasons;
  const sentinel = payload.sentinel;

  // if contract sentinel
  const transaction  = payload.transaction;
  const abi = sentinel.abi;

  // if Forta sentinel
  const alert  = payload.alert;



  // custom logic...
}

Event Schema

Contract Sentinel

{
  "transaction": {                // eth_getTransactionReceipt response body
    ...                           // see https://eips.ethereum.org/EIPS/eip-1474
  },
  "blockHash": "0xab..123",       // block hash from where this transaction was seen
  "matchReasons": [               // the reasons why sentinel triggered
    {
      "type": "event",            // event, function, or transaction
      "signature": "...",         // signature of your event/function
      "condition": "value > 5",   // condition expression (if any)
      "args": ["5"],              // parameters by index (unnamed are present)
      "params": { "value": "5" }  // parameters by name (unnamed are not present)
      "metadata": {...}           // metadata injected by Autotask Condition (if applicable)
    }
  ],
  "sentinel": {
    "id": "44a7d5...31df5",       // internal ID of your sentinel
    "name": "Sentinel Name",      // name of your sentinel
    "abi": [...],                 // abi of your address (or undefined)
    "address": "0x000..000",      // address your sentinel is watching
    "confirmBlocks": 0,           // number of blocks sentinel waits
    "network": "rinkeby"          // network of your address
    "chainId": 4                  // chain Id of the network
  }
}

Forta Sentinel

{
  "alert": {                            // Forta Alert
    "addresses": [ "0xab..123" ],       // map of addresses involved in the transaction
    "alert_id": "NETHFORTA-1",          // unique string to identify this class of finding
    "name": "High Gas Used",            // human-readable name of finding
    "description": "Gas Used: 999999",  // brief description
    "hash": "0xab..123",                // Forta Alert transaction hash
    "protocol": "ethereum",             // specifies which network the transaction was mined
    "scanner_count": 1,
    "severity": "MEDIUM",               // indicates impact level of finding
    "type": "SUSPICIOUS",               // indicates type of finding: Exploit, Suspicious, Degraded, Info
    "source": {
      "tx_hash": "0xab..123",           // network transaction hash  e.g ethereum transaction hash
      "agent": {
        "id": "0xab..123",              // Agent ID
        "name": ""                      // Agent name
      },
      "block": {
        "chain_id": 1,                  // Chain ID of the originating network
        "hash": "0xab..123",            // network block hash  e.g ethereum block hash
      }
    }
  },
  "matchReasons": [                     // the reasons why sentinel triggered
    {
      "type": "alert-id",               // Alert ID or Severity
      "value": "NETHFORTA-1"            // Condition Value
    }
  ],
  "sentinel": {
    "id": "forta_id",                   // internal ID of your sentinel
    "name": "forta sentinel",           // name of your sentinel
    "addresses": [ "0xab..123" ],       // addresses your sentinel is monitoring
    "agents": [ "0xab..123" ]           // Agent IDs your sentinel is monitoring
    "network": "mainnet"                // network your sentinel is monitoring
    "chainId": 1                        // chain Id of the network
  }
}

Controlling the Notify Rate

Once you have specified your conditions as desired, there are two ways to limit the number of notifications: Alert and Timeout. These are meant to be used together to achieve a wide range of alerting behaviors.

Alert Threshold

To be alerted when matching transactions exceed a threshold, use an alert threshold.

This threshold is evaluated for each transaction. Once a threshold is exceeded then notifications will continue to fire until the amount falls below the threshold in the time window immediately prior to the current transaction. Consider using a Timeout value to prevent subsequent notifications.
  • Amount is the number of times this sentinel must trigger before firing a notification.

  • Window is the number of seconds that is considered

Example:

At least 5 times within an hour should specify an Amount of 5 and a Window of 3600 seconds.

Timeout

If you do not wish to receive a notification more than a certain rate, consider using a Timeout. This will effectively prevent notifications for a certain duration after a notification is sent.

  • Timeout is the number of seconds to wait between notifications

Example:

Avoid notifying more than once per hour should specify a Timeout of 3600

Pausing

Pausing a Sentinel will pause the monitoring of your address.