Keyboard shortcuts

Press or to navigate between chapters

Press ? to show this help

Press Esc to hide this help

PolicyCage: On-Chain DeFi Constitution Enforcement [SPEC]

Crate: bardo-policy

Depends on: 00-defense.md (six-layer defense architecture), 01-custody.md (custody architecture)

Reader orientation: This document specifies the PolicyCage, an on-chain Solidity smart contract that enforces hard safety limits on what a Golem (mortal autonomous DeFi agent) can do with capital. It belongs to the Safety layer of Bardo (the Rust runtime for these agents). The key concept before diving in: the PolicyCage acts as a “DeFi Constitution” that the EVM enforces via revert, so even a fully compromised LLM cannot execute a transaction the cage rejects. Terms like Grimoire, Heartbeat, and CorticalState are defined inline on first use; a full glossary lives in prd2/11-compute/00-overview.md § Terminology.

The PolicyCage is the on-chain realization of the DeFi Constitution. It cannot be bypassed by prompt injection. The EVM enforces these constraints via revert. A fully compromised LLM still cannot execute a transaction the PolicyCage rejects.

The PolicyCage also serves as the machine-readable constitution for RLAIF-style critique-revision [LEE-2024]. Strategy proposals generated by the self-improvement loop are validated against PolicyCage constraints – any proposal that violates the cage is rejected without LLM evaluation. The LLM proposes, the PolicyCage constrains, and only compliant proposals reach execution. Before the safety extension can mint a Capability<T> token (see 00-defense.md §Capability-Gated Tools), the PolicyCage must validate the proposed action. No PolicyCage approval, no capability, no execution.


1. Design Principle

Safety-critical invariants must be enforced at layers the LLM cannot reach. The PolicyCage operates at Layer 3 (On-Chain Guards) in the six-layer security architecture – a smart contract that reverts on violation. It does not know an LLM exists. It does not parse natural language. It checks numeric bounds and address whitelists, and it reverts if they fail.

The prompt-level expression of the DeFi Constitution (Layer 3 defense-in-depth) is a supplement. The PolicyCage smart contract is the enforcement mechanism.


2. PolicyCage Configuration

#![allow(unused)]
fn main() {
use alloy::primitives::{Address, FixedBytes};
use serde::{Deserialize, Serialize};

/// On-chain PolicyCage configuration.
/// Validated at build time by `PolicyBuilder` — invalid configs
/// fail compilation, not runtime.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PolicyCageConfig {
    // Asset constraints
    pub approved_assets: Vec<Address>,
    pub approved_protocols: Vec<Address>,
    pub max_asset_count: u32, // Default: 10

    // Position constraints
    pub max_position_bps: u32,        // Default: 2500 (25%)
    pub max_concentration_bps: u32,   // Default: 3000 (30%)
    pub min_collateral_ratio_bps: u32, // Default: 12500 (125%)

    // Drawdown constraints
    pub max_drawdown_bps: u32, // Default: 1300 (13%)
    pub drawdown_window: u64,  // Default: 86400 (1 day, in seconds)

    // Rebalance constraints
    pub min_rebalance_interval: u64, // Default: 3600 (1 hour)
    pub max_rebalances_per_day: u32, // Default: 24

    // Strategy whitelist
    pub strategy_whitelist: Vec<FixedBytes<4>>, // Allowed function selectors
    pub allow_arbitrary_calldata: bool,          // Default: false

    // Sanctions
    pub sanction_oracle: Address,
}
}

3. Constitution Principles and Enforcement

PrincipleWhat It MeansPolicyCage Enforcement
Max concentrationNever >30% in one protocolmaxConcentrationBps check, revert ConcentrationExceeded()
Collateral ratioAlways >125% of liquidation thresholdminCollateralRatioBps enforced by lending adapter
Asset whitelistOnly approved tokens/protocolsapprovedAssets mapping, revert AssetNotApproved()
SanctionsNo sanctioned addressessanctionOracle on-chain check
ImmutabilityConstitution cannot be self-modifiedConstitution hash stored on-chain, verified at boot
Shutdown reserveReserved funds for graceful shutdownInfrastructure-enforced at Compute layer
InheritanceSpawned agents inherit ConstitutionSmart contract verifies hash matches parent
Default to inactionUncertain -> do nothingOptional time-delay provides cancellation opportunity

3.1 Constitution Parameters

