FailSafe Expression Language Reference

Purpose: Complete reference for the FailSafe expression language Usage: LLM context loading and developer reference

FailSafe System Architecture Context

Data Pipeline Overview

FailSafe ingests blockchain transactions into a ClickHouse database for contracts under monitoring. When new transactions are ingested, the monitoring system:

  1. Translates watch rule expressions into ClickHouse queries
  2. Executes queries against recent transaction data (last N blocks, typically 100)
  3. Generates alerts when queries return any rows (webhook notifications to Slack, etc.)

Terminology

  • Watch Rules and Expressions are used interchangeably - they refer to the same thing
  • Watch Bot = One or more expressions combined into a monitoring bot (see Monitor Bot Schema)

Query Execution Model

  • Alert Trigger: Any rows returned from expression query = alert generated
  • Block Monitoring: System examines transactions in most recent N blocks (default: 100)
  • Real-time Detection: Optimized for near real-time monitoring of live blockchain activity

Design Principle - Monitoring vs Analysis: FailSafe is designed for ongoing monitoring, not one-time analysis. Focus on expressions that detect ongoing patterns rather than historical analysis queries.

Design Principle - Performance Awareness: The system automatically optimizes expressions for query performance. Complex multi-condition expressions are supported and often necessary for advanced threat patterns - use the complexity your security requirements demand.

Expression Performance Considerations

  • Transaction Limits: Maximum 3 top-level transactions (tx1, tx2, tx3) for query efficiency
  • Internal Transaction Limits: Maximum 3 internal transactions per top-level (tx1.itx1, tx1.itx2, tx1.itx3)
  • Block Range Limits: topLevelTxBlockRange controls maximum block distance between tx1 and tx2
  • Query Translation: Expressions are automatically converted to efficient database queries

FailSafe Monitor Bot Schema

Monitor Bot Structure

"{\n  \"botName\": \"string\",\n  \"botDescription\": \"string (optional)\",\n  \"chainId\": \"number\", // e.g., 1 (Ethereum), 137 (Polygon), etc.\n  \"active\": \"boolean\", // true or false\n  \"severity\": \"High|Medium|Low|Critical\",\n  \"watchRule\": {\n    \"expressions\": [\"string\"] // Array of FailSafe expression strings\n  },\n  \"notifications\": [\"string\"], // Array of notification IDs\n  \"botVersion\": \"string (optional)\",\n  \"options\": {\n    \"topLevelTxBlockRange\": \"number\", // Block range for monitoring\n    \"ruleScope\": { // Address list filtering (optional)\n      \"contractName\": \"string\", // Contract to apply filtering to\n      \"groupId\": \"string\", // Address list name from portal/SDK\n      \"member\": \"boolean\" // true = whitelist mode, false = blacklist mode\n    },\n    \"literalDefs\": { // Literal placeholder definitions (optional)\n      \"placeholderName\": \"value\"\n    }\n  }\n}"

Example Monitor Bot

{
  "botName": "Liquidity Addition Monitor",
  "botDescription": "Monitors for suspicious liquidity additions above threshold",
  "chainId": 1,
  "active": true,
  "severity": "High",
  "watchRule": {
    "expressions": [
      "system.emitted(tx1.itx1.CorkHook.E.AddedLiquidity) && system.uintCompare(tx1.itx1.CorkHook.E.AddedLiquidity.mintedLp, >, ${suspiciousLpAmount})"
    ]
  },
  "notifications": [
    "slack_alerts",
    "email_critical"
  ],
  "options": {
    "topLevelTxBlockRange": 100,
    "ruleScope": {
      "contractName": "USDC",
      "groupId": "local_white_list_test",
      "member": false
    },
    "literalDefs": {
      "suspiciousLpAmount": 1e+24
    }
  }
}

Literal Placeholders

Expressions can use literal placeholders with ${placeholderName} syntax for configurable values. The system substitutes these placeholders with actual values from options.literalDefs before execution.

Syntax: ${placeholderName} where placeholderName matches a key in the options.literalDefs object.

FailSafe Expression Language Reference

