Skip to content

Rules Engine — public, mandatory, on-chain policy

A blockchain transaction is only half the story. The other half is: under what rules was this allowed to happen? On most platforms that policy lives in private backend code you can’t see and can’t audit. Smart Engines inverts that. The rules are public on chain, mandatory, and enforced at the one place that matters — the signing gate.


Public

Rules are published to a Hedera Consensus Service (HCS) topic — a public, append-only log. Anyone can read the rules an entity is bound to.

Mandatory

Enforcement is fail-closed at the signing gate. There is no “skip the rules” path — a missing reader, malformed memo, malformed rule, or rule mismatch all deny.

On-chain anchored

The governing rule is anchored to the entity’s immutable creation-transaction record on its own chain of origin. The validator’s local copy is treated as a cache, not the authority.


Where enforcement actually happens: the sign gate

Section titled “Where enforcement actually happens: the sign gate”

There is a single canonical enforcement point, the sign gate, and every chain’s signing path calls it before releasing a signature, a DKG share or an approval.

Because enforcement lives at the signing layer — not at an HTTP endpoint or in app code — it cannot be routed around. A validator simply will not contribute its threshold-signature share unless the gate says allow. And since signing requires a threshold quorum, a single misbehaving validator that ignores the rules can’t push anything through — the honest validators refuse, so the signature never reaches threshold.

