# Newton Protocol Docs Newton Protocol is a decentralized policy engine for onchain transaction authorization, built as an EigenLayer AVS. # Overview \[Use Newton Protocol to add verifiable risk, compliance, and privacy guardrails to DeFi vault operations.] Vaults are DeFi primitives that let depositors place capital under a strategy managed by a curator, allocator, or automated system. They are increasingly used by institutions, exchanges, and DeFi applications to give users access to differentiated yield products without requiring every user to manage the strategy directly. Newton adds a verifiable policy layer to vaults. Instead of relying only on trust in a curator, frontend checks, or static onchain limits, a vault can require policy approval before sensitive actions are executed. ## Why Vaults Need Policy Enforcement Vault depositors trust that curators and allocators will make thoughtful decisions and will not misuse the authority granted to them. Existing vault infrastructure can restrict some operations onchain, but many risk decisions depend on changing offchain context: * Is the depositor address associated with sanctions, exploits, or high-risk activity? * Has a vault's APY, TVL, allocation, or risk score changed in a concerning way? * Is an oracle feed stale or diverging from an expected reference? * Does the action fit the vault's stated mandate and risk profile? Without verifiable policy enforcement, these checks often live in dashboards, runbooks, or centralized services. That makes them easy to bypass, difficult to audit, and hard for depositors to rely on. ## How Newton Helps Newton is a decentralized policy engine for transaction authorization. A vault action is represented as an intent, evaluated against a policy, and approved only when the Newton operator network returns a valid attestation. ```mermaid sequenceDiagram participant Curator as Curator or Allocator participant Vault as Vault / Shield participant Newton as Newton Policy Engine participant Data as Data Oracles participant Protocol as Vault Protocol Curator->>Vault: Submit vault action Vault->>Newton: Request policy evaluation Newton->>Data: Fetch offchain risk, compliance, or market data Data-->>Newton: Return policy inputs Newton-->>Vault: Return policy result and attestation alt Policy approved Vault->>Protocol: Execute action else Policy denied Vault-->>Curator: Reject action end ``` This gives vault participants a stronger operating model: * **Depositors** can see the rules that govern the vault and understand the risk controls before depositing. * **Curators** can enforce their own mandates consistently, reducing operational mistakes across many vaults. * **Institutions** can review verifiable guardrails and audit evidence before allocating capital. * **Protocols** can expose higher-trust vault products without replacing their existing vault infrastructure. ## Common Vault Policy Areas ### Security Security policies help prevent unsafe or unauthorized vault changes. They can restrict curator actions, check vault health, compare oracle feeds, and make sure decisions reflect the risk profile depositors agreed to up front. ### Compliance Compliance policies help vaults control who can deposit and whether funds meet the vault's requirements. Policies can incorporate KYC status, sanctions screening, address reputation, and AML signals from external providers. ### Privacy Privacy policies help curators protect sensitive curation decisions and proprietary strategy inputs. Newton's privacy layer can keep sensitive policy data away from public chain state while still producing an authorization result that a smart contract can enforce. ## Where Newton Fits Newton does not replace the vault, the curator, or the DeFi protocol. It adds a policy enforcement layer between the requested action and execution. For vaults, that layer can be used in two ways: * **Policy packs** provide ready-made data inputs and Rego guardrails for vault risk, address screening, price divergence, and depeg risk. See [Policy Packs](/developers/vaults/policy-packs). * **Newton VaultKit** provides the SDK and Shield contracts for curator-facing vault integrations, starting with Morpho and generic call support for other protocols. See [Vault SDK](/developers/vaults/sdk/overview). Review the policy packs Newton makes available for vault risk, compliance, and oracle checks. Integrate Newton VaultKit into curator workflows. Understand intents, attestations, tasks, and operator evaluation. # Policy Packs \[Use Newton policy packs to enforce risk, compliance, and oracle checks before vault actions execute.] Newton vault policy packs combine Rego policy logic with policy data oracles. The oracle gathers the external data needed for a decision, then the Rego policy evaluates that data against the vault's configured parameters. In VaultKit 2.1, each published pack implements the `PolicyPack` contract from `@newton-xyz/policy-core`. Declare packs with `definePolicy(...).with(pack)`; use `defineOracle(...)` when authoring your own compatible oracle. ```typescript import { definePolicy } from '@newton-xyz/vaultkit' import { vaultsfyi } from '@newton-xyz/policy-pack-vaultsfyi' const policy = definePolicy({ chainId: '8453', env: 'prod' }) .with(vaultsfyi) ``` In the Newton dashboard, use **Explore Data Oracles** to browse available data oracles, inspect the data they provide, and add the relevant oracle to a policy. Vault policy packs can combine multiple oracles, so a single vault action can be checked against risk, compliance, and market data before it is approved. The open-source policy packs live in [newt-foundation/newton-policy-packs](https://github.com/newt-foundation/newton-policy-packs). Data providers can follow the repo's [CONTRIBUTING guide](https://github.com/newt-foundation/newton-policy-packs/blob/main/CONTRIBUTING.md) to add a pack that other developers can reuse in Newton-powered applications. For more detail on how packs work, how they are implemented, and how to build policies from them, see [Policy Packs](/developers/guides/policy-packs). :::note The canonical deployment source is [`newton-policy-packs/deployments.json`](https://github.com/newt-foundation/newton-policy-packs/blob/main/deployments.json). The table below is a production-address snapshot for common curator workflows; use `deployments.json` for stage environments, WASM CIDs, deploy dates, and future redeploys. ::: ## Available Packs Grouped by data provider. | Provider | Policy pack | Use it for | | --- | --- | --- | | [Arkham](https://arkm.com/api) | [Entity-Aware Spending Gate](/developers/vaults/policies/arkham-entity) | Tiering a spending limit by who the destination actually is | | [Arkham](https://arkm.com/api) | [Adaptive Counterparty Gate](/developers/vaults/policies/arkham-counterparty) | Judging a payment against the sending wallet's own history and outflow baseline | | [Arkham](https://arkm.com/api) | [Explainable Risk Exposure](/developers/vaults/policies/arkham-risk) | Denying exposure you can trace to a specific transaction route | | [Balancer](https://balancer.fi) | Data Checks | Adding keyless Balancer pool or market data | | [Blockaid](https://www.blockaid.io) | Transaction Screening | Screening transaction and address risk | | [Chainalysis](https://www.chainalysis.com) | [Address Screening](/developers/vaults/policies/chainalysis) | Screening addresses for sanctions, high-risk categories, and configured blocklists | | [Guardrail](https://guardrail.so) | Risk Checks | Adding a Guardrail risk signal | | [Persona](https://withpersona.com) | Identity Checks | Adding identity and verification checks | | [Pharos](https://pharos.watch) | [Risk-Adjusted Treasury Gate](/developers/vaults/policies/pharos-treasury) | Admitting a stablecoin only when peg, stress, redemption, and exit capacity are all healthy | | [Pharos](https://pharos.watch) | [Stablecoin Safe Mode](/developers/vaults/policies/pharos-safe-mode) | Blocking exposure-increasing actions under stress while leaving withdrawals open | | [Pharos](https://pharos.watch) | [Redemption-Backed Asset Gate](/developers/vaults/policies/pharos-redemption) | Admitting a stablecoin only when it can actually be redeemed at par | | [RedStone](https://redstone.finance) | [Oracle Divergence](/developers/vaults/policies/redstone) | Stopping actions when market data is stale, divergent, or persistently outside thresholds | | [Sumsub](https://sumsub.com) | Verification Checks | Adding KYC and verification checks | | [Vaults.fyi](https://vaults.fyi) | [Vault Risk Rating](/developers/vaults/policies/vaultsfyi) | Gating actions on APY, TVL, risk score, allocation changes, critical flags, and corruption status | | [Webacy](https://developers.webacy.co/) | [Depeg Risk](/developers/vaults/policies/webacy) | Monitoring pegged-token depeg risk for manager-action gates | | [Xerberus](https://xerberus.io) | [Liquidity Exit](/developers/vaults/policies/xerberus-liquidity-exit) | Denying a position that can't be unwound quickly at a bounded market impact | | [Xerberus](https://xerberus.io) | [Portfolio What-If](/developers/vaults/policies/xerberus-what-if) | Denying a trade that concentrates the book or makes it harder to exit | | [Xerberus](https://xerberus.io) | [Common Cause](/developers/vaults/policies/xerberus-common-cause) | Denying false diversification across venues that share an oracle, collateral, or missing safeguard | ## Deployed PolicyData Addresses Production `PolicyData` addresses for each pack: :::note The Arkham, Pharos and Xerberus packs are documented but **not yet deployed**, so they have no row below. [`deployments.json`](https://github.com/newt-foundation/newton-policy-packs/blob/main/deployments.json) is the source of truth and will carry their addresses first. ::: | Pack | Ethereum (`1`) | Base (`8453`) | Ethereum Sepolia (`11155111`) | Base Sepolia (`84532`) | | --- | --- | --- | --- | --- | | Chainalysis | `0xCacbdEDA32C0048bbE0Ae0d3081Ced3490699049` | `0xceB2027a5fA390740971c7cbb2523841C19CC2f9` | `0x226d196d565b92952669701Fb6cb85B586706996` | `0xdC2e9d30B8e415F907e39f0bfe22974B9894740F` | | RedStone | `0xc81371B54F5d71E8aA92a3601f30f3520c34A5a9` | `0xa5aDDdcf0d37C50A338B3C7F8067FB175AFAf6Bf` | `0x6621D7F739a3682054ac974D772AEEBb8548b1BF` | `0xDbfE2fea061e71109e65e511CD11D070f49A187d` | | Vaults.fyi | `0xd0947ba40e526E7cdD9d2f9C276cED1B2e089c49` | `0x763800149600164edBd19d0eeE9f858d7ac27836` | `0x90ef32c7D103D5Af2A546f94BdA994BC37372017` | `0x443487D506eEc522e5fb5075eBDbD7496DB454dF` | | Webacy | `0xc127976860eEF5FF5d610548C456ABFEE66CD620` | `0x031afeF4c77774960Eb33fb446F9Ce3612C56b5c` | `0xBFf38a5A77262ADfC10667004A550337A97EFB36` | `0x01206E5302794Fba1B71b761594a5e8b4111Be60` | ## Composing Policy Packs Vault policy packs are most useful when combined. For example, a regulated yield vault might require: * Webacy depeg-risk checks for pegged-token exposure * Chainalysis sanctions screening for the depositor address * Vaults.fyi risk checks before accepting additional capital * RedStone oracle divergence checks before curator reallocations The result is a vault whose rules are explicit, inspectable, and enforced before execution. The three Xerberus packs are built to run together as one gate for exposure-increasing actions: an exit quote for the asset, a what-if for the portfolio, and a common-cause check across venues. Pass the same `window` to all three so every result comes from one Xerberus snapshot. For custom oracles, policy-level `_policy` params, bare policies, manifest introspection, and provenance classification, see [Policies](/developers/vaults/sdk/policies) and [Custom Oracles](/developers/vaults/sdk/custom-oracles). Learn how policy packs are structured, implemented, and used in Newton policies. Check vault APY, TVL drawdown, risk score, allocation changes, flags, and corruption status. Screen addresses against sanctions and risk categories. Monitor pegged-token depeg risk before manager actions execute. Compare RedStone price data against onchain oracle data before allowing actions. Tier a spending limit by the destination's real-world entity attribution. Judge a payment against the sending wallet's own counterparty history. Deny risk exposure traced to a specific transaction route. Admit a stablecoin only when peg, stress, redemption, and exit capacity all hold. Block exposure-increasing actions under stress while leaving the exits open. Admit a stablecoin only when it can actually be redeemed at par. Deny a position that can't be unwound quickly at a bounded market impact. Deny a trade that concentrates the book or makes it harder to exit. Deny false diversification across venues that share a failure mechanism. # Protocol Concepts \[Core Newton Policy Protocol types and actors used by VaultKit.] VaultKit is a Newton policy client. Every protected vault action becomes an `Intent`, every `Intent` is evaluated by Newton operators, and every approved action is bound to an attestation before the Shield forwards it. ## Actors | Actor | Role | | --- | --- | | Curator | Builds and signs the manager action. This may be an EOA, application signer, or Safe-controlled workflow. | | SDK | Encodes calldata, prepares policy inputs, submits tasks, polls for results, and submits the Shield transaction. | | Gateway | Receives tasks from the SDK and coordinates offchain policy evaluation. | | Operators | Run policy WASM against the intent, params, and `wasmArgs`, then sign allow or deny results. | | Aggregator | Collects operator signatures until quorum and commits the result. | | Shield | Onchain policy client that validates attestations and forwards approved calls. | | Vault protocol | The underlying protocol contract, such as a MetaMorpho or Euler vault. | ## `Intent` An `Intent` is the exact delegated call the curator wants the Shield to execute: ```solidity struct Intent { address from; address to; uint256 value; bytes data; uint256 chainId; bytes functionSignature; } ``` The attestation binds to all fields. `from` must match the caller of `Shield.execute(...)`, `chainId` must match the execution chain, and `data` must match the exact calldata operators approved. ## `Task` A `Task` is the offchain evaluation request sent to Newton Gateway. It carries the intent, policy reference, pack-specific `wasmArgs`, expiration metadata, and the curator's signature over the intent. ## `TaskResponse` A `TaskResponse` is the operator quorum result for a task. Operators sign the result they evaluated; they do not rewrite the intent. A denial response becomes a typed SDK error. An approval response can become an attestation for onchain execution. ## `Attestation` An `Attestation` is the approval object consumed by the Shield in standard mode: ```solidity struct Attestation { bytes32 taskId; bytes32 policyId; address policyClient; uint32 expiration; Intent intent; bytes intentSignature; } ``` The aggregated BLS signature data is recorded through the AVS task manager. The Shield validates the task id, policy id, client address, intent signature, chain, expiration, and replay status before forwarding the call. ## Policy and Params A Newton policy pins a policy template and a `PolicyConfig`. The config includes `policyParams`, a typed byte blob produced by the selected policy pack. Updating params changes future policy evaluations; it does not rewrite old attestations. ## `wasmArgs` `wasmArgs` are per-call inputs for the policy WASM. They are not global configuration. They usually include the vault, network, asset, market id, external-data query fields, and freshness snapshots required for the policy to make a decision. ## Fail-Closed Behavior If Newton operators deny the task, quorum is not reached, the attestation expires, or the Shield validation fails, the vault action is not forwarded. This fail-closed property is the core operating assumption for VaultKit. ## Policy-set clients For policy-set-capable deployments, a Task freezes the ordered specs, set ID, and revision. The response retains ordered per-policy evidence and aggregate `allowed`; all policies must allow. Preserve order and repeats when forwarding signed responses. Verify the Shield and Vault SDK release before adopting the core SDK schema. # Attestation Flow \[How a VaultKit manager action becomes a Newton policy attestation and an onchain Shield execution.] This is the lifecycle from "a curator wants to call `reallocate(...)`" to "the vault contract receives the call." ```mermaid sequenceDiagram participant Curator as Curator Wallet participant SDK as VaultKit SDK participant Gateway as Newton Gateway participant Operators as Operators + Aggregator participant Shield as Shield Clone participant Vault as Vault Contract Curator->>SDK: Typed manager action SDK->>SDK: Build Intent and wasmArgs SDK->>Curator: Request EIP-712 intent signature SDK->>Gateway: newt_createTask(intent, wasmArgs, signature) Gateway->>Operators: Dispatch task Operators->>Operators: Evaluate policy WASM Operators-->>Gateway: Allow or deny quorum result Gateway-->>SDK: TaskResponse or attestation data SDK->>Shield: execute(...) or executeDirect(...) Shield->>Shield: Validate policy, signature, chain, expiration, replay Shield->>Vault: Forward approved calldata ``` ## 1. Intent Construction The vendor module or `sendCall(...)` path produces calldata for the target vault action. The policy pack prepares `wasmArgs`. The SDK assembles an `Intent` that binds: * The curator signer. * The target contract. * The calldata and value. * The chain id. * The human-readable function signature encoded as bytes. The curator signs the intent with EIP-712. That signature lets operators and the Shield verify that the approved call originated from the expected curator address. ## 2. Task Submission The SDK sends the signed intent, `wasmArgs`, and policy reference to Newton Gateway. The gateway returns a task id and coordinates policy evaluation. ## 3. Operator Evaluation Operators load the policy WASM and evaluate it against: * The intent. * The Shield's current policy params. * The per-call `wasmArgs`. * Any allowed external data or encrypted secrets the pack needs. If the policy denies, the SDK raises `PolicyDeniedError`. The Shield never receives a valid approval for that action. ## 4. Attestation Commit In standard mode, the aggregator commits the quorum response through the AVS task manager. The SDK then calls `Shield.execute(attestation)`. In direct mode, the SDK receives the raw `Task`, `TaskResponse`, and signature data, then calls `Shield.executeDirect(...)`. Direct mode avoids waiting for aggregator commit but costs more gas because the Shield verifies signatures inline. ## 5. Onchain Validation and Forwarding The Shield validates: * The attestation policy id matches the Shield's current policy id. * The intent `from` matches the caller. * The intent chain id matches `block.chainid`. * The task is approved, unexpired, and not already spent. * The BLS quorum is valid, either through the task manager or inline direct validation. If validation passes, the Shield forwards `intent.data` and `intent.value` to `intent.to`. If the vault reverts, the Shield bubbles the target revert reason where possible. ## Replay, Expiration, and Freshness Task ids are one-shot. Reusing the same attestation reverts. Attestations also expire after their configured block window. Freshness comes from the policy pack. Packs commonly include hashes or snapshots in `wasmArgs`, then deny if chain state or external data moved beyond the accepted window during evaluation. ## Standard vs Direct | Trait | Standard `execute` | Direct `executeDirect` | | --- | --- | --- | | Aggregator commit | Required | Skipped | | BLS verification | AVS task manager | Inline in Shield | | Gas | Lower | Higher | | Latency | Higher | Lower | | SDK argument | `shield.sendCall(args, 'ATTESTATION')` | `shield.sendCall(args, 'DIRECT')` | # Guarantees \[The enforcement guarantees VaultKit relies on when routing vault actions through a Shield.] VaultKit's guarantees come from combining typed calldata, signed intents, operator quorum, onchain Shield validation, and a timelocked owner bypass. ## 1. Typed Calldata Only Curators should use typed vendor overlays such as `shield.morpho.reallocate(vault, allocations, options?)`. The overlay builds calldata through the vendor's own SDK. `shield.sendCall(...)` remains an escape hatch for unwrapped vendors; when using it, the curator owns calldata integrity. ## 2. Exact-Tx Attestation Binding The attestation binds to the full `Intent`: signer, target, value, calldata, chain, and function signature. A curator cannot swap calldata after approval, and an attacker cannot reuse an approval for a different vault action. ## 3. Onchain Policy Parameters Policy params live onchain in `NewtonPolicy.PolicyConfig.policyParams`. For composite packs, those bytes are a manifest envelope with `_manifest`, `modules`, and `params`; depositors can decode it and compare the modules against `getPolicyData()` and each oracle's `getWasmCid()`. ## 4. Manager Actions Only The Shield holds vendor manager roles, such as a MetaMorpho allocator or curator role. End-user deposits and withdrawals do not route through Shield. Vendor-side role grants determine which forwarded manager actions can succeed. ## 5. Fail Closed; Bypass Via Timelock If the gateway is unavailable, operators do not reach quorum, policy evaluation denies, delegate validation fails, or onchain attestation validation fails, Shield does not forward the vault call. The only emergency escape is the owner-queued bypass path, which waits at least the configured delay before execution and emits observable events. ## Supporting Checks | Check | What enforces it | | --- | --- | | Chain isolation | `Intent.chainId` is checked against `block.chainid`. | | Replay protection | The AVS task manager tracks consumed task ids. | | Expiration | Attestations are valid only for the configured block window. | | Delegate gate | `execute` and `executeDirect` require `onlyApprovedDelegate`. | | Policy binding | Shield validates the attestation's `policyId` against the currently bound policy id. | ## Non-Upgradeable Clones Shield clones are EIP-1167 minimal proxies to a fixed implementation. There is no proxy-admin upgrade path. A new implementation requires a new factory and curator opt-in through a new clone. # Vaults.fyi Vault Risk Rating \[Use Vaults.fyi data in Newton policies to gate vault actions on APY, TVL, risk score, allocation changes, flags, and corruption status.] The Vaults.fyi policy checks whether a vault still fits the risk profile configured by the vault owner or curator. It is useful before deposits, cap changes, reallocations, or other actions that should stop when the vault's health changes. ## Deployment | Field | Value | | --- | --- | | Pack id | `vaultsfyi` | | PolicyData addresses | [VaultKit address table](/developers/vaults/policy-packs#deployed-policydata-addresses) | | Canonical deployments | [`deployments.json`](https://github.com/newt-foundation/newton-policy-packs/blob/main/deployments.json) | ### Secret | Secret | Required? | Where to get it | | --- | --- | --- | | `VAULTS_FYI_API_KEY` | Required | [vaults.fyi](https://vaults.fyi) | ## Data Inputs The Vaults.fyi data oracle normalizes vault performance, TVL, reputation score, flags, capacity, and historical time-series data from the [Vaults.fyi vault data API](https://docs.vaults.fyi/concepts/vault-data). | Field | What it means | Why the policy uses it | | --- | --- | --- | | `apy_current` | The vault's current 1 day total APY from `apy.1day.total`. Vaults.fyi also exposes 1 hour, 7 day, and 30 day APY windows. | Used as the short-window yield signal for APY anomaly detection. | | `apy_30d` | The vault's 30 day total APY from `apy.30day.total`. Vaults.fyi recommends longer windows, such as 7 day or 30 day, for user-facing yield displays because shorter windows are noisier. | Used as the baseline for detecting whether current APY moved unusually far from recent history. | | `apy_z_score` | A normalized measure of how far the current APY is from the 30 day APY baseline. In this policy oracle, it is computed from current APY and 30 day APY. | Denies actions when yield changes look anomalous enough to require review. | | `tvl_usd` | Total value locked in the vault, denominated in USD. Vaults.fyi also exposes native-asset TVL. | Used to calculate drawdowns and understand vault scale. | | `tvl_drawdown_24h_pct` | The percentage drop from the historical TVL point roughly 24 hours ago to current `tvl_usd`. | Denies actions when the vault has lost too much TVL over the last day. | | `tvl_drawdown_7d_pct` | The percentage drop from the historical TVL point roughly 7 days ago to current `tvl_usd`. | Denies actions when the vault has had a larger medium-term TVL drawdown. | | `risk_score` | Vaults.fyi's composite vault reputation score, after component scores and flag penalties are applied. Higher is better. | Denies actions when the vault score falls below the configured minimum. | | `flags` | Active vault flags from Vaults.fyi, including severity and content. Flags can describe warnings such as audits, migrations, paused deposits, or incidents. | Used to detect critical or high-severity warnings that should pause vault actions. | | `has_critical_flag` | Boolean derived from `flags` when any flag has `critical` or `high` severity. | Denies actions when severe active warnings are present and `deny_on_critical_flag` is enabled. | | `allocation_changed_since_last` | Boolean derived by hashing vault metadata such as protocol, tags, fees, and child vaults, then comparing it to the last known allocation hash. | Denies actions when the vault's observed allocation metadata changed unexpectedly. | | `capacity_remaining` / `capacity_max` | Deposit capacity data when the vault has a cap. Vaults.fyi exposes maximum capacity and remaining capacity where applicable. | Useful for display and future policy extensions around deposits near capacity. | | `is_corrupted` | Vaults.fyi marks vaults as corrupted when indexed data is known to be unreliable. | Denies actions when the vault data should not be trusted. | ## Rego Checks Use these exact Rego checks to enforce each guardrail. ### APY Spike This check compares `apy_z_score` to the configured `apy_z_max`. Use it to stop actions when short-window APY moves too far from the 30 day baseline, which can indicate abnormal rewards, stale data, manipulation, or a strategy regime change. ```rego deny contains "apy_spike" if v.apy_z_score > t.apy_z_max ``` ### 24 Hour TVL Drawdown This check blocks actions when current TVL has fallen by more than `tvl_drawdown_24h_max_pct` compared with the recent 24 hour historical point. It is useful for catching sudden withdrawals, migrations, or confidence shocks. ```rego deny contains "tvl_drawdown_24h" if v.tvl_drawdown_24h_pct > t.tvl_drawdown_24h_max_pct ``` ### 7 Day TVL Drawdown This check blocks actions when current TVL has fallen by more than `tvl_drawdown_7d_max_pct` compared with the 7 day historical point. The 7 day window is less noisy than a daily check and helps catch sustained capital flight. ```rego deny contains "tvl_drawdown_7d" if v.tvl_drawdown_7d_pct > t.tvl_drawdown_7d_max_pct ``` ### Risk Score Floor This check blocks actions when Vaults.fyi returns a non-null reputation score below the configured `risk_score_floor`. Because the score includes multiple components and can include flag penalties, this gives the policy a compact vault-health threshold. ```rego deny contains "risk_score_below_floor" if { v.risk_score != null v.risk_score < t.risk_score_floor } ``` ### Allocation Change This check blocks actions when the oracle detects a change from the last known allocation hash and the policy is configured to deny on allocation change. Use it when a vault should pause until an allocator or risk team reviews changed vault metadata. ```rego deny contains "allocation_changed" if { v.allocation_changed_since_last t.deny_on_allocation_change } ``` ### Critical Flag This check blocks actions when Vaults.fyi reports a high or critical flag and the policy is configured to deny on critical flags. Use it to pause actions during severe active warnings. ```rego deny contains "critical_flag" if { v.has_critical_flag t.deny_on_critical_flag } ``` ### Corrupted Vault This check blocks actions when Vaults.fyi marks the vault's indexed data as corrupted and the policy is configured to deny corrupted vaults. Use it when policy decisions should not proceed on unreliable vault data. ```rego deny contains "vault_corrupted" if { v.is_corrupted t.deny_on_corrupted } ``` ### Final Allow Rule ```rego allow if count(deny) == 0 ``` ## Complete Policy ```rego package vault_risk_rating import future.keywords default allow := false t := data.params.vaultsfyi v := data.wasm.vaultsfyi deny contains "apy_spike" if v.apy_z_score > t.apy_z_max deny contains "tvl_drawdown_24h" if v.tvl_drawdown_24h_pct > t.tvl_drawdown_24h_max_pct deny contains "tvl_drawdown_7d" if v.tvl_drawdown_7d_pct > t.tvl_drawdown_7d_max_pct deny contains "risk_score_below_floor" if { v.risk_score != null v.risk_score < t.risk_score_floor } deny contains "allocation_changed" if { v.allocation_changed_since_last t.deny_on_allocation_change } deny contains "critical_flag" if { v.has_critical_flag t.deny_on_critical_flag } deny contains "vault_corrupted" if { v.is_corrupted t.deny_on_corrupted } allow if count(deny) == 0 ``` # Chainalysis Address Screening \[Use Chainalysis data in Newton policies to screen addresses for sanctions and high-risk categories before vault actions execute.] The Chainalysis policy screens an address for sanctions and risk categories. It is useful for vault deposits, withdrawals, curator operations, or any flow where the vault needs address-level compliance and AML controls. ## Deployment | Field | Value | | --- | --- | | Pack id | `chainalysis` | | PolicyData addresses | [VaultKit address table](/developers/vaults/policy-packs#deployed-policydata-addresses) | | Canonical deployments | [`deployments.json`](https://github.com/newt-foundation/newton-policy-packs/blob/main/deployments.json) | ### Secrets | Secret | Required? | Where to get it | | --- | --- | --- | | `CHAINALYSIS_SANCTIONS_KEY` | Required by schema | [chainalysis.com](https://www.chainalysis.com) | | `CHAINALYSIS_SCREENING_KEY` | Required only for paid risk screening APIs | [chainalysis.com](https://www.chainalysis.com) | The sanctions endpoint is public/keyless, so a placeholder can satisfy schemas that require `CHAINALYSIS_SANCTIONS_KEY`. Use `CHAINALYSIS_SCREENING_KEY` only when your deployed policy calls the paid risk API. ## Data Inputs The Chainalysis data oracle returns values such as: * `sanctioned` * `is_high_risk` * `risk_categories` ## Rego Checks Use these exact Rego checks to enforce each guardrail. ### Sanctioned Address ```rego deny contains "chainalysis_sanctioned" if { t.deny_on_sanctioned v.sanctioned } ``` ### High-Risk Address ```rego deny contains "high_risk_address" if { t.deny_on_high_risk_category v.is_high_risk } ``` ### Blocklisted Risk Category ```rego deny contains "risk_category_blocklisted" if { some cat in v.risk_categories cat in t.risk_categories_blocklist } ``` ### Final Allow Rule ```rego allow if count(deny) == 0 ``` ## Complete Policy ```rego package chainalysis_address_screening import future.keywords default allow := false t := data.params.chainalysis v := data.wasm.chainalysis deny contains "chainalysis_sanctioned" if { t.deny_on_sanctioned v.sanctioned } deny contains "high_risk_address" if { t.deny_on_high_risk_category v.is_high_risk } deny contains "risk_category_blocklisted" if { some cat in v.risk_categories cat in t.risk_categories_blocklist } allow if count(deny) == 0 ``` # RedStone Oracle Divergence \[Use RedStone data in Newton policies to stop vault actions when price feeds are stale, divergent, or persistently outside thresholds.] The RedStone policy compares a RedStone price feed against an onchain oracle and checks whether the divergence is acceptable. It is useful when vault operations depend on fresh and reliable market data. ## Deployment | Field | Value | | --- | --- | | Pack id | `redstone` | | PolicyData addresses | [VaultKit address table](/developers/vaults/policy-packs#deployed-policydata-addresses) | | Canonical deployments | [`deployments.json`](https://github.com/newt-foundation/newton-policy-packs/blob/main/deployments.json) | ### Secret | Secret | Required? | Where to get it | | --- | --- | --- | | None | Keyless | [redstone.finance](https://redstone.finance) | ## Data Inputs The RedStone data oracle returns values such as: * `redstone_feed_age_seconds` * `divergence_bp` * `prev_snapshot_present` * `prev_divergence_bp` * `sustained_seconds` ## Rego Checks Use these exact Rego checks to enforce each guardrail. ### Stale Feed ```rego deny contains "redstone_feed_stale" if v.redstone_feed_age_seconds > t.max_feed_age_seconds ``` ### Hard Divergence Cap ```rego deny contains "divergence_above_hard_cap" if v.divergence_bp >= t.deny_bp ``` ### Sustained Divergence ```rego deny contains "divergence_sustained" if { t.enable_sustained_check v.prev_snapshot_present v.divergence_bp >= t.warn_bp v.prev_divergence_bp >= t.warn_bp v.sustained_seconds >= t.deny_sustained_seconds } ``` ### Final Allow Rule ```rego allow if count(deny) == 0 ``` ## Complete Policy ```rego package redstone_oracle_divergence import future.keywords default allow := false t := data.params.redstone v := data.wasm.redstone deny contains "redstone_feed_stale" if v.redstone_feed_age_seconds > t.max_feed_age_seconds deny contains "divergence_above_hard_cap" if v.divergence_bp >= t.deny_bp deny contains "divergence_sustained" if { t.enable_sustained_check v.prev_snapshot_present v.divergence_bp >= t.warn_bp v.prev_divergence_bp >= t.warn_bp v.sustained_seconds >= t.deny_sustained_seconds } allow if count(deny) == 0 ``` # Webacy Depeg Risk \[Use Webacy data in Newton policies to monitor pegged-token depeg risk before manager actions execute.] The Webacy vault policy monitors pegged-token depeg risk. It is for manager-action gates, such as curator or allocator operations, not end-user deposit gating. The policy data oracle calls Webacy's `/rwa/{address}` endpoint and returns depeg-risk signals for the asset or token address the policy checks. ## Deployment | Field | Value | | --- | --- | | Pack id | `webacy` | | PolicyData addresses | [VaultKit address table](/developers/vaults/policy-packs#deployed-policydata-addresses) | | Canonical deployments | [`deployments.json`](https://github.com/newt-foundation/newton-policy-packs/blob/main/deployments.json) | ### Secret | Secret | Required? | Where to get it | | --- | --- | --- | | `WEBACY_API_KEY` | Required | [developers.webacy.co](https://developers.webacy.co/) | ## Data Inputs | Field | What it means | Why the policy uses it | | --- | --- | --- | | `is_collapsed` | Whether Webacy reports the pegged asset as collapsed. | Denies manager actions when the asset is already in a collapsed state. | | `depeg_event_count` | Number of detected depeg events in the relevant lookback. | Lets policies stop actions after repeated peg failures. | | `consecutive_days_below_peg` | Number of consecutive days the asset has remained below peg. | Captures sustained, not just momentary, peg stress. | | `address` | The checked token or RWA address. | Binds the Webacy result to the asset used by the policy. | ## Example Checks ```rego deny contains "webacy_collapsed" if { t.deny_on_collapsed v.is_collapsed } deny contains "webacy_depeg_events" if { v.depeg_event_count > t.max_depeg_event_count } deny contains "webacy_sustained_depeg" if { v.consecutive_days_below_peg > t.max_consecutive_days_below_peg } ``` ## Notes Webacy depeg risk is usually composed with other manager-action packs. For example, a curator can combine Webacy with Vaults.fyi and RedStone so a reallocation stops when the vault is unhealthy, the pegged asset is stressed, or price feeds diverge. # Arkham Entity-Aware Spending Gate \[Use Arkham entity attribution in Newton policies to tier a spending limit by who the destination actually is.] This policy tiers a spending limit by **who the destination actually is**, using [Arkham Intelligence](https://arkm.com/api). Arkham connects on-chain addresses to real-world entities, so instead of a flat allow/deny an autonomous wallet gets graduated authority: a verified, approved-category destination (exchange, custodian, established protocol) gets the full tier, an unlabelled one gets a small introductory cap, a low-confidence attribution denies so a human can approve it out of band, and a prohibited risk tag denies outright at any size. ## Deployment | Field | Value | | --- | --- | | Pack id | `arkham_entity` | | Rego package | `arkham_entity_wallet` (the `--entrypoint` is `arkham_entity_wallet.allow`) | | PolicyData addresses | [VaultKit address table](/developers/vaults/policy-packs#deployed-policydata-addresses) | | Canonical deployments | [`deployments.json`](https://github.com/newt-foundation/newton-policy-packs/blob/main/deployments.json) | :::note This oracle is not deployed yet, so it has no row in the VaultKit address table. [`deployments.json`](https://github.com/newt-foundation/newton-policy-packs/blob/main/deployments.json) is the source of truth for every pack, chain, and environment. ::: ### Secret | Secret | Required? | Where to get it | | --- | --- | --- | | `ARKHAM_API_KEY` | Required | [arkm.com](https://arkm.com/api) | Sent by the oracle as the `API-Key` request header. ## Data Inputs Two GET requests against `https://api.arkm.com`: `/intelligence/address_enriched/{address}/all` for entity attribution, labels, tags and predictions, and `/risk/address/{address}` for the headline risk level and score. Arkham returns **per-chain** results, because the same address can carry different labels and activity on different networks. When `chain` is supplied the oracle takes that slice; otherwise it takes the first populated one and reports which, so the decision stays traceable to a specific network rather than silently blending several. | Field | What it means | Why the policy uses it | | --- | --- | --- | | `address` | The queried destination address | Echoed for traceability | | `chain` | The chain slice actually used, which may differ from the request when `chain` was omitted | Makes the decision traceable to one network | | `has_attribution` | True when Arkham has either a verified entity or a prediction | Selects the verified tier vs. the introductory cap | | `entity_name` | Real-world entity behind the address, or `null` | Reviewer context | | `entity_category` | Arkham entity category (`cex`, `custodian`, `defi`, …), or `null` | Checked against `approved_entity_categories` | | `address_role` | From `arkhamLabel.name` — e.g. `Hot Wallet`, `Deposit` | Reviewer context | | `is_contract` | Whether Arkham marks the address as a contract | Reviewer context | | `tags` | From `populatedTags[].id` — the stable machine name (`cex`), lowercased and deduplicated, **not** the display label | Matched against `prohibited_tags` | | `attribution_type` | `verified`, `predicted`, or `none` | Reviewer context | | `attribution_confidence` | `1` for a verified `arkhamEntity` (Arkham asserts these and attaches no confidence field), the prediction's own value for a predicted match, `null` when unattributed | Gated by `min_attribution_confidence` | | `risk_level` | Arkham headline risk level | Reviewer context | | `max_risk_score` | Highest category score driving the risk level, or `null` | Gated by `max_risk_score` | | `transaction_amount_usd` | Echoed from `wasmArgs` — **not attested**, see Notes | Every tier comparison rests on it | | `data_age_seconds` | Age of the Arkham observation, or `null` | Gated by `max_data_age_seconds` | | `timestamp` | When this snapshot was taken | Audit | ## Per-call wasmArgs This pack ships **no `prepareQuery`**: the address it screens is not something the SDK can read off the vault, so the curator supplies it per call. | Field | Required? | Notes | | --- | --- | --- | | `address` | Required | The destination being judged | | `chain` | Optional | Arkham chain slug (`ethereum`, `base`, `arbitrum_one`, …). Arkham indexes production networks only | | `transaction_amount_usd` | Optional, defaults to `0` | USD notional, for the tier rules | From the CLI or dashboard, these go in the simulate payload's `wasm_args`. From the SDK they go in `sendCall`'s `wasmArgs` bag — keyed by pack id, and typed per module, so a misspelled field is a compile error rather than a fail-closed evaluation at run time: ```typescript await shield.morpho.submitCap(vault, marketParams, newCap, { wasmArgs: { arkham_entity: { address: destination, chain: 'ethereum', transaction_amount_usd: 5_000, }, }, }) ``` `wasmArgs` requires `@newton-xyz/vaultkit` 2.2.0 or later. See [Policies](/developers/vaults/sdk/policies#per-call-inputs) for why this is a separate bag from `prepareQueryOptions`. ## Policy Parameters | Param | Type | Description | | --- | --- | --- | | `prohibited_tags` | `string[]` | Tags that deny outright regardless of amount. Matched case-sensitively against the lowercased form the oracle emits, so configure in lowercase | | `approved_entity_categories` | `string[]` | Categories qualifying for the full tier | | `tier_verified_max_usd` | `number` | Hard per-transaction ceiling in USD, applied even to trusted destinations | | `tier_unlabelled_max_usd` | `number` | Introductory cap for unattributed or unapproved destinations | | `min_attribution_confidence` | `number` | Minimum confidence 0-1 to act on an entity match | | `deny_on_no_attribution` | `boolean` | Refuse unattributed destinations outright | | `max_risk_score` | `number` | Maximum tolerated headline risk score 0-100 | | `max_data_age_seconds` | `number` | Freshness ceiling | | `deny_on_missing_fields` | `string[]` | Field names whose unreported (`null`) value denies rather than passes. Accepts `attribution_confidence`, `max_risk_score`, `data_age_seconds`. An empty list opts out | ## Rego Checks Every rule this policy enforces: | Deny reason | Condition | What it catches | | --- | --- | --- | | `prohibited_tag` | any tag in `prohibited_tags` | Sanctioned, hacker, scam or mixer destinations, at any size | | `no_attribution` | `deny_on_no_attribution` and no attribution | Unlabelled destinations, when the curator refuses them outright | | `no_attribution_over_limit` | unattributed and amount over `tier_unlabelled_max_usd` | An unknown address used for more than an introductory amount | | `unapproved_entity_category` | attributed, category not approved, amount over the intro cap | A known but out-of-policy venue used at scale | | `low_attribution_confidence` | confidence below `min_attribution_confidence` | Weak entity matches that need human review | | `amount_over_verified_tier` | amount over `tier_verified_max_usd` | The hard ceiling | | `risk_score_above_max` | score over `max_risk_score` | Elevated headline risk | | `stale_data` | age over `max_data_age_seconds` | Decisions made on stale intelligence | | `missing_` | the field is named in `deny_on_missing_fields` and the oracle reported `null` | A configured threshold quietly doing nothing | ### Prohibited Tag A prohibited tag denies at any size — there is no amount below which a sanctioned or mixer destination becomes acceptable. `prohibited_tag_hits` is a set rather than a bare boolean so the offending tag stays traceable. ```rego prohibited_tag_hits contains tag if { some tag in v.tags tag in t.prohibited_tags } deny contains "prohibited_tag" if count(prohibited_tag_hits) > 0 ``` ### Unattributed Destination Two rules, so the curator picks the posture. `deny_on_no_attribution` refuses an unknown address outright; leave it off and an unknown address is instead capped at the introductory tier. ```rego deny contains "no_attribution" if { t.deny_on_no_attribution v.has_attribution == false } deny contains "no_attribution_over_limit" if { v.has_attribution == false amount > t.tier_unlabelled_max_usd } ``` ### Unapproved Entity Category Note the `has_attribution == true` guard: an unattributed address has no category to judge, and is handled by the rules above instead. ```rego deny contains "unapproved_entity_category" if { v.has_attribution == true not v.entity_category in t.approved_entity_categories amount > t.tier_unlabelled_max_usd } ``` ### Missing Fields A threshold the curator configured is worth nothing if the oracle never reports the value it applies to. This is opt-in **per field** rather than one blanket switch, so a provider that never populates a given field cannot force a choice between requiring the field you care about and denying every transaction. ```rego deny contains sprintf("missing_%v", [name]) if { some name in t.deny_on_missing_fields nullable_fields[name] == null } ``` ### Final Allow Rule The groundedness probes are load-bearing, not decoration. Every deny rule silent-skips on an undefined field, so an error envelope yields an **empty** deny set — a bare `count(deny) == 0` would fail *open* on exactly the payload that most needs to fail closed. ```rego allow if { not v.error is_boolean(v.has_attribution) is_number(amount) count(deny) == 0 } ``` ## Complete Policy ```rego package arkham_entity_wallet import future.keywords default allow := false t := data.params.arkham_entity v := data.wasm.arkham_entity amount := v.transaction_amount_usd nullable_fields := { "attribution_confidence": v.attribution_confidence, "max_risk_score": v.max_risk_score, "data_age_seconds": v.data_age_seconds, } prohibited_tag_hits contains tag if { some tag in v.tags tag in t.prohibited_tags } deny contains "prohibited_tag" if count(prohibited_tag_hits) > 0 deny contains "no_attribution" if { t.deny_on_no_attribution v.has_attribution == false } deny contains "no_attribution_over_limit" if { v.has_attribution == false amount > t.tier_unlabelled_max_usd } deny contains "unapproved_entity_category" if { v.has_attribution == true not v.entity_category in t.approved_entity_categories amount > t.tier_unlabelled_max_usd } deny contains "low_attribution_confidence" if { v.attribution_confidence != null v.attribution_confidence < t.min_attribution_confidence } deny contains "amount_over_verified_tier" if amount > t.tier_verified_max_usd deny contains "risk_score_above_max" if { v.max_risk_score != null v.max_risk_score > t.max_risk_score } deny contains "stale_data" if { v.data_age_seconds != null v.data_age_seconds > t.max_data_age_seconds } deny contains sprintf("missing_%v", [name]) if { some name in t.deny_on_missing_fields nullable_fields[name] == null } allow if { not v.error is_boolean(v.has_attribution) is_number(amount) count(deny) == 0 } ``` Composing this pack into a VaultKit composite changes only where params live: the manifest envelope puts the curator's slice at `data.params.params.arkham_entity`. See [Policies](/developers/vaults/sdk/policies). ## Notes * **`transaction_amount_usd` is caller-supplied and NOT attested.** It arrives through `wasmArgs`, so a caller controls it, and every tier rule rests on it. The attested alternative — `input.value` — is native-token wei rather than USD, and arrives as a *string*. A curator who needs a tamper-proof ceiling should pair this pack with a native-value cap in a composite rather than relying on the USD tiers alone. * `null` is the oracle's "Arkham did not report this", deliberately distinct from `0`: a null risk score means "not reported", a zero score would be a genuine clean verdict. Null optional fields fail soft. A **missing** key rather than an explicit null leaves the groundedness probes undefined and correctly blocks `allow`. * **Verified attribution carries no confidence field.** Arkham asserts `arkhamEntity` rather than inferring it, so the oracle scores it `1`. Only `arkhamEntityPrediction` has a real confidence value, which is what `min_attribution_confidence` meaningfully gates. * Arkham populates all three `deny_on_missing_fields` candidates for an attributed address, so listing them is safe for curators who would rather an unreported value block than pass. * Tags are lowercased by the oracle and matched case-sensitively against that normalised form, so configure `prohibited_tags` in lowercase. * The lookup chain is **not** the execution chain. Arkham indexes production networks only, so on a testnet `chain` must name a real mainnet or the oracle has no data and the policy fails closed. # Arkham Adaptive Counterparty Gate \[Use Arkham counterparty history in Newton policies to judge a payment against the sending wallet's own baseline.] This policy judges a payment against **the sending wallet's own history**, using [Arkham Intelligence](https://arkm.com/api). Routine payments to established counterparties clear automatically; anything that breaks the wallet's established pattern stops for human approval. A new recipient is capped at an introductory amount, an unusually large payment relative to prior transactions with that counterparty denies, a concentration spike denies, and total outflow running well above the wallet's normal daily level denies. ## Deployment | Field | Value | | --- | --- | | Pack id | `arkham_counterparty` | | Rego package | `arkham_counterparty_activity` (the `--entrypoint` is `arkham_counterparty_activity.allow`) | | PolicyData addresses | [VaultKit address table](/developers/vaults/policy-packs#deployed-policydata-addresses) | | Canonical deployments | [`deployments.json`](https://github.com/newt-foundation/newton-policy-packs/blob/main/deployments.json) | :::note This oracle is not deployed yet, so it has no row in the VaultKit address table. [`deployments.json`](https://github.com/newt-foundation/newton-policy-packs/blob/main/deployments.json) is the source of truth for every pack, chain, and environment. ::: ### Secret | Secret | Required? | Where to get it | | --- | --- | --- | | `ARKHAM_API_KEY` | Required | [arkm.com](https://arkm.com/api) | Sent by the oracle as the `API-Key` request header. ## Data Inputs Two GET requests against `https://api.arkm.com`: `/counterparties/address/{sender}` for counterparties aggregated by USD volume with transaction counts and labels, and `/flow/address/{sender}` for the inflow/outflow time series used to derive the outflow baseline. The baseline deliberately **excludes the most recent bucket**: that bucket is what is being judged, and including it would let a single anomalous day inflate its own baseline and hide itself. | Field | What it means | Why the policy uses it | | --- | --- | --- | | `sender_address` / `destination_address` | Echoed from `wasmArgs` | Audit | | `chains` | Chain scope used, or `null` for all | Audit | | `is_known_counterparty` | Whether the destination appears in the sender's history | Selects the new-recipient cap vs. the established path | | `counterparty_transaction_count` | Prior transactions with this counterparty | Gated by `min_counterparty_transactions` | | `counterparty_total_usd` | Cumulative USD transacted with them | Reviewer context | | `counterparty_avg_usd` | Average prior transaction size, or `null` when there is no history to average | Basis for the anomaly multiple | | `counterparty_last_seen_days` | **Always `null`** — Arkham's counterparties endpoint returns no per-relationship timestamp | Would gate `stale_relationship`; inert today | | `counterparty_concentration_pct` | This counterparty's share (0-100) of recent activity | Gated by `max_counterparty_concentration_pct` | | `normal_daily_outflow_usd` | Mean daily outflow across the window, excluding the latest bucket | Baseline denominator | | `recent_daily_outflow_usd` | Latest bucket's outflow | Baseline numerator | | `outflow_ratio` | `recent / normal`, or `null` when no baseline exists | Gated by `max_outflow_vs_baseline_multiple` | | `transaction_amount_usd` | Echoed from `wasmArgs` — **not attested**, see Notes | The anomaly and cap rules rest on it | | `timestamp` | When this snapshot was taken | Audit | ## Per-call wasmArgs This pack ships **no `prepareQuery`**: the sender and destination are not something the SDK can read off the vault, so the curator supplies them per call. | Field | Required? | Notes | | --- | --- | --- | | `sender_address` | Required | The wallet whose history is read | | `destination_address` | Required | The counterparty being judged | | `chains` | Required | Comma-separated chain scope. **Not optional** — see Notes | | `transaction_amount_usd` | Optional, defaults to `0` | USD notional | | `history_window_days` | Optional, 1-365, defaults to `90` | Materially changes the counterparty set — see Notes | From the CLI or dashboard these go in the simulate payload's `wasm_args`. From the SDK they go in `sendCall`'s `wasmArgs` bag: ```typescript await shield.morpho.submitCap(vault, marketParams, newCap, { wasmArgs: { arkham_counterparty: { sender_address: shieldClone, destination_address: destination, chains: 'ethereum', transaction_amount_usd: 12_000, }, }, }) ``` `wasmArgs` requires `@newton-xyz/vaultkit` 2.2.0 or later. See [Policies](/developers/vaults/sdk/policies#per-call-inputs). ## Policy Parameters | Param | Type | Description | | --- | --- | --- | | `require_known_counterparty` | `boolean` | Refuse any destination absent from history | | `max_new_counterparty_usd` | `number` | Introductory cap for a first payment | | `min_counterparty_transactions` | `number` | Prior transactions before a relationship counts as established | | `max_counterparty_last_seen_days` | `number` | Recency ceiling for the relationship. **No effect today** — see Notes | | `max_amount_vs_avg_multiple` | `number` | Amount ceiling as a multiple of the historical average. Must be greater than zero | | `max_counterparty_concentration_pct` | `number` | Concentration ceiling 0-100 | | `max_outflow_vs_baseline_multiple` | `number` | Outflow ceiling as a multiple of normal | | `max_data_age_seconds` | `number` | Freshness ceiling | | `deny_on_missing_fields` | `string[]` | Field names whose unreported (`null`) value denies. **Two entries deny everything today** — see Notes | ## Rego Checks Every rule this policy enforces: | Deny reason | Condition | What it catches | | --- | --- | --- | | `unknown_counterparty` | `require_known_counterparty` and unknown | New recipients, when the curator forbids them outright | | `new_counterparty_over_limit` | unknown and amount over `max_new_counterparty_usd` | A first payment larger than the introductory cap | | `counterparty_too_new` | known but fewer than `min_counterparty_transactions` prior txs | A relationship too thin to count as established | | `stale_relationship` | last seen over `max_counterparty_last_seen_days` ago | A dormant counterparty reactivating. **Currently inert** | | `amount_anomaly` | amount over `max_amount_vs_avg_multiple` × the historical average | A payment wildly out of scale with the relationship | | `misconfigured_max_amount_vs_avg_multiple` | the multiple is absent or not greater than `0` | A multiplier that would silently disable the anomaly rule | | `concentration_spike` | share over `max_counterparty_concentration_pct` | One destination suddenly dominating outflow | | `outflow_above_baseline` | ratio over `max_outflow_vs_baseline_multiple` | The wallet draining faster than it normally does | | `stale_data` | age over `max_data_age_seconds` | Decisions made on stale intelligence | | `missing_` | the field is named in `deny_on_missing_fields` and the oracle reported `null` | A configured threshold quietly doing nothing | ### Amount Anomaly A zero-or-null historical average **skips** this rule: multiplying by zero would make every payment infinitely anomalous. That leaves a genuine gap — a counterparty whose average is a true `0` admits any amount here, gated only by the other rules. Closing it needs a notion of "expected size" this API does not provide, so lean on `max_new_counterparty_usd` and `max_outflow_vs_baseline_multiple` instead. ```rego deny contains "amount_anomaly" if { v.counterparty_avg_usd != null v.counterparty_avg_usd > 0 amount > v.counterparty_avg_usd * t.max_amount_vs_avg_multiple } ``` ### Misconfigured Multiplier Fail closed on a bad multiplier: a `0` would silently *disable* the anomaly rule rather than tighten it. The check routes through a named helper because OPA hoists `t.max_amount_vs_avg_multiple` out of a `not ... > 0` into its own conjunct, so an **absent** param would make the whole rule body undefined instead of negating to true. Negating a named rule catches the absent case as well as the zero one. ```rego deny contains "misconfigured_max_amount_vs_avg_multiple" if not valid_avg_multiple valid_avg_multiple if t.max_amount_vs_avg_multiple > 0 ``` ### Missing Fields Opt-in **per field**, which matters more here than in most packs: two of the five candidates are always `null` today, so a blanket switch would force a choice between requiring the fields you care about and denying every transaction. ```rego deny contains sprintf("missing_%v", [name]) if { some name in t.deny_on_missing_fields nullable_fields[name] == null } ``` ### Final Allow Rule The groundedness probes are load-bearing: every deny rule silent-skips on an undefined field, so an error envelope or a partial payload yields an **empty** deny set and a bare `count(deny) == 0` would fail *open*. ```rego allow if { not v.error is_boolean(v.is_known_counterparty) is_number(amount) count(deny) == 0 } ``` ## Complete Policy ```rego package arkham_counterparty_activity import future.keywords default allow := false t := data.params.arkham_counterparty v := data.wasm.arkham_counterparty amount := v.transaction_amount_usd nullable_fields := { "counterparty_last_seen_days": v.counterparty_last_seen_days, "counterparty_avg_usd": v.counterparty_avg_usd, "counterparty_concentration_pct": v.counterparty_concentration_pct, "outflow_ratio": v.outflow_ratio, "data_age_seconds": v.data_age_seconds, } deny contains "unknown_counterparty" if { t.require_known_counterparty v.is_known_counterparty == false } deny contains "new_counterparty_over_limit" if { v.is_known_counterparty == false amount > t.max_new_counterparty_usd } deny contains "counterparty_too_new" if { v.is_known_counterparty == true v.counterparty_transaction_count < t.min_counterparty_transactions } deny contains "stale_relationship" if { v.counterparty_last_seen_days != null v.counterparty_last_seen_days > t.max_counterparty_last_seen_days } deny contains "amount_anomaly" if { v.counterparty_avg_usd != null v.counterparty_avg_usd > 0 amount > v.counterparty_avg_usd * t.max_amount_vs_avg_multiple } deny contains "misconfigured_max_amount_vs_avg_multiple" if not valid_avg_multiple valid_avg_multiple if t.max_amount_vs_avg_multiple > 0 deny contains "concentration_spike" if { v.counterparty_concentration_pct != null v.counterparty_concentration_pct > t.max_counterparty_concentration_pct } deny contains "outflow_above_baseline" if { v.outflow_ratio != null v.outflow_ratio > t.max_outflow_vs_baseline_multiple } deny contains "stale_data" if { v.data_age_seconds != null v.data_age_seconds > t.max_data_age_seconds } deny contains sprintf("missing_%v", [name]) if { some name in t.deny_on_missing_fields nullable_fields[name] == null } allow if { not v.error is_boolean(v.is_known_counterparty) is_number(amount) count(deny) == 0 } ``` Composing this pack into a VaultKit composite changes only where params live: the manifest envelope puts the curator's slice at `data.params.params.arkham_counterparty`. See [Policies](/developers/vaults/sdk/policies). ## Notes * **`transaction_amount_usd` is caller-supplied and NOT attested.** It arrives through `wasmArgs`, so a caller controls it. The attested alternative — `input.value` — is native-token wei rather than USD, and arrives as a *string*. A curator needing a tamper-proof ceiling should pair this pack with a native-value cap in a composite. * **Two rules are inert today.** Arkham's counterparties and flow endpoints expose no per-relationship timestamp and no observation timestamp, so `counterparty_last_seen_days` and `data_age_seconds` are always `null`; `stale_relationship` and `stale_data` fail soft. Both fields and rules are kept so they activate automatically once Arkham exposes the data. * ⚠️ **Consequently, listing `counterparty_last_seen_days` or `data_age_seconds` in `deny_on_missing_fields` denies *every* transaction.** The list is per field precisely so that does not hold the others hostage: require `outflow_ratio` and `counterparty_avg_usd` and leave the two inert fields out. * **`chains` is required.** Unscoped, `/flow/address` returns every chain's full daily history — over 1 MB for an active wallet, which exhausts the WASM heap. Scoping to one chain brings it to roughly 270 KB. * **`history_window_days` materially changes the counterparty set.** A 90-day window on a wallet whose large relationships are older returns only small, recent ones — so an "established" counterparty can read as new. Widen the window to match the relationships you intend to recognise. * **`max_amount_vs_avg_multiple` must be greater than zero.** The check lives in Rego rather than the params schema because `exclusiveMinimum` is outside the regorus-clean keyword set the AVS-side schema sticks to. * `null` is the oracle's "not reported", deliberately distinct from `0`. Null optional fields fail soft; a **missing** key leaves the groundedness probes undefined and correctly blocks `allow`. # Arkham Explainable Risk Exposure \[Use Arkham exposure paths in Newton policies to deny risk you can trace to a specific transaction route.] This policy denies **risk exposure you can explain**, using [Arkham Intelligence](https://arkm.com/api). Rather than collapsing everything into one score, it reasons about the actual transaction routes connecting an address to a risky source: direct or near-hop exposure to a severe category denies on presence alone, more distant exposure denies only when materially large or recent, dust is ignored outright so a dusting attack cannot brick a wallet, and the deny set names the offending route so a reviewer can see *why*. ## Deployment | Field | Value | | --- | --- | | Pack id | `arkham_risk` | | Rego package | `arkham_risk_exposure` (the `--entrypoint` is `arkham_risk_exposure.allow`) | | PolicyData addresses | [VaultKit address table](/developers/vaults/policy-packs#deployed-policydata-addresses) | | Canonical deployments | [`deployments.json`](https://github.com/newt-foundation/newton-policy-packs/blob/main/deployments.json) | :::note This oracle is not deployed yet, so it has no row in the VaultKit address table. [`deployments.json`](https://github.com/newt-foundation/newton-policy-packs/blob/main/deployments.json) is the source of truth for every pack, chain, and environment. ::: ### Secret | Secret | Required? | Where to get it | | --- | --- | --- | | `ARKHAM_API_KEY` | Required | [arkm.com](https://arkm.com/api) | Sent by the oracle as the `API-Key` request header. ## Data Inputs Two GET requests against `https://api.arkm.com`: `/risk/address/{address}` for risk level, per-category scores and seed status, and `/risk/address/{address}/paths` for up to ten paths to risky sources, ranked by contributed USD. The oracle deliberately emits Arkham's **raw** paths rather than a precomputed verdict. "Severe" is a curator notion that lives in `data.params` and the Rego filters on it — anything collapsed in the WASM would be a decision the curator can no longer see or tune. | Field | What it means | Why the policy uses it | | --- | --- | --- | | `address` / `chain` | Echoed from `wasmArgs` | Audit | | `risk_level` | Arkham headline risk level | Reviewer context | | `max_score` | Highest category score, or `null` | Gated by `max_risk_score` | | `category_scores` | `{category: score}` map, harvested from Arkham's flat `_score` sibling keys (`hacker_score`, `sanctioned_1hop_score`, …). Aggregate `max_score*` keys are excluded | Reviewer context | | `top_risk_category` | Category driving the headline score | Reviewer context | | `is_seed` | Whether the address **is** a known risky source, not merely exposed to one | Gated by `deny_on_seed` | | `hop_distance` / `risk_weighted_incoming_usd` / `risk_weighted_outgoing_usd` | Additional Arkham risk context | Reviewer context | | `paths[]` | `category`, `direction`, `hop_distance`, `seed_address`, `score`, `contributed_usd`, `nodes[]`. `contributed_pct`, `first_seen_days` and `last_seen_days` are **always `null`** — Arkham does not return them | The three path rules filter on these | | `data_age_seconds` | Age of the Arkham observation, or `null` | Gated by `max_data_age_seconds` | | `timestamp` | When this snapshot was taken | Audit | ## Per-call wasmArgs This pack ships **no `prepareQuery`**: the address it screens is not something the SDK can read off the vault, so the curator supplies it per call. | Field | Required? | Notes | | --- | --- | --- | | `address` | Required | The address whose exposure is judged | | `chain` | Optional | Arkham chain slug. Arkham indexes production networks only | From the CLI or dashboard this goes in the simulate payload's `wasm_args`. From the SDK it goes in `sendCall`'s `wasmArgs` bag: ```typescript await shield.morpho.submitCap(vault, marketParams, newCap, { wasmArgs: { arkham_risk: { address: destination, chain: 'ethereum' }, }, }) ``` `wasmArgs` requires `@newton-xyz/vaultkit` 2.2.0 or later. See [Policies](/developers/vaults/sdk/policies#per-call-inputs). ## Policy Parameters | Param | Type | Description | | --- | --- | --- | | `severe_categories` | `string[]` | Categories treated as severe (e.g. `sanctions`, `hacker`, `mixer`). Configure in lowercase | | `max_severe_hop_distance` | `number` | Hops within which severe exposure denies outright | | `material_exposure_usd` | `number` | USD threshold for distant exposure | | `recent_exposure_days` | `number` | Recency window for distant exposure. **No effect today** — see Notes | | `dust_tolerance_usd` | `number` | Paths at or below this are ignored entirely. Strict `>`, so `0` considers every path | | `deny_on_seed` | `boolean` | Deny when the address is itself a risky source | | `max_risk_score` | `number` | Maximum tolerated headline score 0-100 | | `max_data_age_seconds` | `number` | Freshness ceiling | | `deny_on_missing_fields` | `string[]` | Field names whose unreported (`null`) value denies. `path_last_seen_days` is its own entry, covering undated exposure paths | ## Rego Checks Paths are filtered in three stages — dust floor, then severity, then distance: | Deny reason | Condition | What it catches | | --- | --- | --- | | `seed_address` | `deny_on_seed` and the address is a seed | The address **is** the risky source | | `severe_exposure_within_hops` | severe category within `max_severe_hop_distance` | Direct and near-hop exposure, at any size above dust | | `material_distant_exposure` | severe, beyond the hop limit, over `material_exposure_usd` | Large exposure further out in the graph | | `recent_distant_exposure` | severe, beyond the hop limit, within `recent_exposure_days` | Fresh exposure further out. **Currently inert** | | `risk_score_above_max` | score over `max_risk_score` | Elevated headline risk, independent of paths | | `stale_data` | age over `max_data_age_seconds` | Decisions made on stale intelligence | | `missing_` | the field is named in `deny_on_missing_fields` and the oracle reported `null` | A configured threshold quietly doing nothing | | `missing_path_last_seen_days` | `path_last_seen_days` is listed and a severe distant path has no date | Undated exposure slipping past the recency rule entirely | ### Path Classification Dust is filtered **first**, so a dusting attack cannot manufacture exposure. Severity then narrows to the curator's categories, and distance splits the remainder into "denies on presence" versus "denies only if large or recent". ```rego material_paths contains p if { some p in v.paths p.contributed_usd > t.dust_tolerance_usd } severe_paths contains p if { some p in material_paths p.category in t.severe_categories } severe_near_paths contains p if { some p in severe_paths p.hop_distance <= t.max_severe_hop_distance } ``` ### Distant Exposure Two rules on value and recency, plus a third for the strict-mode case where Arkham reports no date at all — an undated path slips past the recency rule, so the missing date is itself the signal a curator asked to act on. ```rego material_distant_paths contains p if { some p in severe_paths p.hop_distance > t.max_severe_hop_distance p.contributed_usd > t.material_exposure_usd } undated_distant_paths contains p if { some p in severe_paths p.hop_distance > t.max_severe_hop_distance p.last_seen_days == null } deny contains "missing_path_last_seen_days" if { "path_last_seen_days" in t.deny_on_missing_fields count(undated_distant_paths) > 0 } ``` ### Explainability A separate `risk_paths` rule renders each offending route as `" exposure via at hop(s), $ contributed"`. This is **not** evaluated on-chain — the AVS entrypoint is `arkham_risk_exposure.allow` and nothing else. It exists for `opa eval`, local simulation, and composites that want to surface a reason to an operator. ```rego risk_paths contains detail if { some p in offending_paths detail := sprintf( "%v exposure via %v at %v hop(s), $%v contributed", [p.category, p.seed_address, p.hop_distance, p.contributed_usd], ) } ``` ### Final Allow Rule `is_array(v.paths)` is the load-bearing probe here: an error envelope has no `paths` key, every path rule then yields an empty set, and a bare `count(deny) == 0` would fail *open* on exactly that payload. ```rego allow if { not v.error is_boolean(v.is_seed) is_array(v.paths) count(deny) == 0 } ``` ## Complete Policy ```rego package arkham_risk_exposure import future.keywords default allow := false t := data.params.arkham_risk v := data.wasm.arkham_risk nullable_fields := { "max_score": v.max_score, "data_age_seconds": v.data_age_seconds, } material_paths contains p if { some p in v.paths p.contributed_usd > t.dust_tolerance_usd } severe_paths contains p if { some p in material_paths p.category in t.severe_categories } severe_near_paths contains p if { some p in severe_paths p.hop_distance <= t.max_severe_hop_distance } material_distant_paths contains p if { some p in severe_paths p.hop_distance > t.max_severe_hop_distance p.contributed_usd > t.material_exposure_usd } recent_distant_paths contains p if { some p in severe_paths p.hop_distance > t.max_severe_hop_distance p.last_seen_days != null p.last_seen_days <= t.recent_exposure_days } undated_distant_paths contains p if { some p in severe_paths p.hop_distance > t.max_severe_hop_distance p.last_seen_days == null } offending_paths := (severe_near_paths | material_distant_paths) | recent_distant_paths risk_paths contains detail if { some p in offending_paths detail := sprintf( "%v exposure via %v at %v hop(s), $%v contributed", [p.category, p.seed_address, p.hop_distance, p.contributed_usd], ) } deny contains "seed_address" if { t.deny_on_seed v.is_seed == true } deny contains "severe_exposure_within_hops" if count(severe_near_paths) > 0 deny contains "material_distant_exposure" if count(material_distant_paths) > 0 deny contains "recent_distant_exposure" if count(recent_distant_paths) > 0 deny contains "risk_score_above_max" if { v.max_score != null v.max_score > t.max_risk_score } deny contains "stale_data" if { v.data_age_seconds != null v.data_age_seconds > t.max_data_age_seconds } deny contains sprintf("missing_%v", [name]) if { some name in t.deny_on_missing_fields nullable_fields[name] == null } deny contains "missing_path_last_seen_days" if { "path_last_seen_days" in t.deny_on_missing_fields count(undated_distant_paths) > 0 } allow if { not v.error is_boolean(v.is_seed) is_array(v.paths) count(deny) == 0 } ``` Composing this pack into a VaultKit composite changes only where params live: the manifest envelope puts the curator's slice at `data.params.params.arkham_risk`. See [Policies](/developers/vaults/sdk/policies). ## Notes * **"Severe" is yours, not Arkham's.** The oracle emits raw exposure paths; `severe_categories` decides what counts. That is what makes the denial explainable, and what lets you tighten it without a redeploy. * **The `recent_distant_exposure` rule is currently inert.** Arkham's paths endpoint returns no first/last transaction timestamps, so `last_seen_days` is always `null` and the rule fails soft; `recent_exposure_days` has no effect today. Distant exposure is still caught by `material_distant_exposure` on value. * ⚠️ **Consequently, listing `path_last_seen_days` in `deny_on_missing_fields` denies any address with a severe path beyond the hop limit.** Because the list is per field, you can require `max_score` without taking that on. * **`contributed_pct` is also unavailable**, so size-relative thresholds must use `contributed_usd`. * **Dust tolerance is a strict `>` comparison**, so a path contributing exactly `dust_tolerance_usd` counts as dust and is ignored. Set it to `0` to consider every path. * A category outside `severe_categories` never trips the hop, materiality or recency rules, however large or direct. Only `max_risk_score` constrains it. * Path categories and seed addresses are lowercased by the oracle, so configure `severe_categories` in lowercase. * The lookup chain is **not** the execution chain. Arkham indexes production networks only, so on a testnet `chain` must name a real mainnet or the oracle has no data and the policy fails closed. # Pharos Risk-Adjusted Treasury Gate \[Use Pharos stablecoin data in Newton policies to admit an asset only when peg, stress, redemption, and exit capacity are all healthy.] This policy admits a stablecoin into a treasury only when it is healthy on **all four** dimensions [Pharos](https://pharos.watch) measures — not on price alone: no active depeg and current deviation within tolerance, stress below threshold (the earlier, broader signal that rises before a full depeg), an approved redemption route so the position has a credible exit at par, and exit capacity comfortably larger than the position being taken on. ## Deployment | Field | Value | | --- | --- | | Pack id | `pharos_treasury` | | Rego package | `pharos_treasury_risk` (the `--entrypoint` is `pharos_treasury_risk.allow`) | | PolicyData addresses | [VaultKit address table](/developers/vaults/policy-packs#deployed-policydata-addresses) | | Canonical deployments | [`deployments.json`](https://github.com/newt-foundation/newton-policy-packs/blob/main/deployments.json) | :::note This oracle is not deployed yet, so it has no row in the VaultKit address table. [`deployments.json`](https://github.com/newt-foundation/newton-policy-packs/blob/main/deployments.json) is the source of truth for every pack, chain, and environment. ::: ### Secret | Secret | Required? | Where to get it | | --- | --- | --- | | `PHAROS_API_KEY` | Required | [pharos.watch](https://pharos.watch) | Sent by the oracle as the `X-API-Key` request header. :::warning This is the heaviest pack in the catalog: **five API calls per evaluation**, against a self-serve Pharos rate limit of 30 requests per minute. ::: ## Data Inputs Five GET requests against `https://api.pharos.watch`. Four of the five are scoped to a single asset, using per-asset endpoints that are orders of magnitude smaller than the bulk alternatives: | Call | Size | Why this one | | --- | --- | --- | | `/api/stablecoin-summary/{id}` | ~0.6 KB | Identity, `priceUsd`, peg type/mechanism, supply, chain count. Replaces `/api/stablecoin/{id}`, a 345 KB supply time series with no price in it | | `/api/depeg-events?stablecoin={id}&active=true&includePending=true` | ~0.3 KB | Confirmed active incidents | | `/api/stress-signals?stablecoin={id}&days={n}` | ~7.5 KB | Stress score, band, per-signal sub-scores | | `/api/dex-liquidity-history?stablecoin={id}&days=1` | ~47 KB | Liquidity score, TVL, and the `exitRouteObservations` capacity curves. Replaces `/api/dex-liquidity` (2.98 MB, all assets, no query parameters) — a 63× reduction | | `/api/redemption-backstops` | 1.1 MB | The one endpoint with **no filter at all**. This coin's object is cut out of the raw text and only that ~3 KB parsed; `JSON.parse` on the full document exhausts the WASM heap | **Reading exit capacity correctly.** Each observation's top-level `executableUsd` is capped at its `requestedNotionalUsd`, so a $1M simulation that fills completely reports $1M — which says nothing about the ceiling. The real ladder is `capacityCurve`: successive notionals (100k → 1M → 10M → 25M) each with the `executionCostBps` actually incurred. The oracle walks that curve and takes the largest rung within the caller's `max_cost_bps`. An observation's own `maxCostBps` is the bound the *simulation ran under* (always 200), not the cost incurred — filtering on it would use the wrong number. Across routes it takes the **max**, not the sum: routes overlap, so summing double-counts shared liquidity. | Field | What it means | Why the policy uses it | | --- | --- | --- | | `stablecoin_id` / `symbol` / `name` | Asset identity | Audit | | `peg_type` / `peg_mechanism` | e.g. `peggedUSD`, `fiat-backed` | Reviewer context | | `price` / `price_confidence` | Aggregated price and Pharos's confidence in it | Reviewer context | | `peg_target` / `peg_deviation_bps` | Target (1) and **signed** deviation. `null` when no source could resolve a price — never `0`, which would read as a perfect peg | Gated symmetrically via `abs()` | | `depeg_active` / `depeg_severity` / `depeg_direction` | Confirmed incident state | Gated by `deny_on_active_depeg` | | `depeg_pending_count` | Unconfirmed threshold crossings; does **not** trip `depeg_active` | Reviewer context | | `supply_usd` / `supply_change_7d_usd` / `chain_count` | Market footprint | Reviewer context | | `stress_score` / `stress_band` / `stress_signals` | Stress posture; `stress_signals` is the raw `{name: 0-100}` map | Gated by `max_stress_score` | | `active_stress_indicators` | Convenience view of signals at or above 50. No rule depends on it | Reviewer context | | `liquidity_score` / `effective_tvl_usd` / `volume_24h_usd` / `pool_count` | Liquidity depth | Gated by `min_liquidity_score` | | `exit_capacity_usd` / `exit_capacity_multiple` | Capacity within `max_cost_bps`, and its ratio to the position | Gated by `min_exit_capacity_multiple` | | `redemption_available` / `redemption_route_family` / `redemption_access_model` / `redemption_route_status` / `redemption_score` | Redemption posture | Four separate route rules | | `immediate_capacity_usd` | Redeemable right now | Reviewer context | | `price_data_age_seconds` / `stress_data_age_seconds` / `liquidity_data_age_seconds` / `redemption_data_age_seconds` | Per-source ages — these move on very different clocks | Diagnosing which feed drives staleness | | `data_age_seconds` | The **oldest** of the above | Gated by `max_data_age_seconds` | | `timestamp` | When this snapshot was taken | Audit | ## Per-call wasmArgs This pack ships **no `prepareQuery`**: nothing on-chain maps an ERC-20 address to a Pharos stablecoin id, so the curator supplies it per call. | Field | Required? | Notes | | --- | --- | --- | | `stablecoin_id` | Required | `ticker-issuer` form, e.g. `usdc-circle` | | `transaction_amount_usd` | Optional, defaults to `0` | Position size. Omit and `exit_capacity_multiple` is `null` | | `stress_lookback_days` | Optional, defaults to `7` | Stress window | | `max_cost_bps` | Optional | Execution-cost bound for the capacity curve walk | From the CLI or dashboard these go in the simulate payload's `wasm_args`. From the SDK they go in `sendCall`'s `wasmArgs` bag: ```typescript await shield.morpho.submitCap(vault, marketParams, newCap, { wasmArgs: { pharos_treasury: { stablecoin_id: 'usdc-circle', transaction_amount_usd: 1_000_000, }, }, }) ``` `wasmArgs` requires `@newton-xyz/vaultkit` 2.2.0 or later. See [Policies](/developers/vaults/sdk/policies#per-call-inputs). ## Policy Parameters | Param | Type | Description | | --- | --- | --- | | `deny_on_active_depeg` | `boolean` | Deny outright during a confirmed depeg. Contrast `pharos_safe_mode`'s `safe_mode_on_active_depeg`, which only *engages* safe mode | | `max_peg_deviation_bps` | `number` | Symmetric deviation tolerance in bps | | `max_stress_score` | `number` | Stress ceiling 0-100 | | `require_redemption` | `boolean` | Require a working redemption route | | `approved_redemption_route_families` | `string[]` | Acceptable route families | | `approved_access_models` | `string[]` | Acceptable access models | | `required_route_status` | `string` | Status a working route must report. Pharos reports **`open`**, not `active` | | `min_exit_capacity_multiple` | `number` | Required exit capacity as a multiple of the position | | `min_liquidity_score` | `number` | Liquidity score floor 0-100 | | `max_data_age_seconds` | `number` | Freshness ceiling on the oldest source. Read the Notes before tightening it | | `deny_on_missing_fields` | `string[]` | Field names whose unreported (`null`) value denies. Listing `exit_capacity_multiple` requires every call to supply a position size | ## Rego Checks Every rule this policy enforces: | Deny reason | Condition | What it catches | | --- | --- | --- | | `active_depeg` | `deny_on_active_depeg` and an incident is active | A confirmed depeg in progress | | `peg_deviation_above_max` | `abs(deviation)` over `max_peg_deviation_bps` | Drift off peg before an incident is declared | | `missing_peg_deviation_bps` | deviation is `null` | An unresolvable price. **Cannot be opted out of** | | `stress_above_max` | stress over `max_stress_score` | Developing stress ahead of a depeg | | `redemption_unavailable` | `require_redemption` and no route | No credible exit at par | | `unapproved_redemption_route` | route family not approved | Redemption through an out-of-policy mechanism | | `unapproved_access_model` | access model not approved | The wrong parties can redeem | | `route_status_not_approved` | status is not `required_route_status` | A route reported as impaired or suspended | | `insufficient_exit_capacity` | multiple below `min_exit_capacity_multiple` | A position that cannot realistically be exited | | `liquidity_score_below_min` | score below `min_liquidity_score` | Thin or fragile DEX liquidity | | `stale_data` | age over `max_data_age_seconds` | Decisions made on stale data | | `missing_` | the field is named in `deny_on_missing_fields` and the oracle reported `null` | A configured threshold quietly doing nothing | ### Peg Deviation Deviation is **signed** by the oracle (negative = below peg) because the direction matters to a reader; the threshold is symmetric via `abs()`. A naive unsigned comparison would miss the direction that matters most. ```rego deny contains "peg_deviation_above_max" if abs(v.peg_deviation_bps) > t.max_peg_deviation_bps ``` ### Unresolvable Peg Unconditional, and deliberately **not** a `deny_on_missing_fields` entry: this pack's peg rule is built on the deviation, so an unresolvable one is never tolerable. The oracle used to report `0` here when it could not resolve a price, which reads as a perfect peg — the safest possible input — so it now reports `null`. ```rego deny contains "missing_peg_deviation_bps" if v.peg_deviation_bps == null ``` ### Route Status Pharos reports **`open`** for a working route, not `active`. A curator who guesses `active` denies every healthy asset, which is why the value is a param rather than a constant — and why the pack pins it with a test. ```rego deny contains "route_status_not_approved" if { v.redemption_route_status != null v.redemption_route_status != t.required_route_status } ``` ### Exit Capacity The differentiated Pharos signal: can this position actually be exited? Passing no `transaction_amount_usd` leaves the multiple `null` rather than infinity, so the rule fails soft rather than reading an unbounded ratio as safe. ```rego deny contains "insufficient_exit_capacity" if { v.exit_capacity_multiple != null v.exit_capacity_multiple < t.min_exit_capacity_multiple } ``` ### Final Allow Rule The groundedness probes are load-bearing. `is_number(v.peg_deviation_bps)` also grounds the `abs()` in the peg rule, which is *undefined* rather than false on a non-number. ```rego allow if { not v.error is_boolean(v.depeg_active) is_boolean(v.redemption_available) is_number(v.peg_deviation_bps) count(deny) == 0 } ``` ## Complete Policy ```rego package pharos_treasury_risk import future.keywords default allow := false t := data.params.pharos_treasury v := data.wasm.pharos_treasury nullable_fields := { "stress_score": v.stress_score, "redemption_route_family": v.redemption_route_family, "redemption_access_model": v.redemption_access_model, "redemption_route_status": v.redemption_route_status, "exit_capacity_multiple": v.exit_capacity_multiple, "liquidity_score": v.liquidity_score, "data_age_seconds": v.data_age_seconds, } deny contains "active_depeg" if { t.deny_on_active_depeg v.depeg_active == true } deny contains "peg_deviation_above_max" if abs(v.peg_deviation_bps) > t.max_peg_deviation_bps deny contains "missing_peg_deviation_bps" if v.peg_deviation_bps == null deny contains "stress_above_max" if { v.stress_score != null v.stress_score > t.max_stress_score } deny contains "redemption_unavailable" if { t.require_redemption v.redemption_available == false } deny contains "unapproved_redemption_route" if { v.redemption_route_family != null not v.redemption_route_family in t.approved_redemption_route_families } deny contains "unapproved_access_model" if { v.redemption_access_model != null not v.redemption_access_model in t.approved_access_models } deny contains "route_status_not_approved" if { v.redemption_route_status != null v.redemption_route_status != t.required_route_status } deny contains "insufficient_exit_capacity" if { v.exit_capacity_multiple != null v.exit_capacity_multiple < t.min_exit_capacity_multiple } deny contains "liquidity_score_below_min" if { v.liquidity_score != null v.liquidity_score < t.min_liquidity_score } deny contains "stale_data" if { v.data_age_seconds != null v.data_age_seconds > t.max_data_age_seconds } deny contains sprintf("missing_%v", [name]) if { some name in t.deny_on_missing_fields nullable_fields[name] == null } allow if { not v.error is_boolean(v.depeg_active) is_boolean(v.redemption_available) is_number(v.peg_deviation_bps) count(deny) == 0 } ``` Composing this pack into a VaultKit composite changes only where params live: the manifest envelope puts the curator's slice at `data.params.params.pharos_treasury`. See [Policies](/developers/vaults/sdk/policies). ## Notes * **`required_route_status` is `"open"`, not `"active"`.** Configuring `active` — the intuitive guess — denies every healthy asset. A test pins this. * **Freshness ceilings need care.** The four sources move on very different clocks: price and stress refresh in minutes, `dex-liquidity-history` is a **daily bucket** (up to ~24h old by construction), and redemption-backstops lags by hours. `data_age_seconds` is the oldest of them, so a ceiling below ~24h denies permanently. The per-source ages are emitted separately so you can see which feed is actually driving it. * **`transaction_amount_usd` is caller-supplied and NOT attested.** The attested alternative, `input.value`, is native-token wei (and a *string*), not USD. Pair with a native-value cap in a composite if you need a tamper-proof ceiling. * `null` is the oracle's "not reported", deliberately distinct from `0`. Null optional fields fail soft; a **missing** key leaves the groundedness probes undefined and correctly blocks `allow`. * **`peg_deviation_bps` is the exception** — it denies unconditionally when `null`, as `missing_peg_deviation_bps`, and is not a `deny_on_missing_fields` entry. * Listing `exit_capacity_multiple` in `deny_on_missing_fields` requires every call to supply a position size. Because the list is per field, you can require the rest without that. * **This pack rides within about 40% of a hard runtime memory limit.** `/api/redemption-backstops` has no filter, so the oracle downloads all ~1.14 MB and slices this coin's ~3 KB object out of the raw text. A `MAX_SLICEABLE_BYTES` guard trips first and returns a readable error — which fails closed — rather than trapping the component with no verdict. The durable fix is a per-asset endpoint from Pharos. # Pharos Stablecoin Safe Mode \[Use Pharos stress signals in Newton policies to block exposure-increasing actions under stablecoin stress while leaving the exits open.] This policy is a **graduated response** to stablecoin stress, using [Pharos](https://pharos.watch). When stress rises or a depeg goes active, it stops actions that *increase* exposure while leaving the exits open: deposits and mints are blocked, withdrawals, redemptions and repayments are always permitted, and swaps are permitted only into a curator-approved safer asset. That is the point — an application gets a graduated response instead of a blunt protocol-wide pause. :::note This is the only pack in the catalog that reads the **attested intent** as well as oracle data. A graduated response has to know whether the caller is adding exposure or shedding it, and that fact must come from the signed intent rather than from anything the oracle asserts. ::: ## Deployment | Field | Value | | --- | --- | | Pack id | `pharos_safe_mode` | | Rego package | `pharos_safe_mode` (the `--entrypoint` is `pharos_safe_mode.allow`) | | PolicyData addresses | [VaultKit address table](/developers/vaults/policy-packs#deployed-policydata-addresses) | | Canonical deployments | [`deployments.json`](https://github.com/newt-foundation/newton-policy-packs/blob/main/deployments.json) | :::note This oracle is not deployed yet, so it has no row in the VaultKit address table. [`deployments.json`](https://github.com/newt-foundation/newton-policy-packs/blob/main/deployments.json) is the source of truth for every pack, chain, and environment. ::: ### Secret | Secret | Required? | Where to get it | | --- | --- | --- | | `PHAROS_API_KEY` | Required | [pharos.watch](https://pharos.watch) | Sent by the oracle as the `X-API-Key` request header. ## Data Inputs Three GET requests against `https://api.pharos.watch`, all filtered by `stablecoin`, totalling roughly 11 KB per evaluation — the lightest Pharos pack: `/api/stress-signals?stablecoin={id}&days={n}`, `/api/depeg-events?stablecoin={id}&active=true&includePending=true`, and `/api/mint-burn-flows?stablecoin={id}&hours={n}`. | Field | What it means | Why the policy uses it | | --- | --- | --- | | `stablecoin_id` / `symbol` | Asset identity | Audit | | `stress_score` / `stress_band` | From `current.score` / `current.band`, lowercased | Gated by `safe_mode_stress_threshold` | | `stress_signals` | Raw `{name: 0-100}` map from `current.signals`, skipping unavailable ones | Reviewer context | | `active_indicators` | Convenience view of signals at or above 50. No rule depends on it | Reviewer context | | `age_classification` | Pharos's own freshness label (e.g. `fresh`) | Reviewer context | | `depeg_active` / `depeg_severity` / `peg_deviation_bps` | Active incident state. `peg_deviation_bps` is informational here and is `null`, never `0`, when unresolvable | `depeg_active` engages safe mode | | `net_flow_usd` / `mint_volume_usd` / `burn_volume_usd` | Mint/burn flow | Reviewer context | | `flow_stress_score` / `burn_surge` | From the stress `flow` signal, which already folds burn surge and burn/mint ratio against a baseline | Reviewer context | | `flow_anomaly` | True when `flow_stress_score` is at or above 50 | Reviewer context | | `data_age_seconds` | Oldest age across all three responses | Gated by `max_data_age_seconds` | | `timestamp` | When this snapshot was taken | Audit | ### The attested intent The classifier is `input.function.name` — the bare function name, verified against newton-cli 0.5.2. The full `input` shape: ```json { "from": "0x...", "to": "0x...", "value": "1000000000000000000", "chain_id": "1", "data": "0xb460af94...", "function_signature": "0x7769746864726177...", "function": { "name": "withdraw", "type": "function", "stateMutability": "nonpayable", "inputs": [], "outputs": [] }, "decoded_function_signature": "function withdraw(uint256, address, address)", "decoded_function_arguments": ["1000000000000000000", "0x...", "0x..."] } ``` ## Per-call wasmArgs This pack ships **no `prepareQuery`**: nothing on-chain maps an ERC-20 address to a Pharos stablecoin id, so the curator supplies it per call. Note there is deliberately **no amount** — this pack classifies the *action*, not the size. | Field | Required? | Notes | | --- | --- | --- | | `stablecoin_id` | Required | `ticker-issuer` form, e.g. `usdc-circle` | | `stress_lookback_days` | Optional, defaults to `7` | Stress window | | `flow_window_hours` | Optional, defaults to `24` | Mint/burn flow window | From the CLI or dashboard these go in the simulate payload's `wasm_args`. From the SDK they go in `sendCall`'s `wasmArgs` bag: ```typescript await shield.morpho.submitCap(vault, marketParams, newCap, { wasmArgs: { pharos_safe_mode: { stablecoin_id: 'usdc-circle' }, }, }) ``` `wasmArgs` requires `@newton-xyz/vaultkit` 2.2.0 or later. See [Policies](/developers/vaults/sdk/policies#per-call-inputs). ## Policy Parameters | Param | Type | Description | | --- | --- | --- | | `safe_mode_stress_threshold` | `number` | Stress score at or above which safe mode engages | | `safe_mode_on_active_depeg` | `boolean` | Let an active depeg engage safe mode on its own. **Engages** safe mode — does not deny outright | | `exposure_increasing_functions` | `string[]` | Bare names blocked in safe mode (e.g. `deposit`, `mint`) | | `exposure_reducing_functions` | `string[]` | Bare names always permitted (e.g. `withdraw`, `redeem`) | | `swap_functions` | `string[]` | Bare names treated as swaps | | `swap_destination_arg_index` | `object` | Map from swap function name to the index into `decoded_function_arguments` holding the destination token | | `approved_safe_assets` | `string[]` | Token addresses acceptable as swap destinations. Compared case-insensitively | | `max_data_age_seconds` | `number` | Freshness ceiling | | `deny_on_missing_fields` | `string[]` | Field names (`stress_score`, `data_age_seconds`) whose unreported (`null`) value denies rather than passes | :::warning The three function lists are an **allowlist**. A function in none of them denies as `unclassified_function` — in a calm market too, not just under stress. Enumerate every function your vault actually calls. ::: ## Rego Checks Every rule this policy enforces: | Deny reason | Condition | What it catches | | --- | --- | --- | | `unclassified_function` | the function is in none of the three lists | A novel call that would otherwise sail past every rule | | `safe_mode_blocks_exposure_increase` | safe mode and an exposure-increasing call | Adding exposure to a stressed asset | | `unapproved_swap_destination` | safe mode, a swap, destination not approved | Rotating out of a stressed asset into another risky one | | `missing_swap_destination_index` | a listed swap function has no mapped index | A swap whose destination cannot be evaluated at all | | `stale_data` | age over `max_data_age_seconds` | Decisions made on stale data | | `missing_` | the field is named in `deny_on_missing_fields` and the oracle reported `null` | A stress threshold quietly doing nothing | ### Safe-Mode Trigger Named `safe_mode_on_*`, not `deny_on_*`, because that is what it does: an active depeg **engages safe mode**, it does not deny outright. Exposure-reducing calls stay permitted throughout. If you want a depeg to stop everything, [`pharos_treasury`](/developers/vaults/policies/pharos-treasury)'s `deny_on_active_depeg` is the blunt instrument; the two are deliberately named apart. ```rego safe_mode if { t.safe_mode_on_active_depeg v.depeg_active == true } safe_mode if { v.stress_score != null v.stress_score >= t.safe_mode_stress_threshold } ``` ### Action Classification Fail closed on an action the curator has not classified. Without `unclassified_function`, a novel function name would sail past every rule below. ```rego fn := input.function.name is_reducing if fn in t.exposure_reducing_functions is_increasing if fn in t.exposure_increasing_functions is_swap if fn in t.swap_functions classified if is_reducing classified if is_increasing classified if is_swap deny contains "unclassified_function" if not classified deny contains "safe_mode_blocks_exposure_increase" if { safe_mode is_increasing } ``` ### Swap Destination Keyed by function name, because swap ABIs disagree on where the destination sits: `swap(tokenIn, tokenOut, amt)` is index 1, `swapExactIn(tokenOut, amt)` is 0, and a Curve-style `exchange(i, j, dx)` has no address argument at all and belongs unmapped. `has_swap_index` is a named helper for the same reason as `arkham_counterparty`'s multiplier guard: OPA hoists the ref out of a bare `not t.swap_destination_arg_index[fn]`, so an unmapped function would leave the rule *undefined* rather than negating to true. `is_number` also keeps a legitimate index of `0` valid. ```rego swap_destination_index := t.swap_destination_arg_index[fn] swap_destination := lower(input.decoded_function_arguments[swap_destination_index]) has_swap_index if is_number(swap_destination_index) swap_destination_approved if { some asset in t.approved_safe_assets lower(asset) == swap_destination } deny contains "unapproved_swap_destination" if { safe_mode is_swap not swap_destination_approved } deny contains "missing_swap_destination_index" if { is_swap not has_swap_index } ``` ### Final Allow Rule `is_boolean(v.depeg_active)` grounds the oracle payload and `is_string(fn)` grounds the intent. Note what is deliberately **absent**: there is no blanket stress ceiling that denies everything. Withdrawals and redemptions stay permitted at any stress level. ```rego allow if { not v.error is_boolean(v.depeg_active) is_string(fn) count(deny) == 0 } ``` ## Complete Policy ```rego package pharos_safe_mode import future.keywords default allow := false t := data.params.pharos_safe_mode v := data.wasm.pharos_safe_mode nullable_fields := { "stress_score": v.stress_score, "data_age_seconds": v.data_age_seconds, } fn := input.function.name safe_mode if { t.safe_mode_on_active_depeg v.depeg_active == true } safe_mode if { v.stress_score != null v.stress_score >= t.safe_mode_stress_threshold } is_reducing if fn in t.exposure_reducing_functions is_increasing if fn in t.exposure_increasing_functions is_swap if fn in t.swap_functions classified if is_reducing classified if is_increasing classified if is_swap swap_destination_index := t.swap_destination_arg_index[fn] swap_destination := lower(input.decoded_function_arguments[swap_destination_index]) has_swap_index if is_number(swap_destination_index) swap_destination_approved if { some asset in t.approved_safe_assets lower(asset) == swap_destination } deny contains "unclassified_function" if not classified deny contains "safe_mode_blocks_exposure_increase" if { safe_mode is_increasing } deny contains "unapproved_swap_destination" if { safe_mode is_swap not swap_destination_approved } deny contains "missing_swap_destination_index" if { is_swap not has_swap_index } deny contains "stale_data" if { v.data_age_seconds != null v.data_age_seconds > t.max_data_age_seconds } deny contains sprintf("missing_%v", [name]) if { some name in t.deny_on_missing_fields nullable_fields[name] == null } allow if { not v.error is_boolean(v.depeg_active) is_string(fn) count(deny) == 0 } ``` Composing this pack into a VaultKit composite changes only where params live: the manifest envelope puts the curator's slice at `data.params.params.pharos_safe_mode`. See [Policies](/developers/vaults/sdk/policies). :::warning Composing this pack with [`pharos_treasury`](/developers/vaults/policies/pharos-treasury) changes the **shape** of the response, not just its strictness. `pharos_treasury.deny_on_active_depeg` denies everything during a depeg, including the unwind this pack would have permitted. AND-composition takes the stricter of the two, so set `deny_on_active_depeg: false` if you want the graduated behaviour to survive composition. ::: ## Notes * **Unclassified functions deny, always** — including in a calm market. The three function lists are an allowlist, not a safe-mode-only concern. * **`swap_destination_arg_index` is keyed per function**, because swap ABIs disagree on where the destination sits. Give each name in `swap_functions` its own entry — `{"swap": 1, "swapExactIn": 0}`. * **A swap function with no mapped index denies** as `missing_swap_destination_index`, in a calm market as well as in safe mode: the policy cannot evaluate the destination at all, so it fails closed rather than waving the call through. Leave a Curve-style `exchange` unmapped and it is denied, not misread. * **An out-of-range index also fails closed.** The destination is left undefined, which denies rather than reading as approved. * Swap destinations are compared **case-insensitively**: the intent lowercases addresses while curator params are typically checksummed. * `null` is the oracle's "not reported", deliberately distinct from `0`. Worth listing `stress_score` in `deny_on_missing_fields` if you rely on `safe_mode_stress_threshold` — without a score, safe mode can only ever engage via the depeg branch. * `input.chain_id` arrives as a *string* and is only populated when the intent JSON sets `chainId` (camelCase in, snake\_case out). The `--chain-id` CLI flag does **not** populate it, so no rule here depends on it. * **`/api/mint-burn-flows` carries no anomaly flag of its own.** Pharos's judgement of flow abnormality lives in the stress `flow` signal, so the oracle uses that rather than inventing a threshold over raw mint/burn volumes. * Stress data is nested under `current` (`current.score`, `current.band`, `current.signals`) — not at the top level. # Pharos Redemption-Backed Asset Gate \[Use Pharos redemption data in Newton policies to admit a stablecoin only when it can actually be redeemed at par.] This policy admits a stablecoin only when it can actually be **redeemed at par**, using [Pharos](https://pharos.watch). Market price says nothing about whether a holder can convert back to the underlying, so this pack gates on the redemption path itself: direct redemption must be available, the provider and mechanism must be approved, applicable limits must support the intended position size, and the oracle response must be current. ## Deployment | Field | Value | | --- | --- | | Pack id | `pharos_redemption` | | Rego package | `pharos_redemption_backing` (the `--entrypoint` is `pharos_redemption_backing.allow`) | | PolicyData addresses | [VaultKit address table](/developers/vaults/policy-packs#deployed-policydata-addresses) | | Canonical deployments | [`deployments.json`](https://github.com/newt-foundation/newton-policy-packs/blob/main/deployments.json) | :::note This oracle is not deployed yet, so it has no row in the VaultKit address table. [`deployments.json`](https://github.com/newt-foundation/newton-policy-packs/blob/main/deployments.json) is the source of truth for every pack, chain, and environment. ::: ### Secret | Secret | Required? | Where to get it | | --- | --- | --- | | `PHAROS_API_KEY` | Required | [pharos.watch](https://pharos.watch) | Sent by the oracle as the `X-API-Key` request header. ## Data Inputs Two GET requests against `https://api.pharos.watch`: 1. `/api/redemption-backstops` — the redemption map. This endpoint takes **no query parameters at all** and returns all ~328 coins as a 1.1 MB document. `JSON.parse` on that exhausts the WASM heap, so this coin's object is cut out of the **raw text** and only that ~3 KB is parsed. 2. `/api/stablecoin-reserves/{id}` — ~1.4 KB, reserve composition and provenance. "Available" means Pharos actually reported a route for this asset — an absent entry is a genuine "no redemption path", not a soft null. | Field | What it means | Why the policy uses it | | --- | --- | --- | | `stablecoin_id` / `symbol` / `issuer` | Identity, derived from the `ticker-issuer` id | Audit | | `redemption_available` | Whether a route was reported at all | Gated by `require_redemption_available` | | `route_family` / `access_model` / `settlement_model` / `execution_model` | How redemption works, who may use it, how it settles | Three separate approval rules | | `route_status` | Route health. Pharos reports **`open`** for a working route, not `active` | Gated by `required_route_status` | | `holder_eligibility` / `provider` / `source_mode` | Who qualifies, and who Pharos attributes the route to | Reviewer context | | `immediate_capacity_usd` / `modeled_exit_size_usd` | What can be redeemed now, and the size Pharos modelled | Basis for the capacity multiple | | `capacity_multiple` | `immediate_capacity_usd / transaction_amount_usd`, or `null` when no amount was supplied | Gated by `min_capacity_multiple` | | `capacity_confidence` | Qualitative band (e.g. `documented-bound`) — a **string**, not a number | Gated by `approved_capacity_confidence` | | `route_score` / `access_score` / `settlement_score` / `capacity_score` | Pharos's 0-100 quality measures | `route_score` gated by `min_route_score` | | `fee_bps` / `queue_enabled` | Cost and whether redemptions queue | Reviewer context | | `reserve_composition` | `{slice name: percentage}` | Reviewer context | | `reserve_elevated_risk_pct` | Share of reserves Pharos rates worse than low risk | Gated by `max_reserve_elevated_risk_pct` | | `reserve_mode` / `reserve_source` / `reserve_sync_status` / `reserve_stale` | Backing provenance | Reviewer context | | `data_age_seconds` | Oldest age across both responses | Gated by `max_data_age_seconds` | | `timestamp` | When this snapshot was taken | Audit | ## Per-call wasmArgs This pack ships **no `prepareQuery`**: nothing on-chain maps an ERC-20 address to a Pharos stablecoin id, so the curator supplies it per call. | Field | Required? | Notes | | --- | --- | --- | | `stablecoin_id` | Required | `ticker-issuer` form, e.g. `usdc-circle` | | `transaction_amount_usd` | Optional, defaults to `0` | Position size. Omit and `capacity_multiple` is `null` | From the CLI or dashboard these go in the simulate payload's `wasm_args`. From the SDK they go in `sendCall`'s `wasmArgs` bag: ```typescript await shield.morpho.submitCap(vault, marketParams, newCap, { wasmArgs: { pharos_redemption: { stablecoin_id: 'usdc-circle', transaction_amount_usd: 1_000_000, }, }, }) ``` `wasmArgs` requires `@newton-xyz/vaultkit` 2.2.0 or later. See [Policies](/developers/vaults/sdk/policies#per-call-inputs). ## Policy Parameters | Param | Type | Description | | --- | --- | --- | | `require_redemption_available` | `boolean` | Require a working redemption route | | `approved_route_families` | `string[]` | Acceptable route families (observed: `offchain-issuer`) | | `approved_access_models` | `string[]` | Acceptable access models (observed: `issuer-api`) | | `approved_settlement_models` | `string[]` | Acceptable settlement models (observed: `same-day`) | | `required_route_status` | `string` | Status a working route must report — **`open`**, not `active` | | `min_route_score` | `number` | Minimum 0-100 route score | | `approved_capacity_confidence` | `string[]` | Acceptable confidence bands; an empty array disables the check | | `min_capacity_multiple` | `number` | Required immediate capacity as a multiple of the position | | `max_reserve_elevated_risk_pct` | `number` | Max share of reserves rated worse than low risk | | `max_data_age_seconds` | `number` | Freshness ceiling; the feed lags hours, so keep this generous | | `deny_on_missing_fields` | `string[]` | Field names whose unreported (`null`) value denies. Listing `capacity_multiple` requires every call to supply a position size | ## Rego Checks Every rule this policy enforces: | Deny reason | Condition | What it catches | | --- | --- | --- | | `redemption_unavailable` | `require_redemption_available` and no route | No redemption path at all | | `unapproved_route_family` | family not approved | Redemption through an out-of-policy mechanism | | `unapproved_access_model` | access model not approved | The wrong parties can redeem | | `unapproved_settlement_model` | settlement model not approved | Settlement slower than the curator accepts | | `route_status_not_approved` | status is not `required_route_status` | An impaired or suspended route | | `position_exceeds_capacity` | multiple below `min_capacity_multiple` | A position larger than what can actually be redeemed now | | `low_route_score` | score below `min_route_score` | A route Pharos rates poorly | | `unapproved_capacity_confidence` | band not in `approved_capacity_confidence` | Capacity Pharos cannot evidence | | `reserve_risk_above_max` | elevated-risk share over `max_reserve_elevated_risk_pct` | Backing concentrated in riskier assets | | `stale_data` | age over `max_data_age_seconds` | Decisions made on stale data | | `missing_` | the field is named in `deny_on_missing_fields` and the oracle reported `null` | A configured threshold quietly doing nothing | ### Route Status Pharos reports **`open`** for a working route, not `active`. Configuring `active` — the intuitive guess — denies every healthy asset. A test pins this. ```rego deny contains "route_status_not_approved" if { v.route_status != null v.route_status != t.required_route_status } ``` ### Position Sizing Sizing is against **immediate** capacity: Pharos publishes no daily limit and no redemption minimum on this endpoint. Passing no amount leaves the multiple `null` and the rule fails soft. ```rego deny contains "position_exceeds_capacity" if { v.capacity_multiple != null v.capacity_multiple < t.min_capacity_multiple } ``` ### Capacity Confidence `capacity_confidence` is a qualitative **string** band, not a number, so this is a membership check rather than a floor. The `count(...) > 0` guard is what lets an empty `approved_capacity_confidence` disable the check entirely. ```rego deny contains "unapproved_capacity_confidence" if { count(t.approved_capacity_confidence) > 0 v.capacity_confidence != null not v.capacity_confidence in t.approved_capacity_confidence } ``` ### Final Allow Rule The groundedness probe is load-bearing: every deny rule silent-skips on an undefined field, so an error envelope yields an **empty** deny set and a bare `count(deny) == 0` would fail *open*. ```rego allow if { not v.error is_boolean(v.redemption_available) count(deny) == 0 } ``` ## Complete Policy ```rego package pharos_redemption_backing import future.keywords default allow := false t := data.params.pharos_redemption v := data.wasm.pharos_redemption nullable_fields := { "route_family": v.route_family, "access_model": v.access_model, "settlement_model": v.settlement_model, "route_status": v.route_status, "capacity_multiple": v.capacity_multiple, "route_score": v.route_score, "capacity_confidence": v.capacity_confidence, "reserve_elevated_risk_pct": v.reserve_elevated_risk_pct, "data_age_seconds": v.data_age_seconds, } deny contains "redemption_unavailable" if { t.require_redemption_available v.redemption_available == false } deny contains "unapproved_route_family" if { v.route_family != null not v.route_family in t.approved_route_families } deny contains "unapproved_access_model" if { v.access_model != null not v.access_model in t.approved_access_models } deny contains "unapproved_settlement_model" if { v.settlement_model != null not v.settlement_model in t.approved_settlement_models } deny contains "route_status_not_approved" if { v.route_status != null v.route_status != t.required_route_status } deny contains "position_exceeds_capacity" if { v.capacity_multiple != null v.capacity_multiple < t.min_capacity_multiple } deny contains "low_route_score" if { v.route_score != null v.route_score < t.min_route_score } deny contains "unapproved_capacity_confidence" if { count(t.approved_capacity_confidence) > 0 v.capacity_confidence != null not v.capacity_confidence in t.approved_capacity_confidence } deny contains "reserve_risk_above_max" if { v.reserve_elevated_risk_pct != null v.reserve_elevated_risk_pct > t.max_reserve_elevated_risk_pct } deny contains "stale_data" if { v.data_age_seconds != null v.data_age_seconds > t.max_data_age_seconds } deny contains sprintf("missing_%v", [name]) if { some name in t.deny_on_missing_fields nullable_fields[name] == null } allow if { not v.error is_boolean(v.redemption_available) count(deny) == 0 } ``` Composing this pack into a VaultKit composite changes only where params live: the manifest envelope puts the curator's slice at `data.params.params.pharos_redemption`. See [Policies](/developers/vaults/sdk/policies). ## Notes * **`required_route_status` is `"open"`, not `"active"`** — configuring `active` denies every healthy asset. A test pins this. * **Pharos publishes no daily limit and no redemption minimum.** Sizing is therefore against `immediate_capacity_usd`, and there is no `below_min_redeem` rule. * **There is no numeric confidence either.** `capacity_confidence` is a qualitative band (`documented-bound`), so route quality is gated on the 0-100 `route_score`. * **`transaction_amount_usd` is caller-supplied and NOT attested.** See [`pharos_treasury`](/developers/vaults/policies/pharos-treasury) for the same caveat. * `null` is the oracle's "not reported", distinct from `0`. Null optional fields fail soft; a **missing** key blocks `allow`. * Listing `capacity_multiple` in `deny_on_missing_fields` requires every call to supply a position size. Because the list is per field, you can require the rest without that. * **This pack rides within about 40% of a hard runtime memory limit.** `/api/redemption-backstops` has no filter, so the oracle downloads all ~1.14 MB and slices this coin's ~3 KB object out of the raw text. A `MAX_SLICEABLE_BYTES` guard trips first and returns a readable error — which fails closed — rather than trapping the component with no verdict. The durable fix is a per-asset endpoint from Pharos. # Xerberus Liquidity Exit \[Use Xerberus exit quotes in Newton policies to deny positions that cannot be unwound quickly at a bounded market impact.] This policy asks one question before an agent or curator takes on a position: **can it be unwound?** Using [Xerberus](https://xerberus.io), it estimates how many days selling the proposed notional would take while staying inside a market-impact band, and denies positions that would take too long to exit. It is an asset-level check. It does not tell you whether the portfolio as a whole becomes too concentrated. Pair it with [Xerberus Portfolio What-If](/developers/vaults/policies/xerberus-what-if) and [Xerberus Common Cause](/developers/vaults/policies/xerberus-common-cause) for that. ## Deployment | Field | Value | | --- | --- | | Pack id | `xerberus_liquidity_exit` | | Rego package | `xerberus_liquidity_exit` (the `--entrypoint` is `xerberus_liquidity_exit.allow`) | | PolicyData addresses | [VaultKit address table](/developers/vaults/policy-packs#deployed-policydata-addresses) | | Canonical deployments | [`deployments.json`](https://github.com/newt-foundation/newton-policy-packs/blob/main/deployments.json) | :::note This oracle is not deployed yet, so it has no row in the VaultKit address table. [`deployments.json`](https://github.com/newt-foundation/newton-policy-packs/blob/main/deployments.json) is the source of truth for every pack, chain, and environment. ::: ### Secret | Secret | Required? | Where to get it | | --- | --- | --- | | `XERBERUS_API_KEY` | Required | [xerberus.io](https://xerberus.io) | Sent by the oracle as the `x-api-key` request header. ## Data Inputs One MCP `tools/call` of the `liquidity_exit_quote` tool, sent as JSON-RPC 2.0 to `https://mcp.xerberus.io/enterprise/mcp`. Xerberus exposes its risk functions as MCP tools rather than REST paths, so all three Xerberus packs share one transport: * Non-2xx HTTP, a JSON-RPC `error`, or a tool result with `isError` all fail the oracle, and the policy fails closed. * The response may be plain JSON or a single-shot `text/event-stream`. Either way the oracle reads the JSON-RPC message carrying its request id. * `structuredContent` is preferred; otherwise the JSON in the tool's text content is parsed. | Field | What it means | Why the policy uses it | | --- | --- | --- | | `token` | Echoed from `wasmArgs`. Addresses are lowercased | Gated by `require_token_in_intent` | | `symbol` / `chain` | Token symbol; chain (always `ethereum` today) | Audit | | `notional_usd` | Position size quoted | Audit | | `impact_band_pct` | Impact band the quote was modeled at | Gated by `max_impact_band_pct` | | `max_sale_per_day_usd` | Most that can be sold per day inside the band | `no_exit_liquidity` | | `days_to_exit` | Estimated days to fully exit | Gated by `max_days_to_exit` | | `classification` | Xerberus's label, e.g. moderate, under one week | Reviewer context | | `cex_status` | Whether a centralized spot market was found | Reviewer context | | `requested_window` / `data_window` | The window asked for, and the window Xerberus answered from | Audit | | `window_honored` | `true` or `false` when a window was pinned, `null` otherwise | `window_not_honored` | | `is_stale` | Xerberus's own freshness flag | Gated by `deny_on_stale` | | `data_age_seconds` | Age of `data_window` at evaluation time | Gated by `max_data_age_seconds` | ## Per-call wasmArgs This pack ships **no `prepareQuery`**: the token and notional of the position being acquired are not something the SDK can read off the vault, so the curator supplies them per call. | Field | Required? | Notes | | --- | --- | --- | | `token` | Required | Token contract address. Symbols work but are ambiguous and can't be bound to the intent | | `usd_notional` | Required | Proposed position size in USD | | `impact_pct` | Optional | Maximum acceptable market impact, in percent. Use 0.5 for stablecoins | | `chain` | Optional | `ethereum`, the only chain Xerberus scores today. Defaults to `ethereum` | | `window` | Optional | Xerberus data-window timestamp (ISO-8601). Pass the same value to every Xerberus pack | From the CLI or dashboard this goes in the simulate payload's `wasm_args`. From the SDK it goes in `sendCall`'s `wasmArgs` bag: ```typescript await shield.morpho.submitCap(vault, marketParams, newCap, { wasmArgs: { xerberus_liquidity_exit: { token: '0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0', // wstETH usd_notional: 1_000_000, impact_pct: 1, window, }, }, }) ``` `wasmArgs` requires `@newton-xyz/vaultkit` 2.2.0 or later. See [Policies](/developers/vaults/sdk/policies#per-call-inputs). ## Policy Parameters | Param | Type | Description | | --- | --- | --- | | `max_days_to_exit` | `number` | Days-to-exit ceiling. The Xerberus reference design uses 7 | | `max_impact_band_pct` | `number` | Loosest impact band accepted, so a caller can't request a looser band than you allow. Reference: 1 (0.5 for stablecoins) | | `deny_on_stale` | `boolean` | Deny when Xerberus flags its data window as stale. Keep on for exposure-increasing actions | | `max_data_age_seconds` | `number` | Oldest data window accepted | | `require_token_in_intent` | `boolean` | Deny unless the quoted token is the intent's target or appears in its decoded arguments | ## Rego Checks | Deny reason | Condition | What it catches | | --- | --- | --- | | `oracle_error` | The oracle emitted `{error}` | Transport, auth, or tool failures | | `malformed_oracle_output` | A field the rules read is missing or `null` | An unpriced quote or a changed response shape | | `stale_data` | `deny_on_stale` and `is_stale` | Decisions made on data Xerberus itself calls stale | | `data_too_old` | `data_age_seconds > max_data_age_seconds` | Data older than you accept | | `window_not_honored` | A window was pinned and Xerberus answered from another | Results that can't be lined up with the other Xerberus packs | | `exit_too_slow` | `days_to_exit > max_days_to_exit` | Positions that take too long to unwind | | `impact_band_exceeded` | `impact_band_pct > max_impact_band_pct` | A quote modeled at a looser impact than you accept | | `no_exit_liquidity` | `max_sale_per_day_usd <= 0` | No modeled exit at all | | `token_not_in_intent` | `require_token_in_intent` and the token isn't the target or an argument | A quote for one token authorizing a transaction on another | ### Intent Binding The token is matched against the call target and every decoded argument. `contains` rather than equality lets an address nested in a tuple argument still match. ```rego intent_mentions(addr) if lower(input.to) == addr intent_mentions(addr) if { some arg in input.decoded_function_arguments is_string(arg) contains(lower(arg), addr) } deny contains "token_not_in_intent" if { t.require_token_in_intent is_string(v.token) not intent_mentions(lower(v.token)) } ``` ### Final Allow Rule `well_formed` requires every field a deny rule reads, including the freshness fields. An error envelope, an empty slot, or an unpriced quote leaves them undefined, so a bare `count(deny) == 0` would fail *open*. ```rego allow if { not v.error well_formed count(deny) == 0 } ``` ## Complete Policy ```rego package xerberus_liquidity_exit import future.keywords default allow := false t := data.params.xerberus_liquidity_exit v := data.wasm.xerberus_liquidity_exit well_formed if { is_string(v.token) is_number(v.days_to_exit) is_number(v.max_sale_per_day_usd) is_number(v.impact_band_pct) is_boolean(v.is_stale) is_number(v.data_age_seconds) } intent_mentions(addr) if lower(input.to) == addr intent_mentions(addr) if { some arg in input.decoded_function_arguments is_string(arg) contains(lower(arg), addr) } deny contains "oracle_error" if v.error deny contains "malformed_oracle_output" if { not v.error not well_formed } deny contains "stale_data" if { t.deny_on_stale v.is_stale == true } deny contains "data_too_old" if v.data_age_seconds > t.max_data_age_seconds deny contains "window_not_honored" if v.window_honored == false deny contains "exit_too_slow" if v.days_to_exit > t.max_days_to_exit deny contains "impact_band_exceeded" if v.impact_band_pct > t.max_impact_band_pct deny contains "no_exit_liquidity" if { is_number(v.max_sale_per_day_usd) v.max_sale_per_day_usd <= 0 } deny contains "token_not_in_intent" if { t.require_token_in_intent is_string(v.token) not intent_mentions(lower(v.token)) } allow if { not v.error well_formed count(deny) == 0 } ``` Composing this pack into a VaultKit composite changes only where params live: the manifest envelope puts the curator's slice at `data.params.params.xerberus_liquidity_exit`. See [Policies](/developers/vaults/sdk/policies). ## Notes * **Stale data fails closed.** A missing `is_stale` or an unparseable `data_window` makes the output malformed, which blocks `allow`. * **Pin the window when composing.** Pass one `window` to all three Xerberus packs so their results come from the same Xerberus snapshot. Never combine a fresh exit quote with an older portfolio or common-cause result. * **Use token addresses.** A symbol can't be matched against the intent, so `require_token_in_intent` only works with an address. * **The notional is caller-supplied.** The pack can't price calldata itself, so your intent builder is responsible for passing the real USD size. * **Exit time is asset-level.** In Xerberus's reference scenario, a $1M wstETH position exits in about 5.3 days and passes a 7-day limit, yet the same trade makes the portfolio more concentrated and less liquid. Only [Portfolio What-If](/developers/vaults/policies/xerberus-what-if) catches that. # Xerberus Portfolio What-If \[Use Xerberus portfolio simulations in Newton policies to deny trades that concentrate the book or make it harder to exit.] This policy catches transactions that look fine in isolation but make the **portfolio** riskier. Using [Xerberus](https://xerberus.io), it simulates the managed book before and after a proposed change and denies one that: * pushes the largest position past a concentration limit while growing it, * raises the token Herfindahl-Hirschman index (HHI) too far, * shrinks the share of the book that can be exited within 30 days, or * lengthens the portfolio's slowest exit. Xerberus returns deterministic exposure and liquidity deltas here. It does not rerun a systemic rating or a tail-risk model. ## Deployment | Field | Value | | --- | --- | | Pack id | `xerberus_what_if` | | Rego package | `xerberus_portfolio_what_if` (the `--entrypoint` is `xerberus_portfolio_what_if.allow`) | | PolicyData addresses | [VaultKit address table](/developers/vaults/policy-packs#deployed-policydata-addresses) | | Canonical deployments | [`deployments.json`](https://github.com/newt-foundation/newton-policy-packs/blob/main/deployments.json) | :::note This oracle is not deployed yet, so it has no row in the VaultKit address table. [`deployments.json`](https://github.com/newt-foundation/newton-policy-packs/blob/main/deployments.json) is the source of truth for every pack, chain, and environment. ::: ### Secret | Secret | Required? | Where to get it | | --- | --- | --- | | `XERBERUS_API_KEY` | Required | [xerberus.io](https://xerberus.io) | Sent by the oracle as the `x-api-key` request header. ## Data Inputs One MCP `tools/call` of the `what_if` tool against `https://mcp.xerberus.io/enterprise/mcp`, using the same transport as [Xerberus Liquidity Exit](/developers/vaults/policies/xerberus-liquidity-exit#data-inputs). Position shares are derived from `usd / book_usd`, so the unit never depends on whether Xerberus reports `pct` as a fraction or a percentage. Deltas come from Xerberus's `delta` block and fall back to `after − before`. | Field | What it means | Why the policy uses it | | --- | --- | --- | | `wallets` / `add` / `remove` | Echoed from `wasmArgs`. Addresses are lowercased | Gated by `require_intent_party_in_wallets` | | `is_risk_reducing` | `true` for a pure removal with no additions | The staleness exemption | | `book_usd_before` / `book_usd_after` | Portfolio value either side of the change | Reviewer context | | `post_largest_token` / `post_largest_token_pct` | Largest position after the change, and its share of the book in percent | Gated by `max_post_token_share_pct` | | `pre_largest_token_pct` | That token's share before the change (0 if outside the reported top positions) | Concentration only denies when it grows | | `hhi_before` / `hhi_after` / `hhi_delta` | Token HHI (0–1 scale) and its change | Gated by `max_hhi_delta` | | `ladder_30d_before_pct` / `ladder_30d_after_pct` / `ladder_30d_delta_pp` | Share of the book exitable within 30 days, and its change in percentage points | Gated by `min_ladder_30d_delta_pp` | | `slowest_exit_days_before` / `slowest_exit_days_after` / `slowest_exit_delta_days` | Slowest token exit and its change | Gated by `max_slowest_exit_delta_days` | | `issues` / `issues_count` | Simulation issues, flattened to strings (first 10), and their count | Gated by `deny_on_issues` | | `requested_window` / `data_window` / `window_honored` / `is_stale` / `data_age_seconds` | Freshness, as in [Liquidity Exit](/developers/vaults/policies/xerberus-liquidity-exit#data-inputs) | Freshness rules | ## Per-call wasmArgs This pack ships **no `prepareQuery`**: the change being simulated comes from the action, which the SDK can't price, so the curator supplies it per call. | Field | Required? | Notes | | --- | --- | --- | | `wallets` | Required | 1–10 wallet addresses that make up the managed portfolio | | `add` | Optional | Up to 10 `{ token, usd }` additions. At least one of `add` or `remove` must be non-empty | | `remove` | Optional | Up to 10 `{ token, usd }` removals. Removals with no additions count as risk-reducing | | `impact_pct` | Optional | Market-impact assumption, in percent | | `chain` | Optional | `ethereum` only today. Defaults to `ethereum` | | `window` | Optional | Xerberus data-window timestamp (ISO-8601). Pass the same value to every Xerberus pack | From the SDK: ```typescript await shield.morpho.submitCap(vault, marketParams, newCap, { wasmArgs: { xerberus_what_if: { wallets: [vault], add: [{ token: '0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0', usd: 1_000_000 }], window, }, }, }) ``` `wasmArgs` requires `@newton-xyz/vaultkit` 2.2.0 or later. See [Policies](/developers/vaults/sdk/policies#per-call-inputs). ## Policy Parameters | Param | Type | Description | | --- | --- | --- | | `max_post_token_share_pct` | `number` | Concentration ceiling, in percent. The Xerberus reference design uses 50 | | `max_hhi_delta` | `number` | Largest tolerated HHI increase. Reference: 0.01 | | `min_ladder_30d_delta_pp` | `number` | Largest tolerated drop in the 30-day exitable share, in percentage points (negative). Reference: −3 | | `max_slowest_exit_delta_days` | `number` | Largest tolerated increase in the slowest exit, in days. Reference: 3 | | `deny_on_issues` | `boolean` | Deny on any simulation issue, such as an unpriced token | | `deny_on_stale` | `boolean` | Deny when Xerberus flags its data window as stale | | `max_data_age_seconds` | `number` | Oldest data window accepted | | `require_intent_party_in_wallets` | `boolean` | Deny unless the intent's sender or target is one of the simulated wallets | | `allow_stale_when_risk_reducing` | `boolean` | Let pure removals through on stale or old data | ## Rego Checks | Deny reason | Condition | What it catches | | --- | --- | --- | | `oracle_error` / `malformed_oracle_output` | Oracle error, or a field the rules read is missing | Failures that must not read as "no issues" | | `stale_data` | `deny_on_stale` and `is_stale`, unless exempt | Decisions made on stale data | | `data_too_old` | `data_age_seconds > max_data_age_seconds`, unless exempt | Data older than you accept | | `window_not_honored` | A window was pinned and Xerberus answered from another | Results that can't be lined up with the other Xerberus packs | | `concentration_limit` | Largest post-trade share over the limit **and** grown | A trade that concentrates the book | | `hhi_delta_limit` | `hhi_delta > max_hhi_delta` | A jump in overall token concentration | | `liquidity_30d_deterioration` | `ladder_30d_delta_pp < min_ladder_30d_delta_pp` | A trade that makes the book harder to exit within a month | | `slowest_exit_limit` | `slowest_exit_delta_days > max_slowest_exit_delta_days` | A trade that lengthens the worst-case exit | | `simulation_issues` | `deny_on_issues` and `issues_count > 0` | A simulation Xerberus couldn't fully run | | `wallets_not_bound_to_intent` | `require_intent_party_in_wallets` and neither sender nor target is a simulated wallet | A simulation of one portfolio authorizing a trade on another | ### Concentration Only a change that pushes the largest position past the limit **and** grows its share denies, so a trade that trims an already-concentrated book isn't blocked by the level it's reducing. ```rego deny contains "concentration_limit" if { v.post_largest_token_pct > t.max_post_token_share_pct v.post_largest_token_pct > v.pre_largest_token_pct } ``` ### Risk-Reducing Exemption With `allow_stale_when_risk_reducing` on, a pure removal is exempt from the staleness rules, so unavailable data never stops an agent reducing exposure. The delta checks still apply, and the exemption never covers additions. ```rego stale_exempt if { t.allow_stale_when_risk_reducing v.is_risk_reducing == true } deny contains "stale_data" if { t.deny_on_stale v.is_stale == true not stale_exempt } ``` ### Final Allow Rule ```rego allow if { not v.error well_formed count(deny) == 0 } ``` ## Complete Policy ```rego package xerberus_portfolio_what_if import future.keywords default allow := false t := data.params.xerberus_what_if v := data.wasm.xerberus_what_if well_formed if { is_array(v.wallets) is_boolean(v.is_risk_reducing) is_number(v.post_largest_token_pct) is_number(v.pre_largest_token_pct) is_number(v.hhi_delta) is_number(v.ladder_30d_delta_pp) is_number(v.slowest_exit_delta_days) is_number(v.issues_count) is_boolean(v.is_stale) is_number(v.data_age_seconds) } stale_exempt if { t.allow_stale_when_risk_reducing v.is_risk_reducing == true } wallets_bound_to_intent if { some w in v.wallets w == lower(input.from) } wallets_bound_to_intent if { some w in v.wallets w == lower(input.to) } deny contains "oracle_error" if v.error deny contains "malformed_oracle_output" if { not v.error not well_formed } deny contains "stale_data" if { t.deny_on_stale v.is_stale == true not stale_exempt } deny contains "data_too_old" if { v.data_age_seconds > t.max_data_age_seconds not stale_exempt } deny contains "window_not_honored" if v.window_honored == false deny contains "concentration_limit" if { v.post_largest_token_pct > t.max_post_token_share_pct v.post_largest_token_pct > v.pre_largest_token_pct } deny contains "hhi_delta_limit" if v.hhi_delta > t.max_hhi_delta deny contains "liquidity_30d_deterioration" if { is_number(v.ladder_30d_delta_pp) v.ladder_30d_delta_pp < t.min_ladder_30d_delta_pp } deny contains "slowest_exit_limit" if v.slowest_exit_delta_days > t.max_slowest_exit_delta_days deny contains "simulation_issues" if { t.deny_on_issues v.issues_count > 0 } deny contains "wallets_not_bound_to_intent" if { t.require_intent_party_in_wallets is_array(v.wallets) not wallets_bound_to_intent } allow if { not v.error well_formed count(deny) == 0 } ``` Composing this pack into a VaultKit composite changes only where params live: the manifest envelope puts the curator's slice at `data.params.params.xerberus_what_if`. See [Policies](/developers/vaults/sdk/policies). ## Notes * **The reference scenario denies.** Acquiring $1M of wstETH on a $17.8M treasury moves wstETH from 50.9% to 53.5% of the book, raises HHI by 0.0128, cuts the 30-day exitable share by 4.32 pp and lengthens the slowest exit by 5.26 days. That trips every portfolio limit at the reference thresholds, even though the standalone [exit quote](/developers/vaults/policies/xerberus-liquidity-exit) is under a week. * **The change is caller-supplied.** `add` and `remove` describe the trade; your intent builder is responsible for passing the real one. * **Pin the window when composing** with the other Xerberus packs, so all results come from one snapshot. * A token outside the reported top positions counts as a 0% share before the change. That can only make a trade look more concentrating, never less. # Xerberus Common Cause \[Use Xerberus common-cause analysis in Newton policies to deny false diversification across venues that share a failure mechanism.] This policy catches **false diversification**. Positions in Fluid, Morpho and Compound look independent, but they can depend on the same oracle, hold the same collateral, or lack the same safeguard, and a failure in that shared piece hits them all at once. Using [Xerberus](https://xerberus.io), it compares the venues a portfolio holds and denies when their shared failure mechanisms exceed your limits. ## Deployment | Field | Value | | --- | --- | | Pack id | `xerberus_common_cause` | | Rego package | `xerberus_common_cause` (the `--entrypoint` is `xerberus_common_cause.allow`) | | PolicyData addresses | [VaultKit address table](/developers/vaults/policy-packs#deployed-policydata-addresses) | | Canonical deployments | [`deployments.json`](https://github.com/newt-foundation/newton-policy-packs/blob/main/deployments.json) | :::note This oracle is not deployed yet, so it has no row in the VaultKit address table. [`deployments.json`](https://github.com/newt-foundation/newton-policy-packs/blob/main/deployments.json) is the source of truth for every pack, chain, and environment. ::: ### Secret | Secret | Required? | Where to get it | | --- | --- | --- | | `XERBERUS_API_KEY` | Required | [xerberus.io](https://xerberus.io) | Sent by the oracle as the `x-api-key` request header. ## Data Inputs One MCP `tools/call` of the `common_cause` tool against `https://mcp.xerberus.io/enterprise/mcp`, using the same transport as [Xerberus Liquidity Exit](/developers/vaults/policies/xerberus-liquidity-exit#data-inputs). Arrays are capped at 25 entries, largest first. Failure classes, dependency targets and collateral addresses are lowercased. | Field | What it means | Why the policy uses it | | --- | --- | --- | | `wallets` | Echoed from `wasmArgs`, lowercased | Gated by `require_intent_party_in_wallets` | | `subjects_compared_count` / `scored_subjects_count` | Venues compared, and how many Xerberus could score | `insufficient_venues` | | `total_exposure_usd` | Exposure summed across the compared venues | Denominator for the collateral share | | `unscored_subjects[]` | `{ subject, exposure_usd }` for venues Xerberus couldn't score | Gated by `deny_on_unscored` | | `shared_gaps[]` | `{ tag, failure_class, subject_count, combined_exposure_usd }`: a missing safeguard shared across venues | Gated by `max_shared_gap_exposure_usd` | | `largest_shared_gap_exposure_usd` | Largest combined exposure behind one shared gap | Reviewer context | | `shared_dependencies[]` | `{ target, connection_types, subject_count }`: a declared dependency shared across venues | Gated by `forbidden_shared_dependencies` | | `shared_collateral[]` | `{ token, symbol, subject_count, combined_exposure_usd }`: a collateral token shared across venues | Gated by `max_shared_collateral_pct_of_book` | | `largest_shared_collateral_exposure_usd` | Largest combined exposure behind one collateral token | Reviewer context | | `basis` | Xerberus's basis for the comparison | Reviewer context | | `requested_window` / `data_window` / `window_honored` / `is_stale` / `data_age_seconds` | Freshness, as in [Liquidity Exit](/developers/vaults/policies/xerberus-liquidity-exit#data-inputs) | Freshness rules | ## Per-call wasmArgs This pack ships **no `prepareQuery`**, so the curator supplies the wallets per call. | Field | Required? | Notes | | --- | --- | --- | | `wallets` | Required | 1–10 wallet addresses holding the positions to compare. At least two Xerberus-mapped venues are needed for a meaningful result | | `chain` | Optional | `ethereum` only today. Defaults to `ethereum` | | `window` | Optional | Xerberus data-window timestamp (ISO-8601). Pass the same value to every Xerberus pack | From the SDK: ```typescript await shield.morpho.submitCap(vault, marketParams, newCap, { wasmArgs: { xerberus_common_cause: { wallets: [vault], window }, }, }) ``` `wasmArgs` requires `@newton-xyz/vaultkit` 2.2.0 or later. See [Policies](/developers/vaults/sdk/policies#per-call-inputs). ## Policy Parameters | Param | Type | Description | | --- | --- | --- | | `max_shared_gap_exposure_usd` | `number` | Exposure ceiling behind one shared missing safeguard. The Xerberus reference design uses 5,000,000 | | `gap_failure_classes` | `string[]` | Failure classes the gap limit applies to (case-insensitive). Empty means every class | | `forbidden_shared_dependencies` | `string[]` | Dependency targets that must not be shared, such as `protocol:chainlink` (case-insensitive). Empty disables the check | | `min_venues_for_shared_dependency` | `number` | How many venues must share a forbidden dependency before it denies. Reference: 2 | | `max_shared_collateral_pct_of_book` | `number` | Ceiling on one collateral token's share of the compared exposure, in percent. Reference: 25 | | `material_subject_usd` | `number` | Exposure at which an unscored venue matters | | `deny_on_unscored` | `boolean` | Deny if a material venue is unscored | | `min_subjects_compared` | `number` | Venues needed for a meaningful comparison. Reference: 2 | | `deny_on_stale` | `boolean` | Deny when Xerberus flags its data window as stale | | `max_data_age_seconds` | `number` | Oldest data window accepted | | `require_intent_party_in_wallets` | `boolean` | Deny unless the intent's sender or target is one of the analysed wallets | ## Rego Checks | Deny reason | Condition | What it catches | | --- | --- | --- | | `oracle_error` / `malformed_oracle_output` | Oracle error, or a field the rules read is missing | Failures that must not read as "nothing shared" | | `stale_data` | `deny_on_stale` and `is_stale` | Decisions made on stale data | | `data_too_old` | `data_age_seconds > max_data_age_seconds` | Data older than you accept | | `window_not_honored` | A window was pinned and Xerberus answered from another | Results that can't be lined up with the other Xerberus packs | | `shared_gap_limit` | An in-scope shared gap carries more than `max_shared_gap_exposure_usd` | One missing safeguard exposing too much at once | | `shared_dependency_limit` | A forbidden target is shared by at least `min_venues_for_shared_dependency` venues | Several venues leaning on the same provider | | `shared_collateral_limit` | One shared collateral token exceeds `max_shared_collateral_pct_of_book` | Concentration hidden behind different venues | | `unscored_material_subject` | `deny_on_unscored` and an unscored venue holds at least `material_subject_usd` | Material exposure Xerberus couldn't assess | | `insufficient_venues` | Fewer than `min_subjects_compared` venues | A single-venue result reading as "no shared risk" | | `wallets_not_bound_to_intent` | `require_intent_party_in_wallets` and neither sender nor target is an analysed wallet | Analysis of one portfolio authorizing a trade on another | ### Shared Risks Each breach is a set, so a reviewer can see exactly which gap, dependency or collateral tripped. ```rego gap_breaches contains g if { some g in v.shared_gaps gap_in_scope(g) g.combined_exposure_usd > t.max_shared_gap_exposure_usd } dependency_breaches contains d if { some d in v.shared_dependencies d.target in forbidden_dependencies d.subject_count >= t.min_venues_for_shared_dependency } ``` ### Explainability A separate `shared_risks` rule renders each breach as text, for example `"forbidden dependency protocol:chainlink shared by 3 venues"`. This is **not** evaluated on-chain; the AVS entrypoint is `xerberus_common_cause.allow` and nothing else. It exists for `opa eval`, local simulation, and composites that want to surface a reason to an operator. ### Final Allow Rule `well_formed` requires every array the breach sets iterate over. An error envelope has none of them, every set comes out empty, and a bare `count(deny) == 0` would fail *open*. ```rego allow if { not v.error well_formed count(deny) == 0 } ``` ## Complete Policy ```rego package xerberus_common_cause import future.keywords default allow := false t := data.params.xerberus_common_cause v := data.wasm.xerberus_common_cause well_formed if { is_array(v.wallets) is_number(v.subjects_compared_count) is_number(v.total_exposure_usd) is_array(v.unscored_subjects) is_array(v.shared_gaps) is_array(v.shared_dependencies) is_array(v.shared_collateral) is_boolean(v.is_stale) is_number(v.data_age_seconds) } gap_classes := {lower(c) | some c in t.gap_failure_classes} forbidden_dependencies := {lower(d) | some d in t.forbidden_shared_dependencies} gap_in_scope(_) if count(gap_classes) == 0 gap_in_scope(g) if g.failure_class in gap_classes gap_breaches contains g if { some g in v.shared_gaps gap_in_scope(g) g.combined_exposure_usd > t.max_shared_gap_exposure_usd } dependency_breaches contains d if { some d in v.shared_dependencies d.target in forbidden_dependencies d.subject_count >= t.min_venues_for_shared_dependency } collateral_breaches contains c if { v.total_exposure_usd > 0 some c in v.shared_collateral c.combined_exposure_usd * 100 > t.max_shared_collateral_pct_of_book * v.total_exposure_usd } unscored_material contains s if { some s in v.unscored_subjects s.exposure_usd >= t.material_subject_usd } shared_risks contains detail if { some g in gap_breaches detail := sprintf("missing safeguard %v (%v) across %v venues, $%v combined", [g.tag, g.failure_class, g.subject_count, g.combined_exposure_usd]) } shared_risks contains detail if { some d in dependency_breaches detail := sprintf("forbidden dependency %v shared by %v venues", [d.target, d.subject_count]) } shared_risks contains detail if { some c in collateral_breaches detail := sprintf("collateral %v shared by %v venues, $%v combined", [c.symbol, c.subject_count, c.combined_exposure_usd]) } wallets_bound_to_intent if { some w in v.wallets w == lower(input.from) } wallets_bound_to_intent if { some w in v.wallets w == lower(input.to) } deny contains "oracle_error" if v.error deny contains "malformed_oracle_output" if { not v.error not well_formed } deny contains "stale_data" if { t.deny_on_stale v.is_stale == true } deny contains "data_too_old" if v.data_age_seconds > t.max_data_age_seconds deny contains "window_not_honored" if v.window_honored == false deny contains "shared_gap_limit" if count(gap_breaches) > 0 deny contains "shared_dependency_limit" if count(dependency_breaches) > 0 deny contains "shared_collateral_limit" if count(collateral_breaches) > 0 deny contains "unscored_material_subject" if { t.deny_on_unscored count(unscored_material) > 0 } deny contains "insufficient_venues" if v.subjects_compared_count < t.min_subjects_compared deny contains "wallets_not_bound_to_intent" if { t.require_intent_party_in_wallets is_array(v.wallets) not wallets_bound_to_intent } allow if { not v.error well_formed count(deny) == 0 } ``` Composing this pack into a VaultKit composite changes only where params live: the manifest envelope puts the curator's slice at `data.params.params.xerberus_common_cause`. See [Policies](/developers/vaults/sdk/policies). ## Notes * **New venues are invisible.** Xerberus only sees positions that already exist in the supplied wallets, so a transaction entering a brand-new venue can't be checked for shared risk until after it lands. Gate first-time venue entry separately, with a destination-protocol check or manual approval. * **Collateral share is measured against compared exposure**, not the whole book. That's stricter: in Xerberus's reference scenario, WBTC's $3.80M is about 21% of the $17.8M treasury but about 41% of the $9.31M across the compared venues. * **The reference scenario denies.** About $9.31M across Fluid, Morpho and Compound shares oracle-integrity gaps (price-deviation circuit breakers, graceful degradation, minimum-price protection), and all three declare a Chainlink dependency. * **Pin the window when composing** with the other Xerberus packs, so all results come from one snapshot. # Overview \[VaultKit 2.1 declares typed policies and routes vault-manager actions through Newton attestations.] Newton VaultKit is a TypeScript SDK and companion Solidity contract system for vault curators. It routes manager actions through a Newton Policy Protocol attestation gate before the underlying vault receives the call. Curators continue using the vault SDKs and contracts they already know. VaultKit adds a Shield clone between the curator and the vault, packages each action as an exact `Intent`, asks Newton operators to evaluate that intent against the configured policy, and forwards the action only when a valid attestation is available. ## What It Does VaultKit turns a vault action into this flow: ```mermaid sequenceDiagram participant App as Curator App participant SDK as Newton VaultKit participant AVS as Newton AVS participant Shield as Shield Contract participant Vendor as Vault Protocol App->>SDK: Call typed vault action SDK->>AVS: Submit signed intent and policy inputs AVS-->>SDK: Return allow/deny result and attestation data SDK->>Shield: Execute attested intent Shield->>Vendor: Forward approved manager call ``` ## Core Pieces * **Shield clones:** One audited implementation per chain, with deterministic per-curator clones deployed through `ShieldFactory`. * **Policy-first authoring:** `definePolicy().with(...)` combines published packs and custom `defineOracle(...)` outputs. * **Policy core:** `@newton-xyz/policy-core` owns oracle authoring, manifest encoding, schema generation, introspection, and provenance classification. * **Vendor modules:** Typed wrappers for MetaMorpho, Morpho Blue, Euler Earn, EVault, and Superform manager actions. * **Generic calls:** `shield.sendCall(...)` supports protocols and manager actions without a first-class vendor module. * **Typed errors:** SDK failures map to stable error classes such as `PolicyDeniedError`, `AttestationTimeoutError`, `ShieldExecutionError`, `ParamMismatchError`, `GatewayError`, and `UnsupportedChainError`. * **Verification:** `client.policy`, `client.verification`, and `client.reverify()` expose the resolved policy and attach-time checks. * **Negative assertions:** `assertIntentBlocked(...)` proves a noncompliant intent is rejected without mining a transaction. * **Browser-safe core:** The main package avoids `node:*` imports. Vendor modules may inherit constraints from the vendor SDKs they wrap. ## Manager Actions Only VaultKit is for privileged vault operations such as reallocations, cap changes, and other curator or manager calls. End-user deposits and withdrawals still flow through the vault protocol's normal UX unless the vault deliberately routes those actions through a Shield. ## How to Read This Section * Start with [Getting Started](/developers/vaults/sdk/integration-guide) to install packages, create clients, configure a Shield, and run a policy-gated action. * Use [Morpho](/developers/vaults/sdk/morpho) for MetaMorpho curator setup, composite policy choices, and `reallocate` role requirements. * Use [Morpho Blue](/developers/vaults/sdk/morpho-blue) for managed-sleeve position operations. * Use [Superform](/developers/vaults/sdk/superform) for SuperVault manager actions and typed hook bundles. * Use [Policies](/developers/vaults/sdk/policies) for published packs, custom oracles, bare policies, and verification. * Read [Concepts](/developers/vaults/sdk/concepts) if you want the mental model for `createShield`, packs, vendor modules, and `sendCall`. * Use [Examples](/developers/vaults/sdk/examples) for copyable integration patterns. * Keep [Reference](/developers/vaults/sdk/reference) nearby for parameters, return values, supported chains, and exported errors. Install VaultKit and run a policy-gated vault action. Gate MetaMorpho curator actions with a Newton Shield. Understand Shield clients, policy packs, vendor modules, and generic calls. Review the API surface, chains, errors, and package structure. # Integration Guide \[Install VaultKit 2.1, declare a Newton policy, and run a typed vault-manager action.] VaultKit's current authoring flow is policy-first: declare the policy modules synchronously with `definePolicy(...)`, pass the deployed `NewtonPolicy` address to `createShield(...)`, extend the returned client with a vendor module, and then call a typed action. ## Prerequisites | Requirement | Version or purpose | | --- | --- | | Node.js | 22 or newer | | pnpm | 10 or newer | | `viem` | `^2.35.1` | | `zod` | `^3.0.0` | | Newton API key | Created for the chain environment you will use | | Curator wallet | Funded on the target chain and authorized to manage the target vault | ## Install ```bash pnpm add @newton-xyz/vaultkit @newton-xyz/policy-core viem zod pnpm add @newton-xyz/policy-pack-vaultsfyi ``` Install only the vendor SDKs your integration needs. The typed MetaMorpho module uses: ```bash pnpm add @morpho-org/blue-sdk @morpho-org/blue-sdk-viem @morpho-org/morpho-ts ``` `@newton-xyz/policy-pack-shared` is not part of the VaultKit 2.x flow. Policy authoring and manifest utilities now live in `@newton-xyz/policy-core`. ## 1. Create Viem Clients ```typescript import { createPublicClient, createWalletClient, http } from 'viem' import { privateKeyToAccount } from 'viem/accounts' import { base } from 'viem/chains' const account = privateKeyToAccount( process.env.CURATOR_PRIVATE_KEY as `0x${string}`, ) const publicClient = createPublicClient({ chain: base, transport: http(process.env.RPC_URL), }) const walletClient = createWalletClient({ account, chain: base, transport: http(process.env.RPC_URL), }) ``` ## 2. Declare the Policy ```typescript import { definePolicy } from '@newton-xyz/vaultkit' import { vaultsfyi } from '@newton-xyz/policy-pack-vaultsfyi' const policy = definePolicy({ chainId: '8453', env: 'prod', }).with(vaultsfyi) ``` `definePolicy(...)` is synchronous and does not read the chain. Add more published packs or custom `defineOracle(...)` outputs with additional `.with(...)` calls. Module order does not need to match the deployed contract; VaultKit aligns the declared modules to `NewtonPolicy.getPolicyData()` when the client is created. ## 3. Create the Client and Add a Vendor Module ```typescript import { createShield } from '@newton-xyz/vaultkit' import { morphoActions } from '@newton-xyz/vaultkit/vendors/morpho' const client = (await createShield({ apiKey: process.env.NEWTON_API_KEY!, walletClient, publicClient, rpc: process.env.RPC_URL!, vault: '0xMetaMorphoVault', policy, policyAddress: '0xDeployedNewtonPolicy', })).extend(morphoActions) ``` The `policyAddress` must reference a deployed `NewtonPolicy` whose `getPolicyData()` set matches the modules declared with `.with(...)`. `createShield(...)` resolves that policy and exposes the result as `client.policy`. ## 4. Configure Parameters and Secrets ```typescript await client.setParams({ vaultsfyi: { risk_score_floor: 80, tvl_drawdown_24h_max_pct: 25, }, }) await client.uploadSecrets({ vaultsfyi: { VAULTS_FYI_API_KEY: process.env.VAULTSFYI_API_KEY!, }, }) ``` Parameters are encoded into the onchain composite manifest. Secrets are encrypted client-side and uploaded separately; they are not written onchain. Use `client.encodeParams(params)` to inspect the exact bytes before writing them. ## 5. Verify the Attachment ```typescript console.log(client.policy.address) console.log(client.policy.modules) console.log(client.verification) ``` When attaching to an existing deployment, VaultKit verifies progressively: 1. The attached contract points to the expected policy address. 2. The onchain oracle set and WASM CIDs match the declared modules. 3. Stored params decode against the module schemas. 4. When `expectedParams` is supplied, stored bytes exactly match the expected encoding. Call `client.reverify()` after changing parameters. Use `attachWithoutVerify: true` only to recover an incomplete first-time setup, and remove it immediately after writing the correct configuration. ## 6. Grant the Required Vault Role The client address must hold the manager or allocator role required by the vendor action. Role assignment is vendor-specific; VaultKit does not bypass the vendor's authorization model. ```typescript console.log(client.policyClientAddress) ``` Grant that address the appropriate role using the vendor's normal governance process. ## 7. Execute a Typed Action ```typescript const result = await client.morpho.reallocate( '0xMetaMorphoVault', [{ marketParams, assets: 1_000_000n }], { prepareQueryOptions: { vaultsfyi: { previousAllocationHash: '0x...', }, }, }, ) const receipt = await publicClient.waitForTransactionReceipt({ hash: result.transactionHash, }) ``` Each policy module owns its `wasmArgs` construction. Pass module-specific inputs through `prepareQueryOptions.`; VaultKit validates the resulting data before building the intent. ## 8. Handle Expected Failures ```typescript import { AttestationTimeoutError, PolicyDeniedError, RpcReadError, } from '@newton-xyz/vaultkit' try { await client.morpho.reallocate('0xMetaMorphoVault', allocations) } catch (error) { if (error instanceof PolicyDeniedError) { console.error('The policy denied this action:', error.reason) } else if (error instanceof AttestationTimeoutError) { console.error('Request a fresh task after backoff') } else if (error instanceof RpcReadError) { console.error(error.read, error.cause) } else { throw error } } ``` See [Errors](/developers/vaults/sdk/errors) before adding retries. A deterministic policy or schema failure does not become valid when retried. ## Next Steps Compose published packs, custom oracles, and policy-level parameters. Build a typed oracle with `defineOracle(...)`. Copy current VaultKit 2.1 recipes. # Policies \[Declare VaultKit policies from published packs, custom oracles, or a bare deployed policy.] Every VaultKit client resolves to one `NewtonPolicy`. That policy can contain one or more policy-data oracles, or it can be a bare monolithic policy with no composable modules. ## Published Policy Packs ```typescript import { definePolicy } from '@newton-xyz/vaultkit' import { chainalysis } from '@newton-xyz/policy-pack-chainalysis' import { vaultsfyi } from '@newton-xyz/policy-pack-vaultsfyi' const policy = definePolicy({ chainId: '8453', env: 'prod' }) .with(vaultsfyi) .with(chainalysis) ``` `definePolicy(...)` is synchronous and chainless. `createShield(...)` performs the onchain read, matches the declared modules to `NewtonPolicy.getPolicyData()`, and exposes the resolved result on `client.policy`. ```typescript const client = await createShield({ apiKey, walletClient, rpc, vault, policy, policyAddress: '0xDeployedNewtonPolicy', }) ``` A duplicate short ID is rejected because params and per-call inputs are namespaced by that ID. ## Custom Oracles Published packs and custom `defineOracle(...)` outputs implement the same `PolicyPack` contract: ```typescript const policy = definePolicy({ chainId: '8453', env: 'prod' }) .with(vaultsfyi) .with(myOracle) ``` There is no custom-module registry gate. The same deployment matching, WASM CID checks, schema validation, and manifest encoding apply to every module. ## Policy-Level Parameters Use `.withPolicyParams(...)` for composite-wide values that belong to the policy rather than a single oracle: ```typescript import { z } from 'zod' const policy = definePolicy({ chainId: '8453', env: 'prod' }) .with(vaultsfyi) .withPolicyParams( z.object({ allowedYieldSources: z.array(z.string()), maxFeeBps: z.number().int().min(0).max(10_000), }), ) await client.setParams({ _policy: { allowedYieldSources: ['0xYieldSource'], maxFeeBps: 100, }, vaultsfyi: { risk_score_floor: 80 }, }) ``` The manifest stores that slice at `params._policy`; Rego reads it at `data.params.params._policy`. ## Bare Policies Use `policyFromAddress(...)` for a deployed monolithic or calldata-only policy: ```typescript import { policyFromAddress } from '@newton-xyz/vaultkit' const policy = policyFromAddress({ address: '0xNewtonPolicy', chainId: '8453', env: 'prod', params: { encode: encodeMyParams, decode: decodeMyParams, }, }) ``` A bare policy has `modules: []`. Its optional codec enables `setParams(...)`, decoding, and higher verification rungs. Pass this object as `policy` and omit the separate `policyAddress`. ## Manifest Shape `setParams(...)` writes a versioned UTF-8 JSON envelope: ```json { "_manifest": { "magic": "NPM1", "version": 1 }, "modules": [ { "id": "vaultsfyi/risk-envelope/v1", "policyDataAddress": "0xPolicyData", "wasmCid": "bafy..." } ], "params": { "_policy": { "maxFeeBps": 100 }, "vaultsfyi": { "risk_score_floor": 80 } } } ``` Generate schemas and encode this format with `@newton-xyz/policy-core`; do not hand-maintain a second manifest encoder. ## Per-Call Inputs `sendCall` takes two short-ID-keyed per-module bags. Which one a pack uses depends on whether it ships a `prepareQuery`. ```typescript await client.sendCall({ to, data, functionSignature, // INPUTS to a pack's own prepareQuery, for packs that have one. prepareQueryOptions: { vaultsfyi: { previousAllocationHash: '0x...' }, my_oracle: { market: '0xMarket' }, }, // The wasmArgs THEMSELVES, for packs that ship no prepareQuery. wasmArgs: { arkham_entity: { address: '0xDestination', chain: 'ethereum' }, pharos_treasury: { stablecoin_id: 'usdc-circle' }, }, }) ``` Each module receives only its short-ID-keyed slice. Its `prepareQuery(...)` result is checked against its `wasmArgsSchema`; a failure stops intent construction. A pack ships no `prepareQuery` when its `wasmArgs` name something the SDK cannot read from chain state — [`balancer`](/developers/vaults/policies/balancer)'s `poolId`, [`arkham_entity`](/developers/vaults/policies/arkham-entity)'s `address`, [`pharos_treasury`](/developers/vaults/policies/pharos-treasury)'s `stablecoin_id`. Those modules take their slice from `wasmArgs` instead. The two are separate fields rather than one because they are different things: `prepareQueryOptions` is input **to** a `prepareQuery`, `wasmArgs` is what one would have **returned**. A pack with no `prepareQuery` also carries `TOptions = unknown`, so routing its args through the options bag would compile while checking nothing; `wasmArgs` is typed per module, so a misspelled field is a compile error. A module that ships a `prepareQuery` owns its slice outright — there is no merge, so a stray `wasmArgs` entry cannot override what the pack derived from chain state. Either way a slice its schema rejects, including the `{}` a `prepareQuery`-less module gets when nothing is supplied, throws `InvalidConfigurationError` rather than reaching the AVS. `wasmArgs` requires `@newton-xyz/vaultkit` 2.2.0 or later. ## Verification and Introspection The resolved client exposes objective onchain facts: ```typescript client.policy.address client.policy.modules client.verification await client.reverify() ``` For an independent report: ```typescript import { introspectComposite } from '@newton-xyz/policy-core' const report = await introspectComposite({ publicClient, shieldAddress: client.policyClientAddress, }) ``` `introspectComposite(...)` returns comparison booleans instead of throwing on a binding mismatch. It throws only when the stored manifest cannot be decoded. ## Provenance Is Consumer-Selected `ResolvedModule` reports objective values such as `id`, `policyDataAddress`, and `wasmCid`. It does not claim that a module is audited or trusted. Consumers can classify those facts with `classifyProvenance(...)` from `@newton-xyz/policy-core` and a registry they trust. ## Low-Level Composition VaultKit no longer re-exports `defineComposite(...)`. Most curator integrations should use `definePolicy().with(...)`. Import `defineComposite`, `defineOracle`, manifest encoders, schema generators, and introspection helpers directly from `@newton-xyz/policy-core` when building tooling outside `createShield(...)`. Define schemas, query preparation, deployments, and metadata for your own oracle. Browse published risk, compliance, and market-data packs. # Custom Oracles \[Author a typed policy-data oracle with `@newton-xyz/policy-core` and compose it into VaultKit.] `defineOracle(...)` gives custom and published oracles the same typed interface. It describes schemas, deployments, query preparation, and metadata; it does not build the oracle WASM or deploy `NewtonPolicyData`. ## Define the Oracle ```typescript import { defineOracle } from '@newton-xyz/policy-core' import { z } from 'zod' export const maxLtvOracle = defineOracle({ id: 'my_oracle/max-ltv/v1', paramsSchema: z.object({ max_ltv: z.number().int().min(0).max(10_000), }).strict(), wasmArgsSchema: z.object({ market: z.string(), }).strict(), secretsSchema: z.object({ MY_ORACLE_API_KEY: z.string().min(1), }).strict(), async prepareQuery( args, options?: { market?: string }, ) { return { wasmArgs: { market: options?.market ?? args.target, }, } }, deployments: { '8453': { prod: { policyData: '0xPolicyData', wasmCid: 'bafy...', policyCodeHash: '0x...', deployedAt: '2026-07-30', }, }, }, metadata: { name: 'Max LTV', version: '1.0.0', description: 'Checks current LTV against a configured ceiling.', }, }) ``` ## ID Rules IDs use `//`. The short ID becomes a Rego dot-path and must match `^[a-z][a-z0-9_]*$`. * `my_oracle/max-ltv/v1` is valid. * `my-oracle/max-ltv/v1` is invalid because the short ID contains a hyphen. ## Compose It ```typescript import { definePolicy } from '@newton-xyz/vaultkit' import { vaultsfyi } from '@newton-xyz/policy-pack-vaultsfyi' const policy = definePolicy({ chainId: '8453', env: 'prod' }) .with(vaultsfyi) .with(maxLtvOracle) ``` Custom and published modules receive identical module-set, deployment, WASM CID, and schema checks. ## Configure and Call ```typescript await client.setParams({ vaultsfyi: { risk_score_floor: 80 }, my_oracle: { max_ltv: 7_500 }, }) await client.uploadSecrets({ my_oracle: { MY_ORACLE_API_KEY: process.env.MY_ORACLE_API_KEY!, }, }) await client.sendCall({ to, data, functionSignature, prepareQueryOptions: { my_oracle: { market: '0xMarket' }, }, }) ``` ## Generate the Params Schema Use the schema derivation utilities from `@newton-xyz/policy-core` to generate the complete `{ _manifest, modules, params }` schema. The deployed policy validates the full manifest envelope, not only `params.my_oracle`. Commit the generated schema alongside the policy-authorship project and regenerate it when a module schema changes. ## Deployment Boundary The TypeScript definition is only the consumer-side contract. Taking an oracle live also requires: 1. Implementing and testing the WASM oracle. 2. Authoring fail-closed Rego. 3. Building and publishing the content-addressed artifacts. 4. Deploying `NewtonPolicyData` and a `NewtonPolicy` that references it. 5. Recording the real address and WASM CID in `deployments`. An empty deployment record is valid during offline authoring, but `createShield(...)` cannot resolve it for a live chain. ## Provenance Do not hard-code “audited” into the oracle object. Consumers can call `classifyProvenance(...)` with the module's objective facts and an audited-address registry they trust. # Examples \[VaultKit 2.1 recipes for policies, typed vendor calls, generic calls, verification, and blocked-intent assertions.] ## Declare a Published Policy ```typescript import { definePolicy } from '@newton-xyz/vaultkit' import { chainalysis } from '@newton-xyz/policy-pack-chainalysis' import { vaultsfyi } from '@newton-xyz/policy-pack-vaultsfyi' const policy = definePolicy({ chainId: '8453', env: 'prod', }) .with(vaultsfyi) .with(chainalysis) ``` ## Attach or Deploy Idempotently ```typescript import { createShield } from '@newton-xyz/vaultkit' const client = await createShield({ apiKey: process.env.NEWTON_API_KEY!, walletClient, publicClient, rpc: process.env.RPC_URL!, vault: '0xVault', policy, policyAddress: '0xNewtonPolicy', expectedParams: { vaultsfyi: { risk_score_floor: 80 }, chainalysis: { deny_on_sanctioned: true }, }, }) ``` An existing attachment must pass the verification ladder. A mismatch throws instead of silently changing the policy. ## Use a Bare Policy ```typescript import { createShield, policyFromAddress } from '@newton-xyz/vaultkit' const policy = policyFromAddress({ address: '0xNewtonPolicy', chainId: '8453', env: 'prod', params: { encode: (value) => encodeMyPolicyParams(value), decode: (value) => decodeMyPolicyParams(value), }, }) const client = await createShield({ apiKey, walletClient, rpc, vault, policy, }) ``` Use `policyFromAddress(...)` for a monolithic or calldata-only policy that does not use the composite module model. Do not also pass `policyAddress`; the address is already part of the bare policy. ## Add Policy-Level Parameters ```typescript import { z } from 'zod' const policy = definePolicy({ chainId: '8453', env: 'prod' }) .with(vaultsfyi) .withPolicyParams( z.object({ allowedYieldSources: z.array(z.string()), maxFeeBps: z.number().int().min(0).max(10_000), }), ) await client.setParams({ _policy: { allowedYieldSources: ['0xYieldSource'], maxFeeBps: 100, }, vaultsfyi: { risk_score_floor: 80, }, }) ``` The gate reads these composite-wide values under `data.params.params._policy`. ## Extend with a Typed Vendor Module ```typescript import { morphoActions } from '@newton-xyz/vaultkit/vendors/morpho' const morphoClient = client.extend(morphoActions) await morphoClient.morpho.reallocate( '0xMetaMorphoVault', [{ marketParams, assets: 1_000_000n }], { prepareQueryOptions: { chainalysis: { address: curatorAddress }, }, }, ) ``` ## Use the Generic Escape Hatch ```typescript await client.sendCall( { to: '0xTarget', data: encodedCalldata, functionSignature: 'someManagerAction(uint256)', prepareQueryOptions: { vaultsfyi: { previousAllocationHash: '0x...' }, }, }, 'DIRECT', 30_000, ) ``` With `sendCall(...)`, your integration owns the target, calldata, value, and human-readable function signature. Prefer a typed vendor module when one exists. ## Inspect and Reverify ```typescript console.log(client.policy.address) console.table(client.policy.modules) console.log(client.verification.achievedRung) await client.setParams(nextParams) const current = await client.reverify() ``` For independent manifest inspection, import `introspectComposite(...)` from `@newton-xyz/policy-core`. ## Prove a Denied Intent Is Blocked ```typescript import { NotApprovedDelegateError, PolicyNotDeniedError, } from '@newton-xyz/vaultkit' try { const proof = await client.assertIntentBlocked({ to: '0xTarget', data: noncompliantCalldata, functionSignature: 'someManagerAction(uint256)', }) console.log(proof.blocked, proof.taskId, proof.reason) } catch (error) { if (error instanceof PolicyNotDeniedError) { console.error('The supplied intent was allowed, so it proves no block') } else if (error instanceof NotApprovedDelegateError) { console.error('Use a wallet approved for this client') } else { throw error } } ``` `assertIntentBlocked(...)` evaluates the intent and uses `eth_call` to confirm that the denied attestation reverts specifically with `InvalidAttestation`. It does not mine a transaction and does not return a transaction hash. ## Use Multiple Chains Create one policy declaration and one client per chain. Never reuse a client while changing the wallet's active chain implicitly. ```typescript const basePolicy = definePolicy({ chainId: '8453', env: 'prod' }).with(vaultsfyi) const ethereumPolicy = definePolicy({ chainId: '1', env: 'prod' }).with(vaultsfyi) const baseClient = await createShield({ apiKey, walletClient: baseWalletClient, rpc: baseRpc, vault: baseVault, policy: basePolicy, policyAddress: basePolicyAddress, }) const ethereumClient = await createShield({ apiKey, walletClient: ethereumWalletClient, rpc: ethereumRpc, vault: ethereumVault, policy: ethereumPolicy, policyAddress: ethereumPolicyAddress, }) ``` # Reference \[API reference for `@newton-xyz/vaultkit` 2.1.] ## Installation ```bash pnpm add @newton-xyz/vaultkit @newton-xyz/policy-core viem zod ``` Node 22+ is required. `@newton-xyz/policy-core`, `viem`, and `zod` are peer dependencies. Morpho packages are optional peer dependencies and are only needed for the Morpho vendor modules. ```typescript import { VERSION } from '@newton-xyz/vaultkit' ``` `VERSION` matches the installed package version. ## Supported Chains | Chain | Chain ID | | --- | --- | | Ethereum | `1` | | Base | `8453` | | Sepolia | `11155111` | | Base Sepolia | `84532` | ## `definePolicy` ```typescript function definePolicy(config: { chainId: string env: GatewayEnv }): PolicyDraft ``` A `PolicyDraft` is immutable: ```typescript const policy = definePolicy({ chainId: '8453', env: 'prod' }) .with(vaultsfyi) .with(chainalysis) .withPolicyParams(policyParamsSchema) ``` * `.with(module)` appends a published pack or custom oracle. * `.withPolicyParams(schema)` declares the reserved `_policy` params object. * Chain reads are deferred until `createShield(...)`. ## `policyFromAddress` ```typescript function policyFromAddress(config: { address: Address chainId: string env: GatewayEnv params?: ParamCodec }): BarePolicy ``` Use this for a deployed monolithic or calldata-only policy. `ParamCodec` can supply `encode` and `decode` to enable params methods and higher verification rungs. ## `createShield` ```typescript function createShield(config: CreateShieldConfig): Promise ``` | Field | Type | Notes | | --- | --- | --- | | `apiKey` | `string` | Newton Gateway key for the selected environment. | | `walletClient` | `WalletClient` | Curator signer; its chain selects the deployment slot. | | `rpc` | `string` | RPC URL for reads and writes. | | `policy` | `PolicyDraft \| BarePolicy` | Policy declaration or bare deployed policy. | | `policyAddress?` | `Address` | Required for `PolicyDraft`; omit for `BarePolicy`. | | `vault` | `Address` | Managed target used in deterministic addressing. | | `publicClient?` | `PublicClient` | Reuse an existing client for reads. | | `env?` | `GatewayEnv` | Must agree with `policy.env` when provided. | | `gatewayUrl?` | `string` | Gateway override for testing. | | `version?` | `bigint` | Addressing version; defaults to `0n`. | | `policyClientAddress?` | `Address` | Attach to a known address instead of predicting. | | `expectedParams?` | `Record` | Require canonical byte equality at attach time. | | `attachWithoutVerify?` | `boolean` | Explicit recovery-only escape below verification rung 1. | | `mustDeploy?` | `boolean` | Fail if an existing deployment would be attached. | | `bypassDelaySeconds?` | `bigint` | Deployment configuration; default seven days, minimum one day. | | `attestationExpiration?` | `bigint` | Per-call expiration override. | | `wsRpcUrl?` | `string` | WebSocket RPC for subscription-based paths. | ## `ShieldClient` | Member | Purpose | | --- | --- | | `policy` | Resolved policy address, chain, environment, params codec, and modules. | | `verification` | Attach-time `VerificationResult`. | | `policyClientAddress` | Onchain client address. | | `setParams(params)` | Encode and store policy configuration. | | `encodeParams(params)` | Return the exact encoded bytes without writing. | | `uploadSecrets(secrets)` | Validate, encrypt, and upload per-module secrets. | | `sendCall(args, mode?, timeoutMs?)` | Evaluate and execute a generic manager action. | | `assertIntentBlocked(args, timeoutMs?)` | Confirm that a denied intent is rejected, without mining a transaction. | | `prepareIntent(args)` | Build and sign an intent without submitting it. | | `reverify()` | Refresh the verification result against current state. | | `extend(vendorActions)` | Add a typed vendor namespace. | ## `SendCallArgs` ```typescript interface SendCallArgs { to: Address data: Hex value?: bigint functionSignature: string prepareQueryOptions?: Record } ``` ```typescript type SubmissionMode = 'ATTESTATION' | 'DIRECT' interface SendCallResult { transactionHash: Hex taskId: Hex evaluationResult: boolean } ``` ## `assertIntentBlocked` ```typescript interface PolicyBlockedResult { blocked: true taskId: Hex reason: string } ``` The method expects a denial, simulates the denied direct execution with `eth_call`, and resolves only if the revert is `InvalidAttestation`. It can throw: * `PolicyNotDeniedError` when the supplied action was allowed. * `NotApprovedDelegateError` when the wallet is not authorized to call the client. * `PolicyDeniedError` when the gateway denial cannot be submitted as the negative assertion requires. * The decoded error for any unrelated revert. ## Verification ```typescript interface VerificationResult { achievedRung: 0 | 1 | 2 | 3 | 4 addressChecked: boolean oracleSetChecked: boolean paramsChecked: boolean byteEqualChecked: boolean skippedReason?: 'attachWithoutVerify' | 'freshDeploy' } ``` A bare policy's empty oracle set satisfies the structural check vacuously. Check `client.policy.modules.length > 0` before presenting “oracle set verified” in a UI. ## Gateway Client ```typescript function createGatewayClient(config: GatewayClientConfig): GatewayClient function resolveGatewayUrl( chainId: SupportedChainId, env: GatewayEnv, ): string ``` Construct a gateway client directly only for lower-level calls such as task creation, policy simulation, or encrypted-secret storage. ## Deployment Utilities ```typescript const SHIELD_DEPLOYMENTS: ShieldDeploymentRegistry const SUPPORTED_CHAINS: readonly SupportedChainId[] const GATEWAY_API_URLS: Record function predictShieldAddress(args: PredictShieldAddressArgs): Address function resolveShieldDeployment( chainId: number, env?: GatewayEnv, registry?: ShieldDeploymentRegistry, ): ShieldDeployment function isSupportedChainId(value: unknown): value is SupportedChainId function isGatewayEnv(value: unknown): value is GatewayEnv ``` ## Vendor Subpaths ```typescript import { morphoActions } from '@newton-xyz/vaultkit/vendors/morpho' import { morphoBlueActions } from '@newton-xyz/vaultkit/vendors/morpho-blue' import { eulerActions } from '@newton-xyz/vaultkit/vendors/euler' import { eulerVaultActions } from '@newton-xyz/vaultkit/vendors/euler-vault' import { superformActions } from '@newton-xyz/vaultkit/vendors/superform' ``` Each extension adds its own namespace. `extend(...)` throws `ExtendCollisionError` if an extension would overwrite an existing member. ## Policy Core Import these directly from `@newton-xyz/policy-core`: * `defineOracle` * `defineComposite` * `introspectComposite` * `classifyProvenance` * Manifest encoders and decoders * Composite params-schema derivation VaultKit re-exports common policy types and `classifyProvenance`, but authoring and inspection utilities such as `defineOracle` and `defineComposite` come directly from `policy-core`. ## Error Guard ```typescript function isShieldError(error: unknown): error is ShieldError ``` All SDK errors extend `ShieldError`. `NewtonShieldError` remains a deprecated compatibility alias. ## Related * [Policies](/developers/vaults/sdk/policies) * [Custom Oracles](/developers/vaults/sdk/custom-oracles) * [Errors](/developers/vaults/sdk/errors) # Errors \[VaultKit 2.1 error classes, stable codes, diagnostics, and retry guidance.] Every SDK error extends `ShieldError` and carries a stable `code`. `NewtonShieldError` is retained as a deprecated alias. ```typescript import { isShieldError, ShieldError } from '@newton-xyz/vaultkit' try { await operation() } catch (error) { if (isShieldError(error)) { console.error(error.code, error.message, error.cause) } } ``` ## Error Classes | Class | Code | Retry guidance | | --- | --- | --- | | `PolicyDeniedError` | `policy_denied` | Do not retry unchanged input. Rebuild against current state or accept the denial. | | `PolicyNotDeniedError` | `policy_not_denied` | `assertIntentBlocked(...)` received an allowed intent; supply a genuinely noncompliant intent. | | `NotApprovedDelegateError` | `not_approved_delegate` | Authorize the wallet or use an approved wallet. | | `NewtonTaskEvaluationError` | `newton_task_evaluation_failed` | Inspect `operator_errors[]`; retry only transient operator failures. | | `AttestationTimeoutError` | `attestation_timeout` | Retry with a fresh task after backoff. | | `GatewayError` | `gateway_error` | Transport umbrella; inspect the subtype and status. | | `GatewayHttpError` | `gateway_http_error` | Retry 5xx; fix authentication or request errors for 4xx. | | `GatewayRpcError` | `gateway_rpc_error` | Depends on the JSON-RPC code. | | `GatewayTimeoutError` | `gateway_timeout` | Retry with backoff. | | `RpcReadError` | `rpc_read_error` | Retry against a healthier RPC endpoint. This class is emitted only for reads proven to have failed for a transport reason. | | `PolicyMismatchError` | `policy_mismatch` | Reconcile the expected policy, module set, CIDs, or params. | | `ParamMismatchError` | `param_mismatch` | Reconcile the expected and stored params. | | `InvalidConfigurationError` | `invalid_configuration` | Fix the caller configuration. | | `ExistingCloneVersionError` | `existing_clone_version` | Choose the intended existing version or a genuinely new version. | | `ShieldDeploymentNotFoundError` | `shield_deployment_not_found` | The chain is supported but that environment has no recorded deployment. | | `UnsupportedChainError` | `unsupported_chain` | Select a supported chain. | | `ConcurrentIntentError` | `concurrent_intent` | Wait for the in-flight call; serialize calls per client. | | `ExtendCollisionError` | `extend_collision` | Remove or rename the colliding extension. | | `IntentConfigurationError` | `intent_configuration` | Fix malformed intent inputs. | | `IntentMismatchError` | `intent_mismatch` | Rebuild and re-evaluate; never submit mismatched task data. | | `TransactionFailedError` | `transaction_failed` | Inspect `cause`; retry only if the underlying failure is transient. | | `ShieldExecutionError` | `shield_execution` | Inspect the decoded revert before deciding. | ## RPC Reads: Transport vs Terminal VaultKit classifies chain-read failures before exposing them. Proven network, timeout, socket, HTTP, or transient JSON-RPC failures become `RpcReadError`. Contract reverts, zero data, decode failures, and configuration mismatches remain terminal errors such as `InvalidConfigurationError` or `PolicyMismatchError`. ```typescript try { await createShield(config) } catch (error) { if (error instanceof RpcReadError) { console.error(`Transient read failed: ${error.read}`) // Retry against a healthy endpoint with backoff. } } ``` A chainless `publicClient` is probed with `getChainId()`. A terminal chain mismatch is not safe to retry. ## Failed Evaluation Diagnostics The top-level task summary can say that quorum was not reached even when operators returned deterministic policy failures. Inspect the raw gateway result's `operator_errors[]` before retrying: ```json { "error_code": -32002, "message": "Policy evaluation failed: params schema validation failed", "operator_address": "0x...", "retryable": false, "timestamp": "2026-07-01T00:00:00Z" } ``` `retryable: false` usually indicates invalid params, missing secrets, schema mismatch, or deterministic oracle evaluation. Repeating the same request will not repair it. ## Blocked-Intent Errors `assertIntentBlocked(...)` distinguishes three important outcomes: * A confirmed `InvalidAttestation` simulation returns `{ blocked: true, taskId, reason }`. * An allowed action throws `PolicyNotDeniedError`. * An unauthorized wallet throws `NotApprovedDelegateError` before the policy-block assertion, so an authorization failure is never mislabeled as policy enforcement. For raw simulation tooling, `decodeShieldRevertName(error)` returns the decoded custom-error name when available. ## Retry Pattern ```typescript import { AttestationTimeoutError, GatewayTimeoutError, RpcReadError, } from '@newton-xyz/vaultkit' async function retryable(operation: () => Promise, attempts = 3) { let lastError: unknown for (let attempt = 0; attempt < attempts; attempt++) { try { return await operation() } catch (error) { const canRetry = error instanceof AttestationTimeoutError || error instanceof GatewayTimeoutError || error instanceof RpcReadError if (!canRetry) throw error lastError = error await new Promise((resolve) => setTimeout(resolve, 1_000 * 2 ** attempt), ) } } throw lastError } ``` Always log `code`, `message`, and `cause`. For decoded execution failures, also log the error name and arguments. # Morpho Guide \[How to gate MetaMorpho curator actions with Newton VaultKit and composite policy packs.] Morpho vault curators can route MetaMorpho manager actions through a Newton Shield before the vault receives the call. VaultKit's Morpho overlay builds the MetaMorpho calldata with Morpho's SDK, packages that exact call as an intent, asks Newton operators to evaluate the configured policy, and forwards the call only when the policy returns an approval attestation. This guide focuses on `reallocate`, a curator-only MetaMorpho action. End-user deposits and withdrawals continue through the normal Morpho vault flow. ## What This Demonstrates * **Exact calldata binding:** the attestation is bound to the exact `reallocate` calldata your app encodes. * **Composite policy checks:** one Shield can enforce a Vaults.fyi-only policy or a Vaults.fyi + Chainalysis policy in a single evaluation. * **Onchain policy parameters:** `setParams(...)` writes the curator's thresholds into the Shield-bound policy manifest. * **Manager-action gating:** the Shield clone must hold a MetaMorpho role that can call `reallocate`. ## Target Network The public end-to-end Morpho examples use **Base Sepolia** (`84532`) with Newton `prod`, also called testnet production. Base Sepolia is used because the MetaMorpho V1.1 factory is deployed there for testnet curator flows. Newton environments are separate stacks. Create the key from the Newton dashboard for the same environment your Shield uses. The public Base Sepolia example uses `prod`. ## Install Packages Every VaultKit Morpho integration needs VaultKit, policy core, Viem, the policy packs you plan to enforce, and Morpho's SDK: ```bash pnpm add @newton-xyz/vaultkit @newton-xyz/policy-core viem zod pnpm add @newton-xyz/policy-pack-vaultsfyi pnpm add @morpho-org/blue-sdk @morpho-org/blue-sdk-viem @morpho-org/morpho-ts ``` For a two-module policy that also screens an address, add Chainalysis: ```bash pnpm add @newton-xyz/policy-pack-chainalysis ``` The Morpho packages are optional peer dependencies of VaultKit. Curators who use the generic `sendCall(...)` escape hatch can encode calldata themselves, but the typed path below uses `shield.morpho.reallocate(...)`. ## Declare the Policy Use `definePolicy().with(...)` with the modules deployed in your Newton policy. A Vaults.fyi-only policy looks like this: ```typescript import { definePolicy } from '@newton-xyz/vaultkit' import { vaultsfyi } from '@newton-xyz/policy-pack-vaultsfyi' const policy = definePolicy({ chainId: '84532', env: 'prod', }).with(vaultsfyi) ``` For Vaults.fyi plus Chainalysis, install the Chainalysis pack and include both modules: ```typescript import { chainalysis } from '@newton-xyz/policy-pack-chainalysis' import { definePolicy } from '@newton-xyz/vaultkit' import { vaultsfyi } from '@newton-xyz/policy-pack-vaultsfyi' const policy = definePolicy({ chainId: '84532', env: 'prod', }) .with(vaultsfyi) .with(chainalysis) ``` The module list must match the policy's onchain `getPolicyData()` exactly. If the Shield is configured with a policy address that references a different module set, the SDK raises a typed configuration error before executing a vault action. ## Create or Attach to a Shield Create one client for the curator, vault, chain, policy, and version slot: ```typescript import { createShield } from '@newton-xyz/vaultkit' import { morphoActions } from '@newton-xyz/vaultkit/vendors/morpho' const shield = (await createShield({ apiKey: process.env.NEWTON_API_KEY!, walletClient, rpc: process.env.BASE_SEPOLIA_RPC_URL!, publicClient, policy, policyAddress: '0xCompositePolicyAddress', vault: '0xMetaMorphoVaultAddress', bypassDelaySeconds: 7n * 24n * 60n * 60n, })).extend(morphoActions) console.log(shield.policyClientAddress) ``` `createShield(...)` is idempotent. If a previous run did not finish writing policy params, use `attachWithoutVerify: true` only for a recovery run that immediately calls `setParams(...)`, then remove the flag and call `reverify()`. ## Configure Params and Secrets Write policy thresholds onchain and upload oracle secrets through Newton Gateway: ```typescript await shield.setParams({ vaultsfyi: { apy_z_max: 3, risk_score_floor: 80, tvl_drawdown_24h_max_pct: 25, tvl_drawdown_7d_max_pct: 40, deny_on_allocation_change: true, deny_on_critical_flag: true, deny_on_corrupted: true, }, }) await shield.uploadSecrets({ vaultsfyi: { VAULTS_FYI_API_KEY: process.env.VAULTSFYI_API_KEY!, }, }) ``` For a Vaults.fyi + Chainalysis composite, include the Chainalysis params and secrets required by that pack. The OFAC sanctions endpoint is public, but the policy schema may still require a non-empty secret value; use the key or placeholder required by your deployed policy. ## Grant the Shield a Morpho Role The Shield has two separate authorization gates: * Shield's delegate gate controls who can call `execute` or `executeDirect`. * MetaMorpho's manager roles control what the forwarded call can do on the vault. The initial Shield owner is approved as a delegate at clone initialization. After an ownership handoff or when using a separate hot key, approve the executor explicitly: ```typescript await walletClient.writeContract({ address: shield.policyClientAddress, abi: shieldAbi, functionName: 'setApprovedDelegate', args: [account.address, true], }) ``` MetaMorpho protects `reallocate` with its allocator-role check. The Shield clone must be one of: * the vault curator, * the vault owner, or * an approved allocator. Grant the role from the vault owner before running the gated action. Most curator integrations use `setCurator(shield.policyClientAddress)` or `setIsAllocator(shield.policyClientAddress, true)`. If the Shield can configure policy params but `reallocate` reverts, check this role first. Newton can approve the intent, but MetaMorpho will still reject the forwarded call if the Shield clone is not authorized on the vault. ## Execute `reallocate` With the Morpho overlay attached, call the typed action: ```typescript const allocations = [ { marketParams: marketParamsA, assets: 1_000_000n }, { marketParams: marketParamsB, assets: 0n }, ] const result = await shield.morpho.reallocate( '0xMetaMorphoVaultAddress', allocations, { prepareQueryOptions: { vaultsfyi: { previousAllocationHash: '0x...' }, }, }, ) ``` `reallocate` takes positional arguments: `(vault, allocations, options?)`. Each allocation uses Morpho's `marketParams` shape plus `assets`. For a Vaults.fyi + Chainalysis composite, include the per-call screening input your deployed pack expects under `prepareQueryOptions.chainalysis`. Omit it for a Vaults.fyi-only policy. The returned result includes the onchain transaction hash and Newton task context. A successful run with `allow: true` means the full path worked: exact calldata, policy evaluation, attestation, and Shield execution. Use `shield.sendCall(...)` only as an escape hatch for vendors or actions that VaultKit does not wrap yet. With `sendCall`, your integration owns calldata integrity. ## Read-Only Versus Full Reproduce You can validate most of a Morpho integration before granting a vault role: | Path | What it proves | Requirements | | --- | --- | --- | | Read-only inspection | Policy source, params, and deployed Shield configuration are coherent. | Public RPC and deployed addresses. | | Type-check and encode | Your app compiles against the published package surface and produces MetaMorpho calldata. | Node, package install, and Morpho SDK. | | Setup stages | `definePolicy`, `createShield`, `setParams`, and `uploadSecrets` work. | Funded curator key and Newton/API secrets. | | Full execution | MetaMorpho accepts the Shield-forwarded `reallocate`. | Shield clone has curator, owner, or allocator permissions. | ## Vaults.fyi Testnet Data Vaults.fyi indexes production networks, not testnets. When testing a Shield on Base Sepolia, a Vaults.fyi policy can fail closed because there is no testnet vault data for the lookup target. For demos, point the Vaults.fyi lookup at a real mainnet vault through the policy pack's data-source override fields while the Shield still executes on Base Sepolia. In production, remove the override so the policy describes the same vault the Shield gates. ## Troubleshooting | Symptom | Likely cause | Fix | | --- | --- | --- | | `401` from Newton Gateway | API key was minted for a different Newton environment. | Create a key for the same stack as `env`, usually `prod` for Base Sepolia testnet production. | | `UnsupportedChainError` | Wallet client is not on a VaultKit-supported chain. | Use a supported chain such as Base Sepolia (`84532`) for testnet Morpho flows. | | `ShieldDeploymentNotFoundError` | No Shield factory is registered for the selected chain and environment. | Check `env`, chain id, and RPC configuration. | | Module-set mismatch or composite policy mismatch | The modules declared with `definePolicy().with(...)` do not match the policy address. | Use a policy deployed with exactly the selected modules. | | `PolicyMismatchError` | An existing Shield clone is bound to different policy data. | Verify the clone, write the expected params in a recovery run, or use a new `version`. | | MetaMorpho role revert | The Shield clone is not curator, owner, or allocator. | Grant the clone a qualifying vault role, then rerun the action. | | `PolicyDeniedError` | Newton evaluated the policy and denied the intent. | Inspect the error cause, adjust params, or choose inputs that satisfy the policy. | | Vaults.fyi oracle errors or unexpected denials | Missing Vaults.fyi secret or no data for the lookup target. | Upload the API key and use a production-network data-source override for testnet demos. | | `AttestationTimeoutError` or `GatewayError` | Gateway was slow or unavailable. | Retry with backoff and a fresh task. | Install VaultKit, configure a Shield, and execute a policy-gated vault action. Review typed errors and retry guidance. # Morpho Blue \[Use VaultKit's typed Morpho Blue position module for managed lending sleeves.] `vendors/morpho-blue` wraps position operations on the Morpho Blue singleton. It is distinct from `vendors/morpho`, which wraps MetaMorpho curator and allocator governance. ## Install and Extend ```bash pnpm add @newton-xyz/vaultkit @morpho-org/blue-sdk @morpho-org/blue-sdk-viem ``` ```typescript import { morphoBlueActions } from '@newton-xyz/vaultkit/vendors/morpho-blue' const client = (await createShield(config)).extend(morphoBlueActions) ``` ## Supply Collateral ```typescript await client.morphoBlue.supplyCollateral( morphoBlueSingleton, { marketParams, assets: 1_000_000n, onBehalf: managedVault, }, { prepareQueryOptions: { venue_allowlist: { venue: morphoBlueSingleton }, }, }, ) ``` The first argument is the singleton address. The market is identified by `marketParams` inside calldata. ## Wrapped Operations | Method | Purpose | | --- | --- | | `supplyCollateral` | Post collateral for a managed position. | | `borrow` | Borrow assets or shares against collateral. | | `supply` | Supply loan-token liquidity. | | `repay` | Repay assets or shares. | | `withdraw` | Withdraw supplied loan-token liquidity. | | `withdrawCollateral` | Withdraw posted collateral. | Methods that accept an amount require exactly one of: ```typescript { assets: 1_000_000n } { shares: 1_000_000n } ``` Both-zero and both-specified inputs are rejected before submission. ## Safety Constraints * Callback data is always `0x`; the client contract is not a Morpho callback receiver. * Position owners and receivers cannot be the zero address, singleton, or an address copied from `marketParams`. * The module does not prove that `onBehalf` is the managed vault or that `receiver` is approved. Bind those constraints in the policy. * `marketParams.lltv` uses 1e18 WAD scale. Euler EVault LTV values use a different 1e4 scale. Use this module only for positions operated as part of a managed vault or sleeve. It is not intended to interpose on direct end-user Morpho positions. # Euler Guide \[How to gate Euler Vault Kit EVault governor actions with Newton VaultKit.] Euler Vault Kit EVaults can route governor actions through a Newton Shield before the EVault receives the call. VaultKit's EVault overlay encodes the governor calldata, asks Newton operators to evaluate the configured policy, and forwards the call only when the policy returns an approval attestation. This guide covers `shield.eulerVault.*`, which wraps EVault governor functions. For Euler Earn aggregation-vault curator and allocator actions such as `reallocate`, use the [Euler Earn Guide](/developers/vaults/sdk/euler). ## Install Packages ```bash pnpm add @newton-xyz/vaultkit @newton-xyz/policy-core viem zod pnpm add @newton-xyz/policy-pack-vaultsfyi ``` Add any other policy-pack modules your Shield enforces, such as RedStone or Chainalysis: ```bash pnpm add @newton-xyz/policy-pack-redstone @newton-xyz/policy-pack-chainalysis ``` ## Attach the EVault Overlay Create or attach to a Shield, then extend the client with `eulerVaultActions`: ```typescript import { createShield, definePolicy } from '@newton-xyz/vaultkit' import { eulerVaultActions } from '@newton-xyz/vaultkit/vendors/euler-vault' import { vaultsfyi } from '@newton-xyz/policy-pack-vaultsfyi' const policy = definePolicy({ chainId: '8453', env: 'prod', }).with(vaultsfyi) const shield = (await createShield({ apiKey: process.env.NEWTON_API_KEY!, walletClient, rpc: process.env.RPC_URL!, publicClient, policy, policyAddress: '0xCompositePolicyAddress', vault: '0xEVault', })).extend(eulerVaultActions) ``` `shield.eulerVault.*` produces calldata from EVault's governor ABI and routes the call through `shield.sendCall(...)`. The composite policy pack still owns `wasmArgs`; pass per-call policy inputs through `prepareQueryOptions`. ## Grant the Shield EVault Governance EVault governor actions are manager actions. Borrowers and suppliers continue through normal EVault flows and do not route through the Shield. Every wrapped action is callable by the EVault governor, also called `governorAdmin`. To use the Shield as the policy gate, the current governor should set the EVault governor to the Shield clone or route the relevant governance authority through the clone: ```typescript await walletClient.writeContract({ address: '0xEVault', abi: eVaultAbi, functionName: 'setGovernorAdmin', args: [shield.policyClientAddress], }) ``` After the Shield clone is the governor, `shield.eulerVault.setGovernorAdmin(...)` can rotate governance again through the same policy-gated path. Unlike Euler Earn, EVault governor setters are not a two-step onchain timelock flow. A policy approval plus the EVault governor check is the control surface. `setLTV` includes a `rampDuration` argument for lowered liquidation LTVs, but that is a parameter ramp, not a governance timelock. ## Configure LTV ```typescript await shield.eulerVault.setLTV( '0xEVault', { collateral: '0xCollateralEVault', borrowLtv: 8000, liquidationLtv: 8500, rampDuration: 0, }, { prepareQueryOptions: { vaultsfyi: { previousAllocationHash: '0x...' }, }, }, ) ``` `borrowLtv` and `liquidationLtv` use 1e4 scale, so `8000` means 80%. `liquidationLtv` must be greater than or equal to `borrowLtv`. ## Configure Caps Pass caps as asset-denominated amounts in the vault asset's smallest unit, or `"unlimited"`. VaultKit encodes them to EVault's packed `AmountCap` format before building calldata. ```typescript await shield.eulerVault.setCaps('0xEVault', { supplyCap: 10_000_000_000n, borrowCap: 5_000_000_000n, }) await shield.eulerVault.setCaps('0xEVault', { supplyCap: 'unlimited', borrowCap: 2_500_000_000n, }) ``` Do not pre-encode the packed `uint16` cap yourself when using `shield.eulerVault.setCaps(...)`; the helper expects human asset amounts and rounds up to the nearest representable EVault cap. ## Wrapped Actions | Category | Helpers | | --- | --- | | Governor administration | `setGovernorAdmin` | | Risk parameters | `setLTV`, `setMaxLiquidationDiscount`, `setLiquidationCoolOffTime`, `setInterestRateModel`, `setCaps` | | Vault configuration | `setHookConfig`, `setConfigFlags` | | Fees | `setInterestFee`, `setFeeReceiver` | VaultKit intentionally does not wrap end-user `deposit`, `mint`, `withdraw`, `redeem`, `borrow`, or `repay` actions. ## Other Examples ```typescript await shield.eulerVault.setInterestRateModel('0xEVault', '0xInterestRateModel') await shield.eulerVault.setMaxLiquidationDiscount('0xEVault', 1200) await shield.eulerVault.setHookConfig('0xEVault', { hookTarget: '0xHookContract', hookedOps: 0b101, }) await shield.eulerVault.setInterestFee('0xEVault', 1000) await shield.eulerVault.setFeeReceiver('0xEVault', '0xFeeReceiver') ``` Scale fields follow the EVault ABI. LTVs, liquidation discounts, and interest fees use 1e4 scale. ## Troubleshooting | Symptom | Likely cause | Fix | | --- | --- | --- | | EVault governor revert | The Shield clone is not the EVault governor for that action. | Grant or route `governorAdmin` to the Shield clone before executing. | | Wrong caps onchain | Raw packed caps were passed instead of asset-denominated amounts. | Pass asset amounts or `"unlimited"` to `setCaps`; VaultKit handles packing. | | `PolicyDeniedError` | Newton evaluated the configured policy and denied the intent. | Inspect the policy result, params, and per-call `prepareQueryOptions`. | | Unexpected calldata | The wrong overlay or vault address was used. | Use `shield.eulerVault.*` only for EVaults; use `shield.euler.*` for Euler Earn vaults. | Gate Euler Earn curator and allocator actions with `shield.euler.*`. Review the generic `sendCall` API and expected error types. # Euler Earn Guide \[How to gate Euler Earn manager and allocator actions with Newton VaultKit.] Euler Earn vaults can route curator and allocator actions through a Newton Shield before the Euler Earn vault receives the call. VaultKit's Euler Earn overlay encodes the Euler calldata, packages that exact call as an intent, asks Newton operators to evaluate the configured policy, and forwards the call only when the policy returns an approval attestation. This guide covers `shield.euler.*`, which wraps Euler Earn. For Euler Vault Kit base lending vault governor actions such as `setLTV`, `setCaps`, and `setInterestRateModel`, use the [Euler Guide](/developers/vaults/sdk/euler-vault). ## Install Packages ```bash pnpm add @newton-xyz/vaultkit @newton-xyz/policy-core viem zod pnpm add @newton-xyz/policy-pack-vaultsfyi ``` Add any other policy-pack modules your Shield enforces, such as Chainalysis: ```bash pnpm add @newton-xyz/policy-pack-chainalysis ``` ## Attach the Euler Earn Overlay Create or attach to a Shield, then extend the client with `eulerActions`: ```typescript import { createShield, definePolicy } from '@newton-xyz/vaultkit' import { eulerActions } from '@newton-xyz/vaultkit/vendors/euler' import { vaultsfyi } from '@newton-xyz/policy-pack-vaultsfyi' const policy = definePolicy({ chainId: '8453', env: 'prod', }).with(vaultsfyi) const shield = (await createShield({ apiKey: process.env.NEWTON_API_KEY!, walletClient, rpc: process.env.RPC_URL!, publicClient, policy, policyAddress: '0xCompositePolicyAddress', vault: '0xEulerEarnVault', })).extend(eulerActions) ``` `shield.euler.*` produces calldata from Euler Earn's ABI and routes the call through `shield.sendCall(...)`. The composite policy pack still owns `wasmArgs`; pass per-call policy inputs through `prepareQueryOptions`. ## Grant the Shield an Euler Earn Role Euler Earn manager actions are not end-user deposit or withdrawal flows. The Shield clone must hold a role that can call the forwarded function on the Euler Earn vault: | Role | Typical actions | | --- | --- | | Owner | `setCurator`, `setIsAllocator`, `setFeeRecipient`, `setFee`, `submitTimelock`, `submitGuardian`, `setName`, `setSymbol` | | Curator | `submitCap`, `acceptCap`, `revokePendingCap`, `submitMarketRemoval`, `revokePendingMarketRemoval` | | Allocator | `reallocate`, `setSupplyQueue`, `updateWithdrawQueue` | | Guardian | `revokePendingTimelock`, `revokePendingGuardian`, and other veto-style pending changes | A Shield clone holding the curator role can also call allocator actions. Most integrations grant the clone the curator role or mark it as an allocator before executing policy-gated actions. ## Reallocate Use the typed reallocation helper for the allocator hot path: ```typescript const allocations = [ { id: '0xMarketA', assets: 1_000_000n }, { id: '0xMarketB', assets: 0n }, ] const result = await shield.euler.reallocate( '0xEulerEarnVault', allocations, { prepareQueryOptions: { vaultsfyi: { previousAllocationHash: '0x...' }, }, }, ) ``` `reallocate` takes positional arguments: `(vault, allocations, options?)`. Each allocation uses Euler Earn's `{ id, assets }` shape, where `id` is the market address and `assets` is the amount in the vault asset's smallest unit. ## Manage Queues and Caps Allocator queue updates: ```typescript await shield.euler.setSupplyQueue('0xEulerEarnVault', [ '0xMarketA', '0xMarketB', ]) await shield.euler.updateWithdrawQueue('0xEulerEarnVault', [1n, 0n]) ``` Curator cap changes use Euler Earn's timelocked two-step flow: ```typescript await shield.euler.submitCap('0xEulerEarnVault', '0xMarketA', 5_000_000n) // After the Euler Earn timelock matures: await shield.euler.acceptCap('0xEulerEarnVault', '0xMarketA') ``` Pending cap and market-removal changes can be revoked through the matching typed helpers. ## Wrapped Actions | Category | Helpers | | --- | --- | | Role setup | `setCurator`, `setIsAllocator` | | Allocator operations | `reallocate`, `setSupplyQueue`, `updateWithdrawQueue` | | Market caps | `submitCap`, `acceptCap`, `revokePendingCap` | | Market removal | `submitMarketRemoval`, `revokePendingMarketRemoval` | | Timelock | `submitTimelock`, `acceptTimelock`, `revokePendingTimelock` | | Guardian | `submitGuardian`, `acceptGuardian`, `revokePendingGuardian` | | Fees and metadata | `setFee`, `setFeeRecipient`, `setName`, `setSymbol` | VaultKit intentionally does not wrap end-user ERC-4626 `deposit`, `mint`, `withdraw`, or `redeem` actions. Ownership transfer helpers are also omitted; use direct governance operations or the raw `shield.sendCall(...)` escape hatch only when your policy explicitly models that action. ## Troubleshooting | Symptom | Likely cause | Fix | | --- | --- | --- | | Euler Earn role revert | The Shield clone is not owner, curator, allocator, or guardian for that action. | Grant the clone the correct Euler Earn role, then rerun the action. | | `PolicyDeniedError` | Newton evaluated the configured policy and denied the intent. | Inspect the policy result, params, and per-call `prepareQueryOptions`. | | Module-set mismatch | The modules declared with `definePolicy().with(...)` do not match the deployed policy. | Use the module set from the policy's onchain `getPolicyData()`. | | Unexpected calldata | The wrong overlay or vault address was used. | Use `shield.euler.*` only for Euler Earn vaults; use `shield.eulerVault.*` for EVault governor actions. | Gate Euler Vault Kit EVault governor actions with `shield.eulerVault.*`. Review the generic `sendCall` API and expected error types. # Superform \[Gate Superform SuperVault strategy-manager actions and build typed hook bundles with VaultKit.] The Superform module targets the `SuperVaultStrategy` manager surface. It does not wrap depositor `deposit`, `mint`, `withdraw`, `redeem`, or request flows. ## Extend the Client ```typescript import { superformActions } from '@newton-xyz/vaultkit/vendors/superform' const client = (await createShield(config)).extend(superformActions) ``` Superform does not publish a calldata-builder SDK for this surface, so VaultKit maintains typed encoders and checks their selectors against the strategy ABI. ## Typed Hook Execution ```typescript await client.superform.executeHooks( strategy, [ { kind: 'approveDeposit', hook: approveAndDeposit4626Hook, yieldSourceOracleId: '0x...', yieldSource: sleeveVault, token: sleeveAsset, amount: 1_000_000n, usePrevHookAmount: false, expectedAssetsOrSharesOut: 990_000n, globalProof, strategyProof, }, ], 0n, { prepareQueryOptions: { vaultsfyi: { vault: sleeveVault }, }, }, ) ``` The `approveDeposit` variant builds the allowance and ERC-4626 deposit fields together. Use it when the strategy does not already have a standing allowance. Typed hook kinds are: * `swap` * `deposit` * `approveDeposit` * `redeem4626` `executeHooksRaw(...)` accepts prebuilt bytes for hooks not yet modeled. That path gives the caller responsibility for inner-calldata integrity. ## Other Manager Actions | Method | Purpose | | --- | --- | | `fulfillRedeemRequests` | Settle queued redeem requests as a manager. | | `fulfillCancelRedeemRequests` | Settle cancelled redeems. | | `skimPerformanceFee` | Skim accrued performance fees. | | `manageYieldSource` | Add, update, or remove a yield source. | | `proposeVaultFeeConfigUpdate` | Propose performance and management fees. | | `executeVaultFeeConfigUpdate` | Apply the pending fee configuration. | | `managePPSExpiration` | Propose, execute, or cancel a PPS-expiration update. | Some operations require the strategy's primary-manager role rather than a secondary-manager role. Confirm the target strategy's authorization before building the intent. ## Hook Authorization Superform validates hook bundles with its own hook registry and Merkle proofs. Newton policy evaluation is an additional gate; it does not replace Superform's authorization. Policies that constrain the hook target, token, venue, amount, or receiver must inspect the decoded intent and fail closed. A data oracle alone cannot prove those exact calldata fields because oracle query inputs are caller-supplied. ## Pure Hook Utilities The subpath also exports builders and decoders: ```typescript import { buildApproveAndDepositHookData, buildSwapHookData, decodeHook, makeHookRegistry, } from '@newton-xyz/vaultkit/vendors/superform' ``` Use these for authoring, dashboards, and inspection. Runtime enforcement remains in the deployed policy. # VaultKit Legal Terms \[VaultKit supplement to the Magic Developer Terms of Service.] Effective Date: June 23, 2026 This VaultKit Supplement (this "Supplement") supplements the Magic Developer Terms of Service (the "Developer Terms") and, as applicable, the Magic Terms of Service (together, the "Existing Terms"). It governs your access to and use of VaultKit by Magic Labs, Inc. ("Magic"). Capitalized terms not defined here have the meanings given in the Existing Terms. In the event of a conflict between this Supplement and the Existing Terms with respect to VaultKit, this Supplement controls. ## 1. Definitions * "Data Provider" means a third-party data service whose outputs inform Policy evaluation. * "Newton Protocol" means the decentralized policy-based transaction authorization protocol. * "Policy / Policy Pack" means the policy rules and parameters you configure through VaultKit. * "Secrets" means API credentials or keys uploaded by you via VaultKit for use by operators during Policy evaluation. * "Shield Contract" means a smart contract that requires a valid attestation before executing a Vault Action. * "Vault Action" means a vault management operation executed through a Shield Contract. * "VaultKit" means the software tools and documentation provided by Magic under this Supplement. VaultKit does not include the Gateway. ## 2. Newton Protocol Newton Protocol is stewarded by the Magic Newton Foundation and its subsidiares. Magic is not responsible for the availability, performance, or accuracy of Newton Protocol, the Gateway (except as stated in Section 6), the operator network, or the production of attestations. Your use of Newton Protocol is governed solely by the Magic Newton Foundation's applicable terms. ## 3. Shield Contracts Shield Contracts are deployed by you and are owned and controlled by you once deployed. Magic has no ability to access, modify, pause, or interact with a deployed Shield Contract. You are solely responsible for Shield Contract configuration, security, and operation, including selection and configuration of Policy Packs and all associated parameters. Magic does not audit, certify, or guarantee the correctness or security of Shield Contract code or Policy Pack logic. ## 4. Policy Evaluation; No Compliance Guarantee A policy evaluation is not a legal compliance determination. You retain all independent compliance obligations and may not satisfy those obligations by relying solely on VaultKit policy evaluation. VaultKit does not replace your obligation to maintain appropriate compliance programs. Magic makes no warranty that any Policy or Policy Pack will correctly identify sanctioned addresses, high-risk counterparties, unsafe vault conditions, or any other category of restricted activity. You are responsible for reviewing, testing, and validating all Policy configurations prior to and during production use. ## 5. Third-Party Data Providers Policy Packs may use outputs from Data Providers to inform policy decisions. Policy evaluation is only as accurate and timely as the Data Provider's API response at the time of evaluation. Magic does not control, verify, or guarantee the accuracy, completeness, timeliness, or availability of any Data Provider output. Magic is not responsible for losses or compliance failures arising from Data Provider unavailability or incorrect outputs. You are solely responsible for obtaining and maintaining any agreements required by applicable Data Providers, complying with their terms, and paying any associated fees. ## 6. Gateway Magic stores encrypted Secrets in the Gateway, providing limited access to authorized Newton Protocol operators for the purpose of Policy evaluation and to Magic personnel for Gateway maintenance. Magic's obligations do not extend to Secrets after retrieval by Newton Protocol operators. Magic may change or remove the Gateway's functionality at its discretion. ## 7. Vault Management; Curator Responsibility VaultKit is designed for use by vault curators who manage vault strategies on behalf of depositors. Magic is not a party to any arrangement between you and vault depositors or any third-party, does not owe any duty to vault depositors or any third party, and is not responsible for losses suffered by vault depositors or any third-party as a result of Vault Actions taken or not taken through VaultKit, including losses arising from policy denials, policy approvals, attestation timeouts, or misconfigured Policies. Nothing in VaultKit or its outputs constitutes investment advice or any recommendation regarding the management or allocation of vault assets. You are solely responsible for all investment and management decisions made in connection with vault operations. ## 8. Bypass Mechanism When you execute a Vault Action through the bypass mechanism, Policy controls, Data Provider checks, and all associated guardrails do not apply. You assume full responsibility for all consequences of any Vault Action executed through the bypass mechanism, including financial, legal, and regulatory exposure. Magic has no liability for losses arising from your use of the bypass. ## 9. Additional Disclaimers In addition to the disclaimers in the Existing Terms, and without limiting them, Magic disclaims all representations and warranties with respect to VaultKit, including that: (a) any Policy evaluation will correctly identify sanctioned persons, prohibited transactions, or violations of applicable law; (b) the Gateway will be available on an uninterrupted or error-free basis; (c) Newton Protocol or the operator network will be available at any particular time; (d) any Data Provider output is accurate, current, or complete; (e) any Shield Contract will execute correctly; or (f) use of VaultKit will satisfy any legal, regulatory, fiduciary, or contractual obligation you owe to any person, including vault depositors. VaultKit is provided "as is" to the maximum extent permitted by applicable law. ## 10. General This Supplement is incorporated into and forms part of the Developer Terms. All provisions of the Existing Terms that are not modified by this Supplement continue to apply to your use of VaultKit, including without limitation the limitation of liability, indemnification, termination, and governing law provisions of the Developer Terms. Magic reserves the right to modify this Supplement in accordance with Section 13.3 of the Developer Terms. Questions regarding this Supplement should be directed to legal@magic.link. # What is Newton? \[Newton Protocol is a decentralized policy engine for onchain transaction authorization, built as an EigenLayer AVS. Enforce spend limits, sanctions screening, fraud prevention, and compliance rules in smart contracts.] Newton Protocol is a [policy engine](/developers/overview/core-concepts) that lets you encode, verify, and enforce rules — spend limits, sanctions screening, fraud prevention — directly within your smart contracts. ### The problem Problem: smart contracts lack offchain context Smart contracts are **blind to offchain context**: whether a user is sanctioned, whether an AI agent is hallucinating, or whether a transaction violates a corporate spend policy. Traditional security relies on frontend filters or centralized API checks, leaving protocols vulnerable to unauthorized actions from third-party aggregators, autonomous agents, or direct contract calls. ### The solution Solution: Newton bridges offchain data to smart contracts Newton bridges this gap with real-time offchain data — KYC status, market feeds, proof of reserves — evaluated by a [decentralized operator network](/developers/concepts/consensus-security) and enforced at the smart contract level. Your protocol stays protected regardless of where the transaction originates. ### Modular and chain-agnostic Newton integrates with [most EVM-compatible networks](/developers/concepts/multichain) including Ethereum, Base, and Arbitrum. Non-EVM support is on the roadmap. See a live example of a Newton-powered sanctions-checked transfer app Get in touch to discuss your integration or partnership opportunity ## What it looks like ```ts twoslash import { createWalletClient, http } from 'viem' import { privateKeyToAccount } from 'viem/accounts' import { sepolia } from 'viem/chains' import { newtonWalletClientActions } from '@newton-xyz/sdk' // ---cut--- async function simulate() { const client = createWalletClient({ account: privateKeyToAccount('0x' + '1'.repeat(64) as `0x${string}`), chain: sepolia, transport: http(), }).extend(newtonWalletClientActions({ apiKey: 'test-api-key' })) const sender = '0x0000000000000000000000000000000000000000' as `0x${string}` const recipient = '0x0000000000000000000000000000000000000001' as `0x${string}` const calldata = '0x' as `0x${string}` const policyId = '0x' + '0'.repeat(64) as `0x${string}` const policyAddress = '0x1234567890123456789012345678901234567890' as `0x${string}` // Simulate a policy evaluation against a transaction intent const result = await client.simulateTask({ intent: { from: sender, to: recipient, value: '0x0', data: calldata, chainId: 11155111, functionSignature: '0xa9059cbb' }, policyTaskData: { policyId, policyAddress, policy: '0x', policyData: [] }, }) console.log(result.result?.allow) // true or false } ``` Run this example end-to-end in 5 minutes ## Walkthrough