DSL Structure and Naming Convention

FailSafe expressions follow a structured dot notation pattern for referencing blockchain transaction data:

tx[.itx]...[.Parameter][.SubField]

Transaction Reference Pattern

  • tx<N> - Top-level transaction reference (tx1, tx2, tx3) - maximum 3 transactions
  • itx<N> - Internal transaction reference (itx1, itx2, itx3) - optional, maximum 3 per top-level transaction
  • Examples: tx1, tx2.itx1, tx1.itx3
  • Note: Internal transaction references are optional - use only when monitoring internal calls within a transaction
  • IMPORTANT - Transaction Ordering: Numbers indicate execution order on the blockchain:
    • tx1 executes BEFORE tx2 which executes BEFORE tx3
    • tx1.itx1 executes BEFORE tx1.itx2 which executes BEFORE tx1.itx3
    • This ordering is crucial for correlation patterns and multi-step attack detection

Contract Name Resolution

  • <ContractName> - Human-readable contract identifier
  • Contract names are mapped to addresses via the monitoring portal or SDK
  • Examples: USDC, UniswapV2Router, LendingPool (these are examples - use your actual contract names)

Type Indicators

  • F - Function/method calls (tx1.USDC.F.transfer)
  • E - Event emissions (tx1.USDC.E.Transfer)
  • Transaction Fields - Direct transaction metadata access

Transaction Fields (Top-Level)

Available fields for tx<N>.<ContractName>.<FieldName> (case-insensitive):

  • hash - Transaction hash
  • from - Transaction sender address
  • to - Transaction recipient address
  • value - Transaction value (ETH amount)
  • gas - Gas used
  • blocknumber - Block number
  • reverted - Transaction reverted flag (0/1)
  • reentrant - Reentrancy detected flag (0/1)
  • riskscore - Risk assessment score from FailSafe Radar system (0-100, where 0 = no known risk, 100 = very high risk)

Internal Transaction Fields (Internal)

Available fields for tx<N>.itx<N>.<ContractName>.<FieldName>:

  • to_address - Internal call recipient
  • internal_from - Internal call sender
  • value - Internal transaction value
  • reverted - Internal transaction reverted flag (0/1)
  • reentrant - Internal reentrancy flag (0/1)

Function and Event Names

  • Function Names: Must exist in contract ABI (examples: transfer, approve, mint)
  • Event Names: Must exist in contract ABI (examples: Transfer, Approval, Mint)
  • Wildcard: Use * to match any function or event (tx1.USDC.F.*)
  • Raw Signatures: Use hex signatures to disambiguate function/event overloading
    • Method: tx1.USDC.F.0xa9059cbb (4-byte selector)
    • Event: tx1.USDC.E.0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef (32-byte topic0)

Why Raw Signatures? Solidity supports function overloading - multiple functions/events can have the same name but different parameters. Raw signatures ensure you target the exact function/event you want.

Recommendation: Use function/event names by default for clarity - overloading is uncommon. Only use raw signatures when you encounter actual name conflicts.

System Functions

System functions are case-insensitive - system.Invoked(), system.invoked(), and system.INVOKED() are all valid.

  • system.invoked(tx1.Contract.F.method) - Triggers when function is called
  • system.emitted(tx1.Contract.E.event) - Triggers when event is emitted
  • system.reverted(tx1.Contract.F.method) - Triggers when function reverts
  • system.reentrant(tx1.Contract.F.method) - Detects reentrancy patterns
  • system.uintCompare(value1, operator, value2) - Compare unsigned integers
  • system.addressCompare(addr1, operator, addr2) - Compare addresses
  • system.boolCompare(bool1, operator, bool2) - Compare booleans
  • system.stringCompare(str1, operator, str2) - Compare strings
  • system.noMatches(expression) - Negation wrapper

Comparison Functions

The comparison functions (system.uintCompare, system.addressCompare, system.boolCompare, system.stringCompare) are the ONLY functions that use parameter access and comparison operators.

