From Building Workflows to Breaking Them

Eman Herawy
Eman Herawy
Smart Contract Dev & Auditor

At HackenProof, we believe that some of the most valuable security knowledge is created inside the hacker community itself. This belief is reflected in our ongoing series of guest articles, where security researchers from our community share practical insights, practical knowledge, and real-world lessons from their work in smart contract security, Web3 development, and bug bounty research. By publishing hacker-authored content, we aim to make expert-level security knowledge more accessible and to support continuous learning across the broader Web3 security ecosystem.

We regularly curate and publish the strongest technical articles based on their educational value, technical depth, and relevance to real security challenges. Authors whose work is published on the HackenProof blog receive the Star Author achievement, recognizing their contribution to knowledge sharing and community growth.

Read the article, explore the ideas, and share your thoughts with the community — and if you have expertise to share, this could be your first step toward becoming our next Star Author.

Background and Scope

This article was written by Eman Herawy, a blockchain developer and smart contract security auditor from Egypt, and a member of the HackenProof community since January 6, 2023. With over six years of hands-on experience designing, building, and securing decentralized applications, she brings a rare combination of builder intuition and auditor precision. Her expertise spans Solidity, Rust, and emerging languages such as Move, enabling her to deliver robust, multi-chain solutions. Eman has won more than ten international Web3 hackathons — including three focused specifically on blockchain security tooling — demonstrating both technical depth and creative problem-solving under pressure.

In a Web3 security landscape where female researchers are still underrepresented, her voice adds meaningful diversity to the field and highlights the importance of broader participation in technical security research. We selected this article because it explores CRE security through real production risks, focusing on determinism pitfalls, consensus edge cases, and workflow-level vulnerabilities that matter both to developers building systems and auditors reviewing them.

🖊 Editor’s note: This article was originally published by the author on LinkedIn and is shared here with permission as part of our community guest article series.

Foreword

I live in two worlds: as a developer obsessed with hackathons (they’re how I feed my curiosity and force myself to learn fast) and as a security auditor, which changes how I look at systems. It pushes me to think more about how things break and what it actually takes to build something secure.

Here, I’m approaching CRE as a security auditor. I’ll walk through:

  • Potential mistakes developers are likely to make
  • Why those mistakes matter
  • What both builders and auditors should look for when reviewing CRE workflows

If you’re building with CRE, think of this as your early warning system: the things I’d flag in an audit before they cost you money or credibility.

If you’re auditing CRE projects, treat this as your checklist for the gap between “it compiles” and “it’s actually secure.”

Understanding the Foundation

Before diving into what breaks, it’s important to understand what makes CRE different.

Consensus Computing

Consensus computing means that a decentralized network of nodes must agree on the result of executing code before that result is accepted.

CRE does not run on one machine. It runs on many, simultaneously.

When your workflow calls an API or reads from a blockchain, multiple independent nodes execute the same operation. Each node produces its own result. Those results are compared, and if enough nodes agree (quorum), the result is accepted. If they do not, the workflow fails.

This provides:

  • Tamper resistance: no single node can manipulate results
  • High availability: the system continues operating despite node failures
  • Trust minimization: no single operator must be trusted
  • Verifiability: results are cryptographically verified

This process happens automatically for every capability call. You do not write consensus logic yourself. It is built into the CRE runtime.

There is a critical caveat: consensus only works if every node executes identical logic.

Determinism

Determinism means that given the same inputs, the code produces the exact same outputs. Every time. Everywhere.

Your workflow does not run once. It runs in parallel on multiple nodes. For consensus to succeed:

  • every node must execute the same steps
  • in the same order
  • with the same inputs
  • and produce the same intermediate results

If any step differs, nodes cannot agree.

The key connection:

  • Determinism enables consensus
  • Non-determinism breaks consensus

Determinism isn’t a feature. It’s a prerequisite. Consensus computing only works if the computation is deterministic. If nodes don’t execute exactly the same logic, they can’t agree, even if they’re honest.

DON Mode vs. Node Mode: Understanding execution contexts

The key difference between these modes is who is responsible for creating a single, trusted result from the work of many nodes.

Runtime<C> (DON Mode) — The Default:

This represents the DON’s (Decentralized Oracle Network) execution context. It’s passed to your main trigger callback.