#![allow(unused)]
fn main() {
use alloy::primitives::{Address, B256};

/// The constitution is the immutable core of the PolicyCage.
/// Its keccak256 hash is stored on-chain and verified at boot.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConstitutionConfig {
    pub max_concentration_bps: u32,       // Default: 3000 (30%)
    pub min_collateral_ratio_bps: u32,    // Default: 12500 (125%)
    pub approved_assets: Vec<Address>,
    pub sanction_oracle_address: Address,
    pub shutdown_reserve_usd: f64,         // Default: 0.30, immutable
    pub constitution_hash: B256,           // keccak256 of principles + parameters
}
}

At boot, the agent reads constitutionHash from the on-chain PolicyCage and compares it against its local copy. If the hashes diverge, the agent refuses to start. No negotiation.


4. PolicyCage Local Pre-Flight Buffer

The runtime maintains an in-memory circular buffer of recent transactions (capacity 1,000). The rolling 24-hour window is computed on each validateTrade() call by scanning buffer entries with timestamps within the window. Storage is not on-chain (too expensive for per-trade writes at L1 gas prices); instead, a local SQLite table policy_cage_txns persists the buffer:

CREATE TABLE policy_cage_txns (
    tx_hash TEXT PRIMARY KEY,
    asset TEXT NOT NULL,
    amount_usd REAL NOT NULL,
    timestamp INTEGER NOT NULL,
    golem_id TEXT NOT NULL
);

CREATE INDEX idx_cage_txns_timestamp ON policy_cage_txns(timestamp);
CREATE INDEX idx_cage_txns_golem ON policy_cage_txns(golem_id);

Pruned daily to 30-day retention. The on-chain dailySpent() view function provides the authoritative value; the local buffer is a pre-flight check to avoid submitting transactions that would revert.

Per-Transaction and Rolling Caps

Per-transaction spending caps. No single trade can exceed a configured USDC-equivalent value. A compromised Golem that attempts to drain its entire balance in one transaction hits the cap and reverts.

Rolling daily caps. A 24-hour sliding window limits total outflow. Even if the Golem makes many small transactions under the per-tx cap, the daily cap bounds aggregate exposure. The window is enforced on-chain using block timestamps, not the Golem’s internal clock (which it could manipulate).

Max position size. Maximum capital allocated to any single position, expressed in basis points of total portfolio value. Prevents concentration risk beyond PolicyCage maxConcentrationBps.

Position count limits. Maximum number of concurrent open positions. Prevents a Golem from spreading capital across hundreds of micro-positions that become unmanageable and expensive to unwind.

Protocol whitelist. The Golem can only interact with protocols the owner has approved. Extends the asset whitelist to protocol-level approval. A prompt injection that convinces the LLM to interact with an unapproved lending protocol fails at the contract level because the protocol address is not whitelisted.

High Water Mark and Drawdown

The drawdown circuit breaker uses a high water mark (HWM) tracked via oracle-reported values. HWM update mechanism: the owner or a designated operator submits a signed EIP-712 message containing the new HWM value. The Golem verifies the signature against the registered owner/operator address in the PolicyCage contract. Self-reported HWM is not accepted – the Golem cannot update its own HWM. The data source is an on-chain price feed (Chainlink aggregator or Uniswap V3 TWAP oracle). The owner’s signature authorizes the update, not the data. This separation means a compromised Golem cannot inflate its HWM to bypass the drawdown breaker, even if it has access to the oracle data.

Default drawdownBps: 2000 (20%). Range enforcement: the setDrawdownThreshold function reverts if bps < 500 or bps > 5000 (5%-50%). Below 5% triggers too frequently during normal volatility. Above 50% provides no meaningful protection.


5. PolicyCage Modification

PolicyCage parameters can only be modified by the owner via timelocked transactions:

Parameter CategoryRisk TierDelay
Add/remove approved assetsElevated1 hour
Adjust position limitsElevated1 hour
Modify drawdown thresholdHigh24 hours
Change strategy whitelistHigh24 hours
Disable sanctions oracleCritical48 hours

The agent itself cannot modify PolicyCage parameters. Only the owner’s key can sign these transactions. In Delegation mode, these are MetaMask Smart Account transactions. In Embedded mode, the owner authenticates through Privy. In LocalKey mode (dev only), the owner signs directly with a local key.


6. PolicyBuilder API

The bardo-policy crate provides a fluent builder that generates both custody-layer signing policies (Layer 3) and on-chain PolicyCage configs (Layer 7) from a single source. Build-time validation rejects invalid configurations before they reach the chain.