%%{init: {'sequence':{'showSequenceNumbers':false}}}%%
sequenceDiagram
    autonumber
    participant Req as Transaction request
    participant Gate as Sign gate (per validator)
    participant Chain as Source chain (entity's creation tx)
    participant HCS as Hedera HCS (canonical rules topic)
    participant TSS as Threshold signer

    Req->>Gate: share-request for entity E, op = transfer(...)
    Gate->>Chain: read E's creation-tx memo (immutable)
    Chain-->>Gate: memo → RuleRef (points at HCS topic + timestamp)
    Gate->>HCS: resolve canonical rule
    HCS-->>Gate: ValidatorRules (immutable, cached by consensus timestamp)
    Gate->>Gate: verify resolved rule == the validator's cached copy
    Gate->>Gate: classify op + evaluate rule
    alt allow
        Gate->>TSS: release partial signature share
    else deny (any failure → fail-closed)
        Gate-->>Req: { allowed: false } — no share released
    end

The flow:

  1. Resolve the entity’s on-chain address.
  2. Read the creation-transaction memo from the entity’s chain.
  3. Parse the memo into a RuleRef (always pointing at Hedera HCS — that’s where the canonical rules topic lives).
  4. Resolve the rule from HCS.
  5. Verify the resolved rule matches the validator’s cached copy — divergence means a possible forged record, so deny (defence in depth).
  6. Evaluate the on-chain-resolved rule.

The canonical rules themselves live on Hedera HCS — that’s the one public log everyone reads. But the anchor (the immutable on-chain pointer from an entity back to its governing rule) is stamped on the entity’s own chain of origin, at creation. On every supported chain, an entity is born with a tamper-evident reference to the rule that governs it.

Each chain commits the anchor with its native immutability primitive:

Chain How the rule-anchor is committed at creation
Hedera Live Immutable transaction memo on the create-account / create-token transaction (HCS topics carry it natively)
XRPL Live rule: memo on the SignerListSet
Stellar Coming next Memo.hash of the SHA-256 of the rule grammar (the full ref is committed as a digest — no truncation)
Polkadot Coming next On-chain remark / memo alongside the multisig setup
Solana Coming next Memo on the create transaction
Bitcoin Coming next OP_RETURN on the funding transaction (UTXO model has no create-tx, so the anchor rides the first funding spend)

The validator’s read side mirrors this: each chain has a reader that reads the anchor back off that chain, so the sign gate’s “re-derive from the birth certificate” check works identically no matter which ledger the entity lives on.


Rule resolution — immutable by construction

Section titled “Rule resolution — immutable by construction”

The validators fetch the canonical ValidatorRules for a RuleRef by its HCS consensus timestamp. Because HCS messages are immutable, a rule resolved for a given timestamp never changes — so it can be cached aggressively and the cache can never go stale.

Changing an entity’s policy therefore isn’t an in-place edit; it’s publishing a new immutable rule version to HCS. The old version remains in the public log forever. That’s what makes the policy history auditable: you can see every rule that was ever in effect, and exactly when.


Publishing a new rule version is one thing; deciding who is allowed to is another. Rule changes for an established entity don’t happen by operator fiat — they happen through a DAO governance vote. A passed dao_parameter_change proposal publishes the amended rule to HCS wrapped in an amendment envelope that names the proposal that authorized it and the exact field path it was permitted to change.

The sign gate never trusts that envelope at face value. It re-verifies the whole thing from on-chain facts before it will honor an amended rule.

The pointer is an untrusted hint. The entity’s current-rule pointer is replicated between validators; the gate treats it as a hint, not authority. To honor an amended head, the sign gate walks the chain backwards — amended head → … → the genesis rule anchored at the entity’s immutable creation-transaction memo — and proves every link before accepting any of it.

For each link, the gate requires:

  1. An envelope is present and well-formed (the nested rule still passes its own strict schema — a malformed rule fails closed).
  2. Same rules topic — the head must sit on the same HCS topic as genesis; no cross-topic substitution.
  3. Invariants hold — immutable fields and governance floors are unchanged across the step.
  4. Content-binding — the new rule must byte-for-byte equal the prior rule with the exact voted field set to the exact voted value, and nothing else changed. The code that builds an amendment and the code that verifies it share one field-setting function, so the two can’t drift.
  5. The proposal actually passed — the gate re-derives, from on-chain governance state alone, that the cited proposal cleared quorum and timelock and authorized this entity’s this field. A real-but-unrelated proposal can’t be reused as cover.

Only if every link in the walk passes does the gate evaluate the amended rule. Any gap — missing envelope, wrong topic, content that doesn’t match the vote, a proposal that didn’t pass — fails closed, exactly like every other branch of the gate. Genesis rules stay published bare, and both shapes decode the same way.

The cost of writing each amendment to Hedera is paid by the protocol Treasury — a cluster-controlled DKG-multisig account — not by any single validator’s keys.


The gate is fail-closed at every branch and never throws — it returns { allowed: false } so callers deny deliberately rather than crashing into an ambiguous state:

  • missing chain reader → deny
  • malformed memo → deny
  • unresolvable rule ref → deny
  • resolved rule ≠ the validator’s cached rule → deny
  • unreadable memo, or no rule published at creation → deny, unless the validator’s cached rule is exactly one of the built-in canonical templates the validator derives itself

Dry-run before you sign: POST /api/v3/baas/rules/simulate

Section titled “Dry-run before you sign: POST /api/v3/baas/rules/simulate”

You don’t have to broadcast a transaction to find out whether the rules would allow it. POST /api/v3/baas/rules/simulate (baas.rules.simulate in the SDK) evaluates a concrete action against a rule and returns a truthful accept / reject + reason — no signature, no chain write.

Crucially, the endpoint runs the same evaluator the validator’s sign gate uses against the real production predicates — maxPerTransaction, allowlist / blocklist, timeRange, and the rolling period caps (daily / weekly / monthly / total) when you supply the window totals. It is not a shape-check that rubber-stamps any schema-valid rule; it answers “would this 500-unit transfer be rejected by this account’s rule?” for real.

You can pass either a ruleRef (resolved from HCS) or an inline rule, plus the action and context:

%%{init: {'sequence':{'showSequenceNumbers':false}}}%%
sequenceDiagram
    autonumber
    participant SDK as SDK / smart-app
    participant Sim as "POST /api/v3/baas/rules/simulate"
    participant Eval as "Sign-gate evaluator"

    SDK->>Sim: "{ ruleRef | rule, action, context }"
    Sim->>Sim: resolve canonical ValidatorRules
    Sim->>Eval: classify op + evaluate against real predicates
    Eval-->>Sim: allow / deny + reason
    Sim-->>SDK: "{ isValid, reason }"

Like the gate, simulate is fail-closed: an unresolvable ref, a malformed rule, an unrecognised action, or any evaluation error returns { isValid: false, reason } — it never throws and never accidentally returns allow. (Period-capped rules deny a dry-run that omits context.windowSums, since you can’t claim “within the daily cap” without knowing the day’s spend.)


What gets published to HCS is a ValidatorRules document: a typed set of canonical fields, each of which the sign gate knows how to enforce. You build one with the SDK’s rule builders (Rules.forToken, Rules.forAccount, Rules.forTopic, Rules.forAgent) and can check it locally with Rules.validate before you publish.

Limits and windows

Per-transaction maximums, rolling daily / weekly / monthly / total caps, and time ranges.

Who and what

Allowlists and blocklists, recipient allowlists, and the action types an entity may perform.

Token behaviour

Token controls such as soulbound transfer restrictions, declared as fields the gate evaluates.

Each field is a named check. When a rule check refuses a request with a 403 rule_rejected response, the response names the checks that refused it in ruleAtoms, alongside a reason. A rule that carries modules — a composition format the sign gate does not enforce — is refused at publish and denied at the gate, so every control must be declared as a canonical field.

A couple of checks worth calling out for security:

  • DAO vote weight is read at the snapshot’s pinned timestamp, not “right now” — flash-loan resistance, so an attacker can’t borrow tokens for one block to qualify for a vote.
  • Soulbound tokens: a token rule with soulbound.enforced denies every transfer of the token decided under that rule — the token entity’s own transfers. Another holder’s transfer moves under that holder’s own rule, or under none outside the platform, so it does not stop a holder from transferring the NFT. Mint, burn and wipe are decided by their own operations.

Smart-agents are first-class rule subjects — and deny-by-default

Section titled “Smart-agents are first-class rule subjects — and deny-by-default”

Smart-agents (autonomous on-chain actors) are first-class canonical entities — they get their own DKG-multisig identity and their own canonical rules, gated by the same sign gate as any user account. An agent’s rule (type: 'agent') states exactly what actions the agent is permitted to take. There is no privileged bypass for automation.

The agent path is default-deny. When an agent is registered with no explicit rules, the default rule (buildDefaultAgentValidatorRules in the SDK) is inert on-chain, not merely amount-capped:

  • actionTypeRestrictions permits only transfer — mint, burn, and offer are denied outright by action-type.
  • recipientAllowlist permits only a sentinel address (__deny_all__) that no real chain account ever equals — so every transfer is denied too.

A freshly-registered agent can therefore move nothing until its owner configures a real recipient/action allow-list (or supplies explicit ValidatorRules at register time, which short-circuits the default). Deny-all is expressed within the non-empty-array schema — “allow only a value nothing matches” — and the sign gate enforces it like any other predicate.

The evaluator itself is default-deny on the agent arm, independently of the document: an agent rule that omits actionTypeRestrictions permits no operation, and one that omits recipientAllowlist permits no transfer destination. Leaving a field out withholds authority; it never grants it. An app may not publish (or bind an agent to) an agent rule that grants nothing this way: one with no actionTypeRestrictions, or one allowing transfer with no recipientAllowlist, is refused at publish, inline simulate, SDK build and agent creation.


A scripting engine (Lua, Starlark) is powerful but opens an attack surface, defeats static analysis, and makes hot-path performance unpredictable. Freeform JSON is unstructured and unauditable. Hard-coded TypeScript means every new product is a code change. A typed set of canonical fields is the deliberate sweet spot: declarative enough to publish on chain, typed enough to validate, fast enough for the signing hot path, and small enough that every field is enforced by the gate.


  • Rules are public (Hedera HCS), mandatory (fail-closed sign gate), and immutably anchored at each entity’s creation, on its own chain of origin — Hedera and XRPL today.
  • Enforcement lives at the sign gate — every signing path checks it before releasing a threshold-signature share. No bypass.
  • Rules are fetched by HCS consensus timestamp; immutability makes them cacheable and auditable. Policy changes = new published versions, never edits.
  • POST /api/v3/baas/rules/simulate runs the same evaluator as the sign gate for a real, fail-closed dry-run accept/reject — no signature, no chain write.
  • Rule changes for established entities go through a DAO vote, and the sign gate verifies each amendment is content-bound to the proposal that authorized it (walked back to the immutable genesis), with the HCS write paid by the protocol Treasury.
  • Rules are written as canonical fields in a ValidatorRules document; modules are refused.
  • Smart-agents are first-class, governed by the same gate — and deny-by-default: an unconfigured agent is inert on-chain (no recipient, no action) until its owner grants it explicit permissions.