When to use: For operations that are already guaranteed to be Byzantine Fault Tolerant (BFT).

When you use the Runtime, you ask the network to execute something, and CRE handles the underlying complexity to ensure you get back one final, secure, and trustworthy result.

Common use cases:

  • Writing transactions to a blockchain with the EVM client
  • Accessing secrets
  • Operations where CRE can automatically provide BFT guarantees

NodeRuntime<C> (Node Mode):

This represents an individual node’s execution context.

When to use: When a BFT guarantee cannot be provided automatically (e.g., calling a third-party API).

You tell each node to perform a task on its own, and each node returns its own individual answer. You are then responsible for telling the SDK how to combine them into a single, trusted result by providing a consensus and aggregation algorithm.

Potential vulnerabilities and best practices

Determinism is easy to break

When workflows run, all nodes must generate identical request IDs for capability calls. If code paths diverge, request IDs differ, quorum is not reached, and the workflow fails.

The failure pattern is simple:

Code diverges → different request IDs → no quorum → workflow fails

Common sources of non-determinism

1. Object iteration

JavaScript objects do not guarantee key order by specification. While modern engines preserve insertion order, relying on this behavior can cause subtle bugs across different runtimes or during JSON serialization.

Wrong:

const obj = { b: 2, a: 1 }
for (const key in obj) {
  console.log(key) // Order may vary
}

Correct:

const obj = { b: 2, a: 1 }
for (const key of Object.keys(obj).sort()) {
  console.log(key, obj[key]) // Always alphabetical
}

Maps and Sets preserve insertion order by specification, making them safe for deterministic iteration when order matters.

2. Promise handling and the .result() pattern

SDK capabilities use the .result() pattern instead of async/await. When working with multiple operations, the order you call .result() must be deterministic.

Never use:

const fastest = await Promise.race([fetchFromAPI1(), fetchFromAPI2()])
const firstSuccess = await Promise.any([fetchFromAPI1(), fetchFromAPI2()])

These methods introduce non-determinism because different nodes may “win” the race or succeed with different sources.

Instead, call .result() in a fixed, deterministic order:

import { cre, type Runtime, type NodeRuntime, consensusMedianAggregation } from "@chainlink/cre-sdk"
// Fetch from API 1, then API 2, in a fixed order
const fetchPrice = (nodeRuntime: NodeRuntime<Config>): bigint => {
  const httpClient = new cre.capabilities.HTTPClient()
  // Try first API
  const response1 = httpClient
    .sendRequest(nodeRuntime, {
      url: "<https://api1.example.com/price>",
    })
    .result()
  // If first API succeeds, use it; otherwise try second API
  if (response1.statusCode === 200) {
    return parsePriceFromResponse(response1)
  }
  // Try second API as fallback (deterministic order)
  const response2 = httpClient
    .sendRequest(nodeRuntime, {
      url: "<https://api2.example.com/price>",
    })
    .result()
  return parsePriceFromResponse(response2)
}
// In your DON mode handler
const onTrigger = (runtime: Runtime<Config>): MyResult => {
  // Run the fetch logic in node mode with consensus
  const price = runtime.runInNodeMode(fetchPrice, consensusMedianAggregation<bigint>())().result()
  return { price }
}

3. Dates and times

Never use JavaScript’s built-in time functions in DON mode. Nodes may have slightly different system clocks, causing divergence.

Wrong:

const now = Date.now()
const timestamp = new Date()

Correct:

const now = runtime.now() // Same timestamp across all nodes

runtime.now() returns DON Time, a consensus-derived timestamp that all nodes agree on.

4. Working with LLMs

LLMs generate different responses for the same prompt, even with temperature set to zero. This breaks consensus.

Instead, request structured output (JSON with specific fields) rather than free-form text, then use consensus aggregation on the structured fields. This allows nodes to agree on key data points even if exact text varies.

From Building Workflows to Breaking Them

Randomness must be consensus-safe

Standard randomness breaks determinism. If each node generates its own random value using Math.random(), consensus is impossible.

CRE provides deterministic randomness:

  • In DON mode, runtime.random() produces identical sequences across nodes
  • In Node mode, nodeRuntime.random() produces per-node randomness suitable for later aggregation

