# see-through

Detection Templates

Full transparency into every detection template ChainAlert offers. This page documents the exact math, algorithms, RPC methods, and inputs for all 18 templates — so you know precisely what is running on your contracts and why an alert fired.

Blockchain Concepts

ChainAlert templates rely on four fundamental EVM primitives. A quick primer before diving into the templates:

Event Logs — when a smart contract emits an event (e.g. Transfer(address,address,uint256)), the EVM writes a log entry into the transaction receipt. These are indexed and queryable via eth_getLogs.

Storage Slots — every contract has 2^256 storage slots, each holding a 32-byte value. Readable via eth_getStorageAt. Standards like ERC-1967 assign well-known slots to proxy metadata.

ABI Encoding — the Application Binary Interface defines how function calls and event parameters are encoded into bytes. ChainAlert uses ABI definitions to decode raw log data into typed arguments like addresses, uint256 values, and bytes32 hashes.

Balance Queries eth_getBalance returns the native token balance (ETH, MATIC, etc.) and eth_call can invoke read-only functions like balanceOf without submitting a transaction.

Token Activity

Templates that monitor ERC-20 token transfer events for volume and frequency anomalies.

Large Transfer

freehigh

ERC-20 Transfer events where the transferred value exceeds a user-defined threshold.

How it works

Listens for Transfer(address,address,uint256) events on the monitored contract. When one is emitted, decodes the value argument and compares it against the configured threshold as a BigInt.

trigger = decodedArgs.value > BigInt(threshold)

Inputs: threshold, tokenAddress (optional)

Repeated Transfer

promedium

N or more Transfer events sent to the same recipient within a rolling time window.

How it works

Uses a Redis sorted set keyed by recipient address. Each Transfer event adds a timestamped entry. Before evaluating, stale entries outside the window are pruned. If the remaining count meets or exceeds the threshold, the detection fires.

ZADD key timestamp eventId ZREMRANGEBYSCORE key -inf cutoff count = ZCARD key trigger = count >= countThreshold

Inputs: countThreshold, windowMinutes

Balance

Templates that poll on-chain balances and alert on drops, thresholds, or anomalies.

Fund Drainage

freecritical

Token or native balance dropping by X% or more within a rolling time window.

How it works

The state poller records balance snapshots at each poll interval. Within the configured window, the maximum recorded balance is compared to the current balance. If the percentage drop (in basis points) meets or exceeds the threshold, the alert fires.

dropBps = ((maxInWindow - current) * 10000) / maxInWindow trigger = dropBps >= dropPercent * 100

Inputs: dropPercent, windowMinutes, tokenAddress (optional)

Balance Low

freemedium

Token or native balance falling below an absolute threshold value.

How it works

The state poller reads the current balance via eth_getBalance or an ERC-20 balanceOf call. If the value is less than the configured minimum, the detection fires.

trigger = current < BigInt(minBalance)

Inputs: minBalance, tokenAddress (optional)

Native Balance Anomaly

freehigh

Bidirectional native balance changes — both sudden drops and unexpected surges — beyond a percentage threshold.

How it works

Tracks the maximum and minimum native balance within a rolling window. Computes both a drop percentage (from the max) and a rise percentage (from the min). If either exceeds the configured threshold, the detection fires.

Drop: dropBps = ((max - current) * 10000) / max Rise: riseBps = ((current - min) * 10000) / min trigger = dropBps >= threshold OR riseBps >= threshold

Inputs: dropPercent, windowMinutes, pollIntervalMs

Governance

Templates that detect ownership changes, role mutations, proxy upgrades, pause toggles, and storage-level state changes.

Ownership Transfer

freecritical

OwnershipTransferred and OwnershipTransferStarted events from OpenZeppelin-style Ownable contracts.

How it works

Listens for both OwnershipTransferred(address,address) and OwnershipTransferStarted(address,address) event signatures. Any match triggers an alert immediately with the previous and new owner addresses.

Inputs: none

Role Change

freehigh