Parameter Access (For Comparison Functions Only):

  • Simple Parameters: tx1.USDC.F.transfer.amount (example parameter name)
  • Array Access: tx1.Router.F.swap.path[0] (example array parameter)
  • Nested Objects: tx1.Router.F.swap.params.deadline (example nested parameter)
  • Complex Paths: tx1.Router.F.smartSwapByOrderId.batches[0][1].extraData[0] (example complex structure)

Comparison Operators (For Comparison Functions Only):

  • == - Equal to
  • != - Not equal to
  • > - Greater than
  • < - Less than
  • >= - Greater than or equal
  • <= - Less than or equal
  • LIKE - String pattern matching
  • NOT LIKE - String pattern exclusion

Note: Parameter access and comparison operators are NOT used with system.invoked(), system.emitted(), system.reverted(), system.reentrant(), or system.noMatches(). These functions only take transaction/event references as parameters.

Watch Rule Expressions

Basic Expression Examples

// ✅ CORRECT - Comparison function with parameter access and operator
"system.uintCompare(tx1.DAI.F.transfer.amount, >, 1000000)"

// ✅ CORRECT - Non-comparison function with simple reference
"system.invoked(tx1.WETH.F.transfer)"

// ❌ INCORRECT - Non-comparison function cannot use operators
"system.invoked(tx1.USDC.F.transfer.amount, >, 1000000)"

// Basic function call (replace with your actual contract/function)
"system.invoked(tx1.AavePool.F.deposit)"

// Internal transaction event (replace with your actual contract/event)
"system.emitted(tx1.itx1.UniswapV2Pair.E.Swap)"

// Parameter comparison (replace 'amount' with your parameter name)
"system.uintCompare(tx1.CompoundCToken.F.mint.mintAmount, >, 1000000)"

// Transaction field access (address is an example)
"system.addressCompare(tx1.LendingPool.from, ==, ${targetAddress})"

// Risk score monitoring - triggers on high-risk addresses
"system.uintCompare(tx1.MultisigWallet.riskscore, >, 50)"

// Internal transaction field
"system.addressCompare(tx1.itx1.WETH.internal_from, !=, ${zeroAddress})"

// Raw signature with parameter (signature and parameter are examples)
"system.uintCompare(tx1.DAI.F.0xa9059cbb.value, >, 0)"

Combining Expressions with Logical Operators

You can combine multiple conditions within a single expression using logical operators (&& for AND, || for OR):

// AND operator - Both conditions must be true
"system.invoked(tx1.WETH.F.transfer) && system.emitted(tx1.WETH.E.Transfer)"

// OR operator - Either condition can be true  
"system.uintCompare(tx1.AavePool.F.deposit.amount, >, 1000000) || system.uintCompare(tx1.AavePool.F.deposit.amount, ==, 0)"

// Complex OR with nested AND - Either (transfer without approve) OR (transfer with specific value)
"system.invoked(tx1.DAI.F.transfer) && system.noMatches(system.invoked(tx1.DAI.F.approve)) || system.uintCompare(tx1.DAI.F.transfer.value, ==, 498500000)"
Event Correlation
// Function called but no event emitted
"system.invoked(tx1.CompoundCToken.F.mint) && system.noMatches(system.emitted(tx1.CompoundCToken.E.Mint))"

// Event emitted with suspicious parameters
"system.emitted(tx1.UniswapV2Pair.E.Swap) && system.uintCompare(tx1.UniswapV2Pair.E.Swap.amount0In, ==, 0)"
Multi-Transaction Patterns
// Flash loan detection (tx1 BEFORE tx2 - temporal sequence)
"system.invoked(tx1.AavePool.F.flashLoan) && system.invoked(tx2.AavePool.F.repayFlashLoan)"

// Reentrancy detection (within single transaction)
"system.reentrant(tx1.LendingPool.F.withdraw)"

// MEV sandwich attack detection (tx1 → tx2 → tx3 sequence)
"system.invoked(tx1.UniswapV2Router.F.swap) && system.invoked(tx2.USDC.F.transfer) && system.invoked(tx3.UniswapV2Router.F.swap) && system.addressCompare(tx1.UniswapV2Router.from, ==, tx3.UniswapV2Router.from)"