Never use JavaScript’s built-in randomness. Always use the runtime-provided generator.

Local simulation does not catch this. Simulation runs with a single-node model. It validates SDK integration but cannot detect non-determinism or multi-node consensus failures. Passing simulation does not imply production safety.

Decentralized execution does not mean decentralized data

Calling a centralized API from a decentralized workflow does not make the data decentralized.

If every node calls the same API, owned by the same provider, behind the same infrastructure, all nodes can receive the same manipulated data or no data at all if the service is down.

Consensus only ensures nodes agree on what they observed. It does not guarantee the observation itself is trustworthy.

Using aggregation correctly

When you run code in Node mode, each node executes independently and returns its own result. CRE does not decide how those results are combined. In runInNodeMode, that responsibility is entirely yours.

You can aggregate node results using CRE’s built-in aggregation functions or by writing a custom aggregation function. In both cases, aggregation only answers one question: do enough nodes agree? It does not validate correctness. Nodes can reach perfect consensus on a result that is logically wrong.

CRE’s built-in aggregation functions encode assumptions about the data. They are safe only if those assumptions match reality.

  • Median aggregation consensusMedianAggregation<T>() is suitable for numeric values where outliers should be filtered, such as prices or measurements.
  • Identical aggregation consensusIdenticalAggregation<T>() should be used for values that must match exactly, like block hashes, transaction IDs, or addresses.
  • Common prefix and suffix aggregationconsensusCommonSuffixAggregation<T>() are useful for arrays where nodes may observe diverging sequences but still share a common beginning or end.
  • For structured data, ConsensusAggregationByFields<T>() allows each field to use an appropriate strategy, which is often safer than forcing a single aggregation rule onto the entire object.
From Building Workflows to Breaking Them

All built-in aggregation functions support .withDefault() to return a fallback value when consensus fails. Using a default is a design choice, not a best practice. In some workflows it’s safer than failing hard. In others, it hides real problems. Custom aggregation functions carry the same risk if failure cases are not handled explicitly.

Auditor’s takeaway: Aggregation determines how nodes agree, not whether the result is correct. When reviewing workflows, the key question is whether the chosen aggregation strategy actually matches the data being aggregated.

Validate everything you receive

A successful HTTP request does not guarantee valid data. If you do not validate responses, workflows may:

  • process error payloads as valid data
  • operate on undefined or null values
  • aggregate malformed responses
  • write garbage onchain

Always validate:

  • HTTP status codes
  • response schema
  • value ranges
  • presence of required fields
Agreement on bad data is still bad data.

Finality when reading from EVM

Reading from non-finalized blocks exposes your workflow to blockchain reorganizations (reorgs). Data that looks correct now might later be reversed. CRE provides three confidence levels for reading blockchain data:

  • LATEST : The most recent block, no finality guarantees. Useful for real-time dashboards or displays where speed matters more than certainty.
  • SAFE : A block unlikely to be reorganized, but not fully finalized. Good for monitoring or alerting when you need reasonable confidence. (Note: SAFE is not available for all chain reads.)
  • FINALIZED : Blocks considered irreversible. Use for critical operations, financial transactions, or anything where incorrect data could cause significant harm.
⚠️ Warning: If you don’t explicitly specify a block number, CRE defaults to LATEST. This can silently introduce risk.

Critical operations after EVM write

Chain write operations return a WriteReportReply when the transaction is included in a block, not when it reaches finality. If a block containing your transaction is reorganized:

  • CRE’s Transaction Manager (TXM) automatically resubmits your transaction
  • Gas bumping is applied as needed to ensure the transaction is included
If you need absolute certainty that your write transaction reached finality, implement post-write verification by reading the blockchain state after a custom number of confirmations. Do not rely solely on WriteReportReply for finality confirmation.

Multiple duplicate report submissions reaching your API.

When a workflow executes, all nodes in the DON attempt to send the report to your API. Each node generates its own unique cryptographic signature for the report. Because these signatures differ across nodes, traditional HTTP caching has limited effectiveness. The cache can’t match requests with different signatures.

To fix, use a two-layer defense strategy:

  1. Client-side caching (limited but essential): Always include cacheSettingsin your transformation function
  2. Server-side deduplication (required): Your API must implement deduplication using the hash of rawReport (keccak256(rawReport)) as the unique identifier