RoleGranted and RoleRevoked events from AccessControl-style contracts, with optional filtering to a specific role hash.

How it works

Listens for RoleGranted(bytes32,address,address) and RoleRevoked(bytes32,address,address) events. If a roleHash filter is provided, only events matching that specific role trigger an alert.

trigger = decodedArgs.role == roleHash (if provided)

Inputs: roleHash (optional)

Proxy Upgrade (Event)

freecritical

Upgraded(address) events emitted by UUPS and Transparent proxies during an upgrade.

How it works

Listens for the Upgraded(address) event signature defined by ERC-1967. Any match triggers an alert with the new implementation address.

Inputs: none

Multisig Signer Change

freehigh

AddedOwner and RemovedOwner events from Gnosis Safe / Safe multisig wallets.

How it works

Listens for AddedOwner(address) and RemovedOwner(address) event signatures used by Safe contracts. Any match triggers an alert with the affected signer address.

Inputs: none

Pause State Change

freehigh

Paused and Unpaused events from OpenZeppelin Pausable contracts.

How it works

Listens for Paused(address) and Unpaused(address) event signatures. Any match triggers an alert indicating the contract was paused or unpaused and by whom.

Inputs: none

Proxy Upgrade (Storage Slot)

freecritical

Changes to the ERC-1967 implementation storage slot, including silent upgrades that bypass event logs.

How it works

Polls the well-known ERC-1967 implementation slot via eth_getStorageAt at a configurable interval. Compares the current 32-byte value to the previously recorded value. If they differ, the detection fires.

slot = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc current = eth_getStorageAt(address, slot) trigger = current !== previous

Inputs: pollIntervalMs (optional, default 60s)

Storage Anomaly

prohigh

A storage slot value suddenly deviating from its rolling average, indicating potential parameter manipulation.

How it works

Maintains a rolling window of the last 100 storage snapshots. Computes the mean and measures the percentage deviation of the current value. If the deviation exceeds the configured threshold, the detection fires.

mean = sum(last 100 snapshots) / 100 diff = |current - mean| deviation = (diff * 100) / mean trigger = deviation >= percentThreshold

Inputs: slot, percentThreshold, pollIntervalMs

Custom

Flexible templates for monitoring any event, storage slot, view function, or transaction pattern with user-defined parameters.

Custom Event

freemedium

Any event matching a user-provided signature, with optional parameter filtering using comparison operators.

How it works

Listens for the event matching the provided signature hash. When decoded, optionally evaluates a single parameter against a filter condition (eq, gt, lt, gte, lte, neq).

trigger = decodedArgs[filterField] <op> filterValue

Inputs: eventSignature, eventName, filterField, filterOp, filterValue

Custom Storage Slot

prohigh

Changes or threshold crossings on any user-specified EVM storage slot.

How it works

Polls the provided storage slot via eth_getStorageAt. Evaluates one of three condition types: simple change detection, above-threshold, or below-threshold comparison.

"changed": trigger = current !== previous "threshold_above": trigger = BigInt(current) > BigInt(value) "threshold_below": trigger = BigInt(current) < BigInt(value)

Inputs: slot, conditionType, conditionValue, pollIntervalMs

Custom View Function

promedium

Changes or threshold crossings on the return value of any read-only contract function.

How it works

Calls the specified view function via eth_call at each poll interval. Decodes the return value and evaluates it against the configured condition: change detection, threshold comparison, or windowed percent change from a rolling mean.

current = eth_call(contract, encodedFunction) trigger depends on conditionType: "changed": current !== previous "threshold_above/below": BigInt comparison "windowed_percent_change": deviation from rolling mean

Inputs: functionSignature, args, returnType, conditionType, conditionValue, pollIntervalMs

Custom Function Call

freehigh

Top-level transactions calling a specific function on the monitored contract.

How it works

Computes the 4-byte function selector from the provided signature via keccak256. For each transaction to the monitored address, compares the first 4 bytes of tx.input against the selector. Optionally filters on decoded parameters.

selector = keccak256(signature)[0:4] match = tx.input[0:4] == selector AND tx.to == contract