// Multi-step protocol interaction (ordered execution)
"system.invoked(tx1.WETH.F.approve) && system.invoked(tx2.AavePool.F.deposit) && system.uintCompare(tx1.WETH.F.approve.amount, ==, tx2.AavePool.F.deposit.amount)"

// Internal transaction sequence within single tx
"system.invoked(tx1.itx1.DAI.F.transferFrom) && system.invoked(tx1.itx2.UniswapV2Router.F.swap) && system.invoked(tx1.itx3.CompoundCToken.F.mint)"

Operator Precedence: AND (&&) has higher precedence than OR (||).

Multiple Expressions in Watch Rules

When you have multiple expressions in a watch rule array ["expression1", "expression2", ...], this means logically (expression1) && (expression2) - all expressions must be true for the alert to trigger.

Example:

"expressions": [
  "system.invoked(tx1.AavePool.F.deposit)",
  "system.uintCompare(tx1.AavePool.F.deposit.amount, >, 1000000)"
]

This is equivalent to: system.invoked(tx1.AavePool.F.deposit) && system.uintCompare(tx1.AavePool.F.deposit.amount, >, 1000000)

Expression Writing Best Practices

Avoid Redundant Function Calls: When using comparison functions with parameter access, avoid adding redundant system.invoked() calls. Parameter access already implies function invocation, so the extra call makes queries less efficient and expressions harder to read without adding value.

// ✅ CORRECT - Parameter access implies function invocation
"system.uintCompare(tx1.USDC.F.transfer.amount, >, 1000000)"

// ⚠️ INEFFICIENT - Redundant system.invoked() call (works but unnecessary)
"system.invoked(tx1.USDC.F.transfer) && system.uintCompare(tx1.USDC.F.transfer.amount, >, 1000000)"

Critical Transaction Pattern Understanding

Multi-Transaction Same Sender Rule

When using multiple top-level transactions (tx1, tx2, tx3), all transactions will have the same "from" address. This is enforced in query generation.

Single Function Per Transaction: Each top-level transaction (tx1, tx2, tx3) can only contain ONE function call. You cannot reference multiple top-level functions within the same transaction.

// ❌ INCORRECT - Cannot have two top-level function calls in same transaction
"system.invoked(tx1.DAI.F.approve) && system.invoked(tx1.DAI.F.transferFrom)"

// ✅ CORRECT - Use sequential transactions for multiple function calls
"system.invoked(tx1.DAI.F.approve) && system.invoked(tx2.DAI.F.transferFrom)"

// ✅ CORRECT - Or use internal transactions within same transaction
"system.invoked(tx1.itx1.DAI.F.approve) && system.invoked(tx1.itx2.DAI.F.transferFrom)"

EOA vs Contract Call Patterns

CRITICAL: Understanding when expressions will trigger based on caller type:

  • tx1.ContractB.F.transfer - Triggers ONLY when EOA (externally owned address) directly calls ContractB.transfer
  • tx1.itx1.ContractB.F.transfer - Triggers ONLY when ContractA calls ContractB.transfer (internal transaction)

Example Scenario:

  • EOA → ContractA → ContractB.transfer
  • tx1.ContractB.F.transfer = ❌ Will NOT fire
  • tx1.itx1.ContractB.F.transfer = ✅ Will fire

Best Practices for Comprehensive Monitoring

  1. Monitor Both Patterns: Use || operator or create separate bots for EOA vs contract calls
  2. Contract Wallets: If monitoring contract wallets, use tx1.itx1.* patterns
  3. Direct EOA Calls: Use tx1.* patterns for direct user interactions
  4. Universal Monitoring: Consider both tx1.Contract.F.method || tx1.itx1.Contract.F.method

Ordering Examples:

  • tx1tx2: tx1 must execute on blockchain BEFORE tx2
  • tx1.itx1tx1.itx2: itx1 must execute BEFORE itx2 within the same transaction tx1
  • This ordering enables detection of multi-step attacks, arbitrage patterns, and protocol violations

Address Lists and Rule Scope

