> ## Documentation Index
> Fetch the complete documentation index at: https://docs.railnet.org/llms.txt
> Use this file to discover all available pages before exploring further.

# Policy engine

> On-chain guardrails for Advanced Strategies — contract whitelists, function permissions, and calldata checks

export const PolicyEngineFlow = () => {
  const CSS = `
    .pe-bg{fill:#F8F8F8;rx:4;ry:4}.dark .pe-bg{fill:#141414}
    .pe-t{fill:#374151;font-family:"Inter Tight","Inter",system-ui,sans-serif}.dark .pe-t{fill:#D1D5DB}
    .pe-tm{fill:#6B7280;font-family:"Inter Tight","Inter",system-ui,sans-serif}.dark .pe-tm{fill:#9CA3AF}
    .pe-h{fill:#111827;font-family:"Inter Tight","Inter",system-ui,sans-serif}.dark .pe-h{fill:#E5E7EB}
    .pe-serif{fill:#111827;font-family:"ABC Marist",Georgia,serif}.dark .pe-serif{fill:#E5E7EB}
    .pe-cont{fill:none;stroke:#D1D5DB;stroke-width:1;rx:6;ry:6;stroke-dasharray:8 4}.dark .pe-cont{stroke:#4B5563}
    .pe-card{fill:#fff;stroke:#D1D5DB;stroke-width:1;rx:4;ry:4}.dark .pe-card{fill:#1F2937;stroke:#4B5563}
    .pe-policy{fill:#F8EBE5;rx:4;ry:4}.dark .pe-policy{fill:#251C18}
    .pe-strat{fill:#fff;stroke:#D1D5DB;stroke-width:1;rx:4;ry:4}.dark .pe-strat{fill:#1F2937;stroke:#4B5563}
    .pe-sidebar{fill:#FDF8F5;stroke:#E5D1C4;stroke-width:1;rx:4;ry:4}.dark .pe-sidebar{fill:#1F1A17;stroke:#4B3D35}
    .pe-chk{fill:#2D6A5A}.dark .pe-chk{fill:#5AA08E}
    .pe-chk-line{stroke:#fff;stroke-width:1.5;fill:none;stroke-linecap:round;stroke-linejoin:round}
    .pe-ln{stroke:#ACACAC;fill:none;stroke-width:1.5;color:#ACACAC}.dark .pe-ln{stroke:#666;color:#666}
    .pe-lbl{fill:#9CA3AF;font-family:"Inter Tight","Inter",system-ui,sans-serif;font-style:italic}.dark .pe-lbl{fill:#6B7280}
    .pe-sep{stroke:#E5D1C4;stroke-width:0.75}.dark .pe-sep{stroke:#4B3D35}
    .pe-icon{fill:none;stroke:#9CA3AF;stroke-width:1.3;stroke-linecap:round;stroke-linejoin:round}.dark .pe-icon{stroke:#6B7280}
    .pe-dot{fill:#6B7280}.dark .pe-dot{fill:#9CA3AF}
  `;
  const chk = (cx, cy) => <g>
      <circle cx={cx} cy={cy} r="7" className="pe-chk" />
      <path d={`M${cx - 3},${cy + 0.5} L${cx - 0.5},${cy + 3} L${cx + 4},${cy - 3}`} className="pe-chk-line" />
    </g>;
  return <div className="not-prose w-full my-6 overflow-x-auto" style={{
    padding: '2px'
  }}>
      <style>{CSS}</style>
      <svg viewBox="0 0 820 370" style={{
    width: '100%',
    height: 'auto',
    minWidth: '620px',
    display: 'block'
  }} role="img" aria-label="Policy engine execution flow">
        <defs>
          <marker id="pe-a" viewBox="0 0 10 7" refX="9" refY="3.5" markerWidth="8" markerHeight="6" orient="auto"><polygon points="0 0,10 3.5,0 7" fill="currentColor" /></marker>
        </defs>
        <rect x="0" y="0" width="820" height="370" className="pe-bg" />

        <text x="38" y="16" className="pe-tm" fontSize="10" fontWeight="500">Your Strategy Engine</text>
        <rect x="20" y="24" width="455" height="148" className="pe-cont" />

        <rect x="38" y="38" width="202" height="50" className="pe-card" />
        <g transform="translate(50, 50)">
          <line x1="0" y1="0" x2="14" y2="0" className="pe-icon" />
          <circle cx="4" cy="0" r="2" className="pe-dot" />
          <line x1="0" y1="7" x2="14" y2="7" className="pe-icon" />
          <circle cx="10" cy="7" r="2" className="pe-dot" />
        </g>
        <text x="74" y="60" className="pe-h" fontSize="11" fontWeight="600">Strategy Logic</text>
        <text x="74" y="73" className="pe-tm" fontSize="9">Market data in, signals out</text>

        <rect x="254" y="38" width="203" height="50" className="pe-card" />
        <g className="pe-icon" transform="translate(266, 48)">
          <path d="M7,0 L14,3.5 L14,8.5 C14,12.5 7,15 7,15 C7,15 0,12.5 0,8.5 L0,3.5 Z" />
          <path d="M4.5,7 L6.5,9 L10,5.5" />
        </g>
        <text x="290" y="60" className="pe-h" fontSize="11" fontWeight="600">Risk Constraints</text>
        <text x="290" y="73" className="pe-tm" fontSize="9">Your internal guardrails</text>

        <line x1="248" y1="88" x2="248" y2="104" className="pe-ln" markerEnd="url(#pe-a)" />
        <text x="258" y="100" className="pe-lbl" fontSize="9.5">signal</text>

        <rect x="38" y="108" width="419" height="48" className="pe-card" />
        <g className="pe-icon" transform="translate(52, 120)">
          <path d="M10,0 L14,4 L5,13 L0,14 L1,9 Z" />
          <line x1="8.5" y1="1.5" x2="12.5" y2="5.5" />
        </g>
        <text x="76" y="130" className="pe-h" fontSize="11" fontWeight="600">Transaction Crafting and Signing</text>
        <text x="76" y="143" className="pe-tm" fontSize="9">Your preferred wallet infrastructure and approval flow</text>

        <line x1="248" y1="172" x2="248" y2="196" className="pe-ln" markerEnd="url(#pe-a)" />
        <text x="258" y="188" className="pe-lbl" fontSize="9.5">transaction</text>

        <rect x="20" y="200" width="455" height="88" className="pe-policy" />
        <g transform="translate(35, 210)">
          <path d="M8,0 L16,4 L16,11 C16,16 8,19 8,19 C8,19 0,16 0,11 L0,4 Z" fill="#CC3F00" opacity="0.35" />
        </g>
        <text x="58" y="225" className="pe-h" fontSize="12" fontWeight="700">Railnet Onchain Policy Engine</text>
        <circle cx="44" cy="246" r="2" className="pe-dot" />
        <text x="52" y="250" className="pe-t" fontSize="10">Function-level permissions</text>
        <circle cx="44" cy="262" r="2" className="pe-dot" />
        <text x="52" y="266" className="pe-t" fontSize="10">Spending limits and parameter constraints</text>
        <circle cx="44" cy="278" r="2" className="pe-dot" />
        <text x="52" y="282" className="pe-t" fontSize="10">Multi-chain rule sets</text>

        <line x1="248" y1="288" x2="248" y2="312" className="pe-ln" markerEnd="url(#pe-a)" />
        <text x="258" y="306" className="pe-lbl" fontSize="9.5">approved</text>

        <rect x="80" y="316" width="335" height="38" className="pe-strat" />
        <g className="pe-icon" transform="translate(94, 326)">
          <path d="M7,0 L14,4 V5 L0,5 V4 Z" />
          <line x1="3" y1="7" x2="3" y2="14" />
          <line x1="7" y1="7" x2="7" y2="14" />
          <line x1="11" y1="7" x2="11" y2="14" />
          <line x1="0" y1="14" x2="14" y2="14" />
        </g>
        <text x="118" y="340" className="pe-h" fontSize="11.5" fontWeight="600">Strategy</text>
        <text x="177" y="340" className="pe-tm" fontSize="10">execute authorized operations</text>

        <rect x="510" y="55" width="298" height="260" className="pe-sidebar" />
        <text x="530" y="79" className="pe-tm" fontSize="10" fontWeight="500">Operational Benefits</text>
        <text x="530" y="101" className="pe-serif" fontSize="16" fontWeight="400">For Asset Managers</text>
        <line x1="530" y1="113" x2="790" y2="113" className="pe-sep" />

        {chk(542, 137)}
        <text x="558" y="133" className="pe-t" fontSize="10" fontWeight="500">Connect your existing operational</text>
        <text x="558" y="147" className="pe-t" fontSize="10" fontWeight="500">process, signers and review flow</text>

        {chk(542, 177)}
        <text x="558" y="173" className="pe-t" fontSize="10" fontWeight="500">Onchain policy engine defines</text>
        <text x="558" y="187" className="pe-t" fontSize="10" fontWeight="500">spending limits and authorized actions</text>

        {chk(542, 217)}
        <text x="558" y="213" className="pe-t" fontSize="10" fontWeight="500">Engine automatically rejects operations</text>
        <text x="558" y="227" className="pe-t" fontSize="10" fontWeight="500">outside the strategy mandate</text>

        {chk(542, 257)}
        <text x="558" y="253" className="pe-t" fontSize="10" fontWeight="500">Auditable policy engine with</text>
        <text x="558" y="267" className="pe-t" fontSize="10" fontWeight="500">constrained execution rules</text>
      </svg>
    </div>;
};