#![allow(unused)]
fn main() {
use bardo_policy::PolicyBuilder;

let policy = PolicyBuilder::new()
    .transfer_restriction(TransferRestriction::Clade) // Clade = an owner's fleet of sibling Golems
    .main_wallet(owner_wallet)
    .clade_wallets(&clade_wallet_addresses)
    .max_concentration_bps(3000)      // 30%
    .min_collateral_ratio_bps(12500)  // 125%
    .approved_assets(&[USDC, WETH, DAI])
    .max_drawdown_bps(1300)           // 13%
    .max_rebalances_per_day(24)
    .sanction_oracle(chainalysis_sanction_oracle)
    .build()?; // Returns Err if validation fails

// Two artifacts from one config:
let signing_policy = policy.to_custody_policy(); // Delegation caveats / Privy TEE / local key
let cage_config = policy.to_policy_cage_config(); // On-chain (Layer 7)
}

6.1 Build-Time Validation

The PolicyBuilder validates constraints at construction, not at runtime:

  • max_concentration_bps must be in range [500, 10000] (5%–100%)
  • min_collateral_ratio_bps must be >= 10500 (105%)
  • max_drawdown_bps must be in range [100, 5000] (1%–50%)
  • approved_assets must be non-empty
  • sanction_oracle must be a valid contract address (non-zero, checksummed)

Invalid configurations return a typed error with the failing constraint identified. This eliminates the class of bugs where a misconfigured PolicyCage deploys with overly permissive limits.


7. Spending Limits

Default spending limits enforced by PolicyCage:

ScopeDefaultConfigurable Range
Per-transaction$10,000$100 – $1,000,000
Per-session$50,000$1,000 – $10,000,000
Per-day$100,000$1,000 – $10,000,000

Spending limits are enforced at Layer 3 (TEE) and Layer 7 (on-chain). Even if the agent bypasses the TEE policy (via hardware attack), the on-chain guard still enforces the daily limit.



8. IPolicyCage Solidity Interface

The full on-chain interface. All view functions are gas-free for off-chain callers. Custom errors include diagnostic parameters for debugging failed transactions.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

/// @title IPolicyCage
/// @notice On-chain DeFi constitution enforcement.
///         Every write operation passes through these checks before execution.
///         The cage does not know an LLM exists — it checks numeric bounds
///         and address whitelists, and reverts if they fail.
interface IPolicyCage {
    // ── Asset constraints ──

    /// @notice Returns true if the asset is in the approved whitelist.
    function isApprovedAsset(address asset) external view returns (bool);

    /// @notice Returns true if the protocol address is approved for interaction.
    function isApprovedProtocol(address protocol) external view returns (bool);

    // ── Position constraints ──

    /// @notice Checks whether adding `newAmount` of `asset` would exceed
    ///         the maximum concentration limit relative to `totalPortfolio`.
    function checkConcentration(
        address asset,
        uint256 newAmount,
        uint256 totalPortfolio
    ) external view returns (bool);

    /// @notice Checks whether `borrowValue` against `collateralValue`
    ///         maintains the minimum collateral ratio.
    function checkCollateralRatio(
        uint256 borrowValue,
        uint256 collateralValue
    ) external view returns (bool);

    // ── Drawdown ──

    /// @notice Checks whether `currentNav` relative to `highWaterMark`
    ///         is within the maximum allowed drawdown.
    function checkDrawdown(
        uint256 currentNav,
        uint256 highWaterMark
    ) external view returns (bool);

    // ── Spending ──

    /// @notice Checks whether `amount` plus `dailySpent` stays within
    ///         the configured spending limit.
    function checkSpendingLimit(
        uint256 amount,
        uint256 dailySpent
    ) external view returns (bool);

    // ── Sanctions ──

    /// @notice Queries the sanctions oracle for `target`.
    ///         Returns true if the address is clear.
    function checkSanctions(address target) external view returns (bool);

    // ── Constitution ──

    /// @notice The keccak256 hash of the serialized constitution parameters.
    ///         Compared against the Golem's local copy at boot.
    function constitutionHash() external view returns (bytes32);

    // ── Custom errors ──

    error AssetNotApproved(address asset);
    error ProtocolNotApproved(address protocol);
    error ConcentrationExceeded(uint256 current, uint256 max);
    error CollateralRatioTooLow(uint256 current, uint256 min);
    error DrawdownExceeded(uint256 current, uint256 max);
    error SpendingLimitExceeded(uint256 amount, uint256 remaining);
    error SanctionedAddress(address target);
}