Unverified report data being processed by your API.

Unlike onchain submissions where the KeystoneForwarder contract automatically verifies signatures, HTTP endpoints receive raw data without cryptographic verification.

To fix, your receiving API must verify cryptographic signatures against DON public keys before trusting any report data. This is your responsibility. There’s no automatic verification layer for HTTP submissions.

Silent numeric bugs

One class of silent bugs that might get overlooked is about wrong numeric results caused by differences in how languages handle numbers.

CRE workflows today are written in Go and TypeScript, but they frequently read from and write to Solidity contracts, which use very different numeric semantics. The way each language represents and manipulates numbers is not uniform:

  • Solidity uses fixed-width integers like uint256, which have strict bounds and defined overflow/underflow behavior on-chain (reverts in 0.8.0+, wraps in earlier versions).
  • Go represents Solidity integers as big.Int in generated bindings, which supports arbitrary precision but has different behaviors for operations and overflows compared to fixed-width EVM integers.
  • TypeScript uses bigint for Solidity integer types in ABI encoding/decoding, but basic number types are floating-point and can silently lose precision.
  • Future SDKs may introduce new numeric models or type systems (Rust, Python, etc.).

These differences are inherent to the language runtimes, not bugs in CRE itself. If developers aren’t careful, math can silently:

  • Overflow or wrap in ways that differ from on-chain expectations
  • Lose precision in off-chain logic before being encoded for the EVM
  • Behave inconsistently between what the workflow calculates and what the smart contract expects

This doesn’t typically break consensus. Every node will compute the same value, but it can lead to agreements on the wrong value.

Going low-Level without a clear reason

CRE offers two ways to make HTTP requests in TypeScript workflows:

  • http.SendRequest (recommended)
  • cre.RunInNodeMode (lower-level, manual)

If your use case fits a standard request/response model, http.SendRequestshould be your default choice. It handles deterministic request construction, consensus-friendly execution, and proper interaction with the DON, reducing the surface area for subtle bugs.

Using RunInNodeMode provides more flexibility, but it also shifts responsibility to you:

  • ensuring requests are identical across nodes,
  • selecting and applying the correct aggregation strategy,
  • preventing duplicate side effects.

Forgetting cache settings for HTTP requests

By default, every node in the DON executes the same HTTP request independently. For GET requests, this is usually harmless. For non-idempotent requests — such as POST, PUT, PATCH, or DELETE — it introduces real risk.

Without cache settings, the same request may be executed multiple times, even though consensus succeeds and the workflow completes successfully.

From CRE’s perspective, nothing is wrong. From the external system’s perspective, the same action just happened more than once.

How CacheSettings Help

cacheSettings enables a shared cache across the DON:

  • one node executes the request,
  • other nodes reuse the cached response,
  • side effects occur once instead of many times.

A typical configuration looks like:

cache: {
  readFromCache: true,
  maxAgeMs: 60000
}

Important Caveats

  • Caching is best-effort, not a guarantee. Duplicate execution can still occur due to network failures, routing changes, or nodes hitting different gateway instances.
  • Choosing maxAgeMs matters. For write operations, it should be slightly longer than the workflow execution time.For read operations, it should reflect acceptable staleness.To force fresh execution, set it to 0 or omit caching entirely.
  • Caching also requires fully deterministic requests. The URL, headers, and body must be byte-for-byte identical across nodes. Non-deterministic values like Date.now() will break caching.
  • For critical operations, caching should always be combined with API-level idempotency keys, since cache behavior alone cannot guarantee single execution
From Building Workflows to Breaking Them

Conclusion

CRE’s security model is powerful because it is built on consensus. But consensus is unforgiving. It requires strict determinism and cannot detect when all nodes agree on incorrect data.

The vulnerabilities outlined in this article are the gaps between “it compiles” and “it’s secure.” The mistakes that turn promising workflows into production failures.

For builders: Treat determinism as a hard requirement. Validate everything. Test with real multi-node deployments. Use the checklists above.

For auditors: Start with determinism, verify data validation, check finality handling, and never trust simulation results alone.

Share article:
More topics:

Read more on HackenProof Blog