<Info>In Railnet Advanced Strategies, the policy engine is a **Zodiac Roles Modifier v2** contract enabled as a module on the Strategy. Every rule is transparent, auditable, and verifiable on-chain.</Info>

The policy engine is the on-chain governance layer that defines what an operator can and cannot do. There are no off-chain co-signers, no black-box policy services. Every constraint is visible on-chain and verifiable by depositors, platforms, and auditors.

Traditional wallet infrastructure relies on web2 co-signer services for policy enforcement. These systems are not publicly auditable, not verifiable on-chain, and not standardized. The policy engine replaces that trust assumption with deterministic, on-chain rule enforcement.

## How it works

An operator (the "member signer") never calls the Strategy directly. Every transaction routes through the policy engine, which checks the operator's role and validates every parameter before forwarding the call.

<PolicyEngineFlow />

The operator builds their protocol calldata as usual (e.g., an Aave `supply` call), wraps it in a single `execTransactionWithRole` call, and sends it to the policy engine address with the associated role key. The policy engine validates the call against the operator's role permissions. If all checks pass, the policy engine forwards the call through the Strategy, which executes it on the target protocol.

From the protocol's perspective, `msg.sender == STRATEGY_ADDRESS`. The target contract sees no difference between a call from the policy engine and a direct call from the Safe. The operator's signer address is never visible to the target protocol.