8.1 PolicyCage Events (Solidity)

The on-chain PolicyCage emits events for monitoring and audit:

event AssetWhitelisted(address indexed token, bool allowed);
event SpendingCapUpdated(uint256 perTx, uint256 perDay);
event DrawdownThresholdUpdated(uint256 bps);
event PositionLimitUpdated(uint256 maxPositions);
event CageViolation(
    address indexed golem,
    bytes32 violationType,
    uint256 attemptedValue,
    uint256 limit
);
event EmergencyHalt(address indexed triggeredBy, uint256 timestamp);

Owner-Only Configuration Functions

function whitelistAsset(address token, bool allowed) external;
function setSpendingCaps(uint256 perTxUsdc, uint256 perDayUsdc) external;
function setDrawdownThreshold(uint256 bps) external;
function setMaxPositions(uint256 max) external;
function emergencyHalt() external;
function resume() external;

The Golem cannot modify its own cage. PolicyCage parameters are updatable only by the owner’s address (an EOA or multisig, never the Golem’s session key). The Golem can read the cage. It can reason about the cage. It cannot change a single parameter.


Events Emitted

PolicyCage emits GolemEvent variants on constraint violations and configuration changes:

EventTriggerGolemEvent VariantPayload
policy:violationPolicyCage constraint violatedGolemEvent::PolicyViolation{ constraint_id, action, value, limit }
policy:cage_updatedPolicyCage parameters changedGolemEvent::PolicyCageUpdated{ field, old_value, new_value, authority }
policy:spending_limit_hitSpending limit reachedGolemEvent::PolicySpendingLimitHit{ partition, spent, limit, period }

Pi Hook Integration

Two hooks enforce pre- and post-execution policy checks.

HookExtensionBehavior
tool_callbardo-safetyPre-execution PolicyCage validation; mints Capability<T> on pass
tool_resultbardo-result-filterPost-execution result filtering

v1 Enforcement Scope

Constraint Enforcement Matrix

ConstraintOn-Chain (PolicyCage)Off-Chain (Middleware)Advisory Only
Approved asset listYes
Max position size (bps)Yes
Max concentration (bps)Yes
Max drawdownYes
Rebalance frequency limitYes
Sanctions screeningYes
Strategy whitelistYes
Gas price ceilingYes
Slippage warning thresholdYes

On-chain constraints revert the transaction if violated. Off-chain constraints block the tool call before submission. Advisory constraints log a warning but do not block.

Sanctions Oracle

v1 uses the Chainalysis OFAC screening API for sanctions compliance. Every counterparty address is checked before any transfer or swap execution.

Fallback behavior: If the sanctions oracle is unavailable (timeout, 5xx response), the transaction is blocked. This is a fail-closed design — no transactions proceed without a successful sanctions check. The tool returns error code SANCTIONS_ORACLE_UNAVAILABLE with a message instructing the owner to retry.

Constitution Hash Verification

The constitution is the serialized PolicyCage constraint set. Its keccak256 hash is computed at deploy time and stored in the PolicyCage contract. On Golem boot, the local config’s constitution hash is compared against the on-chain hash. If they diverge, the Golem refuses to start and emits a CONSTITUTION_MISMATCH error.

This prevents a scenario where the on-chain policy was updated (by the owner) but the Golem is running with stale local constraints.

Violation Behavior

maxConcentrationBps violation: The transaction reverts. No automatic unwinding occurs. The Golem’s heartbeat will detect the failed execution in the REFLECT phase and can retry with adjusted parameters on the next tick.

maxPositionSizeBps violation: Same as concentration — transaction reverts, no automatic action.

maxDrawdown violation (off-chain): The middleware blocks all write operations for the affected vault. Read operations continue. The Golem enters a degraded mode where it can only observe and report. Recovery requires the owner to acknowledge the drawdown event and explicitly re-enable writes.


Spending Limit Enforcement Order

PolicyCage and session key limits operate in series. Both must approve a transaction. The effective limit is min(policy_cage_limit, session_key_limit). PolicyCage is the hard block (cannot be overridden by the Golem). Session key limits are set by the user and enforced on-chain.


PolicyCage vs. CorticalState: Hard Blocks and Soft Signals

PolicyCage = hard block (transaction rejected, no override). CorticalState (the Golem’s 32-signal atomic shared perception surface) flags = soft signals (Golem is warned, can still proceed if user confirms). PolicyCage evaluates first; if it rejects, CorticalState is never consulted.