Inputs: functionSignature, functionName, filterField, filterOp, filterValue

Custom Event Frequency

promedium

A user-defined event being emitted N or more times within a rolling window, optionally grouped by a parameter.

How it works

Uses the same Redis sorted-set windowed counting mechanism as Repeated Transfer, but for any event signature. Optionally groups counts by a decoded parameter field.

ZADD key timestamp eventId ZREMRANGEBYSCORE key -inf cutoff count = ZCARD key trigger = count >= countThreshold

Inputs: eventSignature, eventName, countThreshold, windowMinutes, groupByField

Event Frequency Spike

prohigh

A sudden spike in event emission rate compared to a longer baseline period.

How it works

Maintains two sliding windows: a short observation window and a longer baseline window. Computes the average rate over the baseline, then measures the percentage increase of the observation count over that baseline average. Fires if the spike percentage exceeds the threshold and the baseline has sufficient data.

baseline_avg = baseline_count / (baselineMs / observationMs) spike% = ((observation_count - baseline_avg) / baseline_avg) * 100 trigger = spike% >= increasePercent AND baseline >= min

Inputs: eventSignature, observationMinutes, baselineMinutes, increasePercent, minBaselineCount

Summary Table

All 18 templates at a glance — mechanism, expected latency, and the primary RPC method used.

TemplateMechanismLatencyRPC Method
Large TransferEvent logSame blocketh_getLogs
Repeated TransferEvent log + Redis windowSame blocketh_getLogs
Fund DrainageBalance pollingPoll intervaleth_getBalance / eth_call
Balance LowBalance pollingPoll intervaleth_getBalance / eth_call
Native Balance AnomalyBalance pollingPoll intervaleth_getBalance
Ownership TransferEvent logSame blocketh_getLogs
Role ChangeEvent logSame blocketh_getLogs
Proxy Upgrade (Event)Event logSame blocketh_getLogs
Multisig Signer ChangeEvent logSame blocketh_getLogs
Pause State ChangeEvent logSame blocketh_getLogs
Proxy Upgrade (Storage Slot)Storage pollingPoll intervaleth_getStorageAt
Storage AnomalyStorage polling + rolling avgPoll intervaleth_getStorageAt
Custom EventEvent logSame blocketh_getLogs
Custom Storage SlotStorage pollingPoll intervaleth_getStorageAt
Custom View Functioneth_call pollingPoll intervaleth_call
Custom Function CallTransaction matchingSame blocketh_getLogs (trace)
Custom Event FrequencyEvent log + Redis windowSame blocketh_getLogs
Event Frequency SpikeEvent log + dual windowSame blocketh_getLogs

Known Limitations

Transparency means being upfront about what these templates cannot do.

Polling latency is not instant — storage, balance, and view-function templates depend on poll intervals. A change between polls is detected at the next poll, not at the moment it occurs. Default interval is 60 seconds.

Event templates miss silent writes — if a contract modifies state without emitting an event, event-based templates will not detect it. Use storage slot monitoring as a complementary layer.

Windowed templates need warm-up — Fund Drainage, Native Balance Anomaly, Storage Anomaly, and Event Frequency Spike all rely on historical data. They will not fire until enough snapshots or events have been collected to fill the baseline window.

BigInt precision, not floating point — all threshold comparisons use BigInt arithmetic (integer math). Percentage calculations use basis points (10000 = 100%) to avoid rounding errors. Thresholds must be specified in the token's smallest unit (e.g. wei, not ETH).

RPC provider reliability — all templates depend on RPC responses. If a provider is down or returns stale data, detection may be delayed. ChainAlert uses redundant providers and automatic failover, but brief gaps are possible.

Cooldowns suppress duplicates — after an alert fires, subsequent matches within the cooldown window are suppressed. This prevents alert fatigue but means rapid repeat incidents may only generate one notification.

Internal transactions are not covered — Custom Function Call matches top-level tx.input data only. Function calls made via internal transactions (contract-to-contract) are not matched by this template.