<Tip>The operator's existing execution pipeline stays unchanged. The integration is one extra ABI-encoding step and a change to the `to` address. See [Operate an Advanced Strategy](/strategies/advanced/operate) for the full integration guide.</Tip>

## Permission layers

The policy engine enforces four layers of constraints, each narrowing what an operator can do. Every layer is configured on-chain and readable by anyone.

### Contract whitelists

The first layer controls which contracts the operator can interact with. Only explicitly whitelisted target addresses are allowed — any call to a non-whitelisted contract reverts.

For example, an operator managing a lending strategy might have three whitelisted targets: the Aave V3 Pool, the USDC token contract (for approvals), and the Morpho Blue contract. Any attempt to call a contract outside this set fails at the policy engine before reaching the Strategy.

### Function-level permissions

The second layer controls which functions on whitelisted contracts the operator can call. Each function is identified by its 4-byte selector.

Functions are categorized by risk level, which determines their default state:

| Risk level | Default state                  | Examples                                |
| ---------- | ------------------------------ | --------------------------------------- |
| Low        | Enabled                        | `withdraw`, `repay`, `claimWithdrawals` |
| Medium     | Enabled (with spending limits) | `supply`, `borrow`, `swap`              |
| High       | Disabled                       | `liquidate`, `absorb`, admin functions  |

