PolicyCage: On-Chain DeFi Constitution Enforcement [SPEC]
Crate:
bardo-policyDepends 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 inprd2/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
| Principle | What It Means | PolicyCage Enforcement |
|---|---|---|
| Max concentration | Never >30% in one protocol | maxConcentrationBps check, revert ConcentrationExceeded() |
| Collateral ratio | Always >125% of liquidation threshold | minCollateralRatioBps enforced by lending adapter |
| Asset whitelist | Only approved tokens/protocols | approvedAssets mapping, revert AssetNotApproved() |
| Sanctions | No sanctioned addresses | sanctionOracle on-chain check |
| Immutability | Constitution cannot be self-modified | Constitution hash stored on-chain, verified at boot |
| Shutdown reserve | Reserved funds for graceful shutdown | Infrastructure-enforced at Compute layer |
| Inheritance | Spawned agents inherit Constitution | Smart contract verifies hash matches parent |
| Default to inaction | Uncertain -> do nothing | Optional 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 Category | Risk Tier | Delay |
|---|---|---|
| Add/remove approved assets | Elevated | 1 hour |
| Adjust position limits | Elevated | 1 hour |
| Modify drawdown threshold | High | 24 hours |
| Change strategy whitelist | High | 24 hours |
| Disable sanctions oracle | Critical | 48 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_bpsmust be in range [500, 10000] (5%–100%)min_collateral_ratio_bpsmust be >= 10500 (105%)max_drawdown_bpsmust be in range [100, 5000] (1%–50%)approved_assetsmust be non-emptysanction_oraclemust 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:
| Scope | Default | Configurable 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:
| Event | Trigger | GolemEvent Variant | Payload |
|---|---|---|---|
policy:violation | PolicyCage constraint violated | GolemEvent::PolicyViolation | { constraint_id, action, value, limit } |
policy:cage_updated | PolicyCage parameters changed | GolemEvent::PolicyCageUpdated | { field, old_value, new_value, authority } |
policy:spending_limit_hit | Spending limit reached | GolemEvent::PolicySpendingLimitHit | { partition, spent, limit, period } |
Pi Hook Integration
Two hooks enforce pre- and post-execution policy checks.
| Hook | Extension | Behavior |
|---|---|---|
tool_call | bardo-safety | Pre-execution PolicyCage validation; mints Capability<T> on pass |
tool_result | bardo-result-filter | Post-execution result filtering |
v1 Enforcement Scope
Constraint Enforcement Matrix
| Constraint | On-Chain (PolicyCage) | Off-Chain (Middleware) | Advisory Only |
|---|---|---|---|
| Approved asset list | Yes | — | — |
| Max position size (bps) | Yes | — | — |
| Max concentration (bps) | Yes | — | — |
| Max drawdown | — | Yes | — |
| Rebalance frequency limit | — | Yes | — |
| Sanctions screening | — | Yes | — |
| Strategy whitelist | Yes | — | — |
| Gas price ceiling | — | — | Yes |
| Slippage warning threshold | — | — | Yes |
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.