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:
- Translates watch rule expressions into ClickHouse queries
- Executes queries against recent transaction data (last N blocks, typically 100)
- 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 transactionsitx<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 BEFOREtx2
which executes BEFOREtx3
tx1.itx1
executes BEFOREtx1.itx2
which executes BEFOREtx1.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 hashfrom
- Transaction sender addressto
- Transaction recipient addressvalue
- Transaction value (ETH amount)gas
- Gas usedblocknumber
- Block numberreverted
- 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 recipientinternal_from
- Internal call sendervalue
- Internal transaction valuereverted
- 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)
- Method:
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 calledsystem.emitted(tx1.Contract.E.event)
- Triggers when event is emittedsystem.reverted(tx1.Contract.F.method)
- Triggers when function revertssystem.reentrant(tx1.Contract.F.method)
- Detects reentrancy patternssystem.uintCompare(value1, operator, value2)
- Compare unsigned integerssystem.addressCompare(addr1, operator, addr2)
- Compare addressessystem.boolCompare(bool1, operator, bool2)
- Compare booleanssystem.stringCompare(str1, operator, str2)
- Compare stringssystem.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 equalLIKE
- String pattern matchingNOT 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.transfertx1.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 firetx1.itx1.ContractB.F.transfer
= ✅ Will fire
Best Practices for Comprehensive Monitoring
- Monitor Both Patterns: Use
||
operator or create separate bots for EOA vs contract calls - Contract Wallets: If monitoring contract wallets, use
tx1.itx1.*
patterns - Direct EOA Calls: Use
tx1.*
patterns for direct user interactions - Universal Monitoring: Consider both
tx1.Contract.F.method || tx1.itx1.Contract.F.method
Ordering Examples:
tx1
→tx2
: tx1 must execute on blockchain BEFORE tx2tx1.itx1
→tx1.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 togroupId
: Name of the address list created in portal/SDKmember
: Boolean flag controlling blacklist vs whitelist behaviormember: 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 transactionfrom
field (EOA sender) - Internal transactions (
tx1.itx1.Contract.F.*
): Usesinternal_from
field (calling contract address)
Supported Functions: Rule scope filtering applies to these system functions:
system.invoked()
- Function call filteringsystem.emitted()
- Event emission filteringsystem.reverted()
- Function revert filteringsystem.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.