FailSafe supports address lists for implementing whitelist/blacklist functionality. Address lists are created via the monitoring portal or SDK and can be applied to expressions using options.ruleScope in the monitor bot definition (see API Structure section above).

Address List Creation:

  • Create named address lists via portal or SDK (e.g., "watchList", "trustedAddresses", "blacklistedWallets")
  • Each list contains an array of addresses (examples): ["0xA0b86a33E6441e6b421b3c4c4b8f7e3c4d5f6789", "0xB1c97a44F7552f7c5c9g8h4j5k6l7890abcdef12", ...]

Rule Scope Parameters:

  • contractName: Contract to apply the scope filtering to
  • groupId: Name of the address list created in portal/SDK
  • member: Boolean flag controlling blacklist vs whitelist behavior
    • member: true = Blacklist mode: Rule fires when address IS in the list (alert on suspicious addresses)
    • member: false = Whitelist mode: Rule fires when address is NOT in the list (alert when not trusted addresses)

Address Selection Logic:

  • Top-level transactions (tx1.Contract.F.*): Uses transaction from field (EOA sender)
  • Internal transactions (tx1.itx1.Contract.F.*): Uses internal_from field (calling contract address)

Supported Functions: Rule scope filtering applies to these system functions:

  • system.invoked() - Function call filtering
  • system.emitted() - Event emission filtering
  • system.reverted() - Function revert filtering
  • system.reentrant() - Reentrancy detection filtering

Note: Rule scope does NOT apply to comparison functions (system.uintCompare, system.addressCompare, etc.), but these can be combined with filtered functions using && operators.

Examples:

// Whitelist mode - alerts on calls from untrusted addresses
"options": {
  "ruleScope": { "contractName": "USDC", "groupId": "trusted_addresses", "member": false }
}
"expressions": ["system.invoked(tx1.USDC.F.*)"]

// Combined filtering with parameter validation
"options": {
  "ruleScope": { "contractName": "AavePool", "groupId": "authorized_contracts", "member": false }
}
"expressions": ["system.invoked(tx1.itx1.AavePool.F.withdraw) && system.uintCompare(tx1.itx1.AavePool.F.withdraw.amount, >, ${largeAmount})"]

Complete Examples Directory

Foundational Patterns

  • Basic Syntax - Core system functions, comparison operators, and basic expression patterns. Essential starting point covering system.invoked(), system.emitted(), numeric/address/string comparisons, risk score monitoring, and comma-separated expressions.

  • Transaction Context - Transaction reference patterns including single transactions, internal transactions, multi-transaction sequences, and cross-transaction internal patterns. Critical for understanding tx1/tx2/tx3 and itx1/itx2/itx3 usage.

  • Data Access Patterns - Comprehensive guide to accessing function parameters, event fields, transaction metadata, nested objects, and array elements. Includes literal placeholder usage and internal transaction field access.

  • Logical Operators - Combining expressions with AND (&&) and OR (||) operators, operator precedence, comma-separated expressions, and complex multi-condition patterns with literal placeholders.

  • Wildcards and Raw Signatures - Wildcard patterns (*) for functions and events, raw hex signatures for disambiguation, and when to use each approach for comprehensive monitoring.

Advanced Patterns

  • Cross-Reference Patterns - Cross-transaction and cross-parameter comparisons for correlation detection. Essential for flash loan verification, parameter correlation, and multi-step attack detection.

  • Negation Logic - Using system.noMatches() to detect missing expected behavior, failed operations, unauthorized transfers, and security bypasses. Critical for comprehensive security monitoring.

  • Real-World DeFi Scenarios - DeFi security patterns including flash loan arbitrage, MEV sandwich attacks, reentrancy detection, treasury drains, and excessive borrowing patterns.

  • Internal Transactions - Advanced internal transaction monitoring including sequential internal calls, parameter correlation, multi-protocol sequences, and reentrancy exploitation patterns.

  • Address Lists - Complete guide to address list filtering with ruleScope, blacklist/whitelist modes, EOA vs contract filtering, and combining address filtering with parameter validation.