Low-risk functions like `withdraw` and `repay` are enabled by default because they move funds back to the Strategy. Medium-risk functions like `supply` and `borrow` are enabled but prompt for spending limits. High-risk functions like `liquidate` and admin functions are disabled by default and require explicit opt-in.

<Warning>Disabling a function at the policy engine level means the operator cannot call it at all, regardless of other permissions. The function selector is simply not in the allowed set.</Warning>

### Calldata-level checks

The third layer inspects the actual parameter values in each function call. The policy engine defines condition trees using these operators:

| Operator          | What it checks                              | Example use                                                  |
| ----------------- | ------------------------------------------- | ------------------------------------------------------------ |
| `EqualTo`         | Parameter must match an exact value         | Restrict to specific tokens or pool IDs                      |
| `EqualToAvatar`   | Parameter must equal the Strategy address   | Lock recipients so funds cannot leave the Strategy           |
| `GreaterThan`     | Parameter must exceed a threshold           | Enforce minimum slippage protection (`amountOutMinimum > 0`) |
| `LessThan`        | Parameter must be below a threshold         | Cap individual transaction sizes                             |
| `WithinAllowance` | Parameter must fit within a spending budget | Rate-limit operations with periodic refills                  |

These operators combine to express precise constraints on every parameter. Three examples:

<AccordionGroup>
  <Accordion title="Recipient locking">
    The most critical constraint. Any parameter named `to`, `recipient`, `receiver`, `onBehalfOf`, or `owner` is constrained to `EqualToAvatar` — meaning it must equal the Strategy's own address. This prevents the operator from sending funds to any external address.

    This constraint is non-overridable. It applies automatically to every function that has a recipient-like parameter, and the operator cannot remove it.

    ```
    supply(address asset, uint256 amount, address onBehalfOf, uint16 referralCode)
                                                 ^
                                          must == Strategy address
    ```
  </Accordion>

  <Accordion title="Token restrictions">
    The `asset` or `tokenIn` parameter is constrained to a specific set of approved tokens using `EqualTo`. The operator can only interact with tokens explicitly listed in the policy — for example, USDC and WETH but not any arbitrary ERC-20.

    ```
    exactInputSingle(ExactInputSingleParams)
        tokenIn: EqualTo(USDC) OR EqualTo(WETH)    // only approved tokens
        recipient: EqualToAvatar                     // locked to Strategy
    ```
  </Accordion>

  <Accordion title="Slippage protection">
    Parameters like `amountOutMinimum` are constrained with `GreaterThan(0)` to prevent zero-slippage swaps that could be exploited by MEV bots. The operator can set a higher minimum, but cannot set it to zero.
  </Accordion>
</AccordionGroup>

#### Non-overridable constraints

Three calldata constraints are safety-critical and cannot be removed by anyone, including the guardian:

1. **Recipient = Strategy address** — prevents funds from being sent to external addresses
2. **Approve spender = known protocol contract** — prevents arbitrary token approvals
3. **delegatecall = false** — always disabled on all permissions, preventing code injection

### Spending limits and allowances

The fourth layer rate-limits how much capital the operator can deploy within a time period. Spending limits use the `WithinAllowance` operator with a periodic refill mechanism.

Each allowance is defined by:

* **Refill amount** — how much budget is restored each period (e.g., 100,000 USDC)
* **Period** — how often the budget refills (daily, weekly, or monthly)
* **Max accrual** — the maximum budget that can accumulate (prevents rollover)

When the operator executes a transaction, the amount parameter is deducted from the allowance balance. If the balance is insufficient, the transaction reverts. The balance refills automatically at the start of each period.

<Tip>Spending limits apply to inbound operations only — `supply`, `deposit`, `borrow`, and swap inputs. Exit operations like `withdraw`, `repay`, and `claim` are unconstrained because they move funds back to the Strategy.</Tip>

Allowances can be scoped per function, per protocol, or shared across multiple functions:

| Scope           | Allowance key example     | Effect                                      |
| --------------- | ------------------------- | ------------------------------------------- |
| Per-protocol    | `aave_v3_daily`           | Caps all Aave V3 operations combined        |
| Per-route       | `eth_arb_usdc_daily`      | Caps a specific bridge route                |
| Global outbound | `eth_outbound_usdc_daily` | Caps all outbound operations from one chain |

When multiple allowances apply (e.g., a per-route cap and a global cap), both are deducted on each transaction. If either is exhausted, the transaction reverts.

## Cross-chain policies

Advanced Strategies can operate across multiple EVM chains using the same Safe address, deployed deterministically via CREATE2. Each chain has its own Roles Modifier instance with its own set of permissions.

```
            Same Safe address (0xSafe...) on all chains
            ============================================

  Ethereum          Arbitrum          Base           Optimism
  Safe + Roles      Safe + Roles      Safe + Roles   Safe + Roles
```

### How cross-chain scoping works

The Roles Modifier on each chain controls **outbound** operations only — bridge calls, token transfers, and cross-chain messages originating from that chain's Safe. Inbound bridged funds are permissionless. Anyone can send tokens to the Safe address, and the funds arrive without any Roles configuration.

For bridge operations, the policy engine enforces:

* **Bridge contract whitelist** — only approved bridge contracts (e.g., Across SpokePool) can be called
* **Recipient locked to Strategy address** — the `recipient` parameter in bridge calls must equal the Safe address, which is the same on all chains due to deterministic deployment
* **Approved destination chains** — the operator can only bridge to explicitly allowed chain IDs
* **Per-route spending caps** — each source-destination pair has its own daily allowance (e.g., `eth_arb_usdc_daily`)

<Tip>Because the Safe has the same address on all chains, locking the bridge recipient to `EqualToAvatar` guarantees funds arrive at the organization's own Safe on the destination chain. Even if an operator key is compromised, bridged funds cannot leave the organization's wallets.</Tip>

### Per-chain, per-route spending caps

Each bridge route has an independent allowance. An operator bridging from Ethereum can have separate daily caps for Ethereum-to-Arbitrum, Ethereum-to-Base, and Ethereum-to-Optimism routes.

For stricter control, routes from the same source chain can share a single allowance key. This enforces a global outbound cap — the total bridged across all destinations from one chain cannot exceed the shared limit.

## Comparing guardrail models

Railnet offers two guardrail architectures. Allocation Strategies use the External Access Control (EAC) system built into Railnet's smart contracts. Advanced Strategies use the Zodiac Roles Modifier as an external policy engine.

| Aspect          | Advanced Strategy                          | Allocation Strategy            |
| --------------- | ------------------------------------------ | ------------------------------ |
| Engine          | Zodiac Roles Modifier v2                   | External Access Control (EAC)  |
| Scope           | Any contract call on any chain             | Railnet operational roles      |
| Granularity     | Calldata-level parameter checks            | Role-based access control      |
| Spending limits | Per-function allowances with period refill | Allocation queue targets       |
| Governance      | Guardian timelock                          | Admin role retention           |
| Visibility      | All rules on-chain, verifiable             | All roles on-chain, verifiable |

Both models enforce constraints on-chain with no off-chain trust assumptions. The choice depends on what the strategy needs to do: Allocation Strategies work within Railnet's standard yield source framework, while Advanced Strategies interact with arbitrary contracts across any EVM chain.

## What to read next

<CardGroup cols={2}>
  <Card title="Operate an Advanced Strategy" icon="terminal" href="/strategies/advanced/operate">
    Integration guide for routing transactions through the policy engine.
  </Card>

  <Card title="Advanced Strategies overview" icon="layer-group" href="/strategies/advanced">
    What Advanced Strategies are, why they exist, and how they connect to Railnet.
  </Card>

  <Card title="Allocation Strategy guardrails" icon="shield" href="/strategies/allocation/guardrails">
    Role-based access control for standard Allocation Strategies.
  </Card>

  <Card title="Risk management" icon="shield-check" href="/strategies/risk-management">
    Portfolio-level risk parameters, circuit breakers, and monitoring.
  </Card>
</CardGroup>
