Policy Engine
Advanced policy enforcement for blockchain transactions and wallet security
The Osiris Policy Engine is a powerful and flexible TypeScript library for defining and enforcing policies on blockchain transactions, with primary focus on EVM-compatible chains and Solana. It provides fine-grained rules to allow or deny operations based on their properties and arguments.
Core Concepts
Policies
A policy consists of two sets of rules:
allow
rules: Define conditions under which an action is permitteddeny
rules: Define conditions under which an action is explicitly forbidden
An action is ultimately allowed if:
- It does not match any
deny
rules - It matches at least one
allow
rule (ifallow
rules are defined)
If no allow
rules are defined, any action not denied is implicitly allowed.
Actions
An action represents an operation that needs to be validated against a policy:
interface ActionData {
method: "signMessage" | "signTypedData" | "signTransaction" | "signRawPayloads";
chain?: string; // e.g., "evm:1" for Ethereum mainnet, "solana:mainnet"
contractAddress?: string; // For EVM transactions
programId?: string; // For Solana transactions
walletAddress?: string; // Address performing the action
payload?: any; // Action-specific data
abi?: ABISpec; // ABI for EVM transaction decoding
idl?: IDLSpec; // IDL for Solana transaction decoding
organizationId: string; // Identifier for the organization
metadata?: {
description?: string;
verifiedOrder?: any;
riskLevel?: "low" | "medium" | "high";
estimatedGas?: string;
estimatedValue?: string;
};
}
Raw Policy Definition
You can define policies directly as JavaScript objects. This is the core way to create policies:
Policy Constraints
General Constraints (JSON Schema)
enum
: Value must be one of the specified values in the arraypattern
: String value must match the provided regular expressionminLength
,maxLength
: For strings and arraysminimum
,maximum
: For numbersproperties
,required
: For objectsallOf
,anyOf
,oneOf
,not
: Logical combinators
Custom Constraints (Blockchain-Specific)
For BigInts (represented as strings)
{
bigIntMin: "100000000000000000", // Minimum value (0.1 ETH)
bigIntMax: "1000000000000000000", // Maximum value (1 ETH)
bigIntEq: "500000000000000000" // Exact value (0.5 ETH)
}
For Addresses (case-insensitive for EVM)
{
addressEq: "0x1234567890123456789012345678901234567890",
addressInList: [
"0x1234567890123456789012345678901234567890",
"0x0987654321098765432109876543210987654321"
]
}
For Strings
{
stringContains: "Osiris",
stringStartsWith: "Login to",
stringEndsWith: "approved"
}
Example Policies
Here are examples of common policy patterns:
ERC20 Token Policy
const erc20Policy = {
allow: [
// Allow transfers up to 100 tokens to trusted addresses
{
method: "signTransaction",
chain: "evm:eip155:1",
decoded: {
functionName: "transfer",
args: {
lengthEq: 2,
"0": { addressInList: ["0xTrustedRecipient1", "0xTrustedRecipient2"] },
"1": { bigIntMax: "100000000000000000000" } // 100 tokens (18 decimals)
}
}
},
// Allow approvals for specific DEX contracts only
{
method: "signTransaction",
decoded: {
functionName: "approve",
args: {
lengthEq: 2,
"0": { addressEq: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D" }, // Uniswap V2
"1": { bigIntMax: "1000000000000000000000" } // 1000 tokens max
}
}
}
],
deny: [
// Deny unlimited approvals
{
method: "signTransaction",
decoded: {
functionName: "approve",
args: {
"1": "115792089237316195423570985008687907853269984665640564039457584007913129639935"
}
}
}
]
};
Uniswap V2 Policy
const uniswapPolicy = {
allow: [
// Allow swaps up to 1 ETH
{
method: "signTransaction",
chain: "evm:eip155:1",
contractAddress: { addressEq: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D" },
decoded: {
functionName: "swapExactETHForTokens",
args: {
lengthEq: 4,
"0": { bigIntMin: "900000000000000000" }, // Min tokens out (0.9 ETH worth)
"3": { bigIntMax: Math.floor(Date.now() / 1000) + 1800 } // 30 min deadline
},
value: { bigIntMax: "1000000000000000000" } // Max 1 ETH
}
},
// Allow token swaps
{
method: "signTransaction",
contractAddress: { addressEq: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D" },
decoded: {
functionName: "swapExactTokensForTokens",
args: {
lengthEq: 5,
"0": { bigIntMax: "1000000000000000000000" }, // Max 1000 tokens in
"1": { bigIntMin: "900000000000000000000" } // Min 900 tokens out
}
}
}
],
deny: [
// Deny adding liquidity with large amounts
{
method: "signTransaction",
decoded: {
functionName: "addLiquidityETH",
args: {
"1": { bigIntGt: "5000000000000000000000" } // > 5000 tokens
}
}
}
]
};
Hyperliquid Policy
const hyperliquidPolicy = {
allow: [
// Allow low-risk orders up to $1000 notional
{
method: "signTypedData",
chain: "evm:eip155:42161", // Arbitrum
metadata: {
verifiedOrder: {
type: "order",
riskLevel: "low",
notionalValue: { numericMax: 1000 }
}
}
},
// Allow canceling orders
{
method: "signTypedData",
metadata: {
verifiedOrder: {
type: "cancel"
}
}
},
// Allow withdrawals up to $500
{
method: "signTypedData",
metadata: {
verifiedOrder: {
type: "withdraw",
amount: { numericMax: 500 }
}
}
}
],
deny: [
// Deny high-risk trading
{
method: "signTypedData",
metadata: {
verifiedOrder: {
riskLevel: "high"
}
}
},
// Deny large leveraged positions
{
method: "signTypedData",
metadata: {
verifiedOrder: {
type: "order",
leverage: { numericGt: 10 }
}
}
}
]
};
Validating Actions
Use the validatePolicy
function to validate actions against policies:
import { validatePolicy, ActionData, PolicyValidationResult } from "@osiris-ai/policy-engine";
async function checkAction(actionData: ActionData, policy: any) {
const result: PolicyValidationResult = await validatePolicy(actionData, policy);
if (result.allowed) {
console.log("✅ Action is allowed!");
console.log("Matched allow rules:", result.matchedAllowRules);
} else {
console.error("❌ Action is denied or no allow rule matched.");
if (result.matchedDenyRules.length > 0) {
console.error("Matched deny rules:", result.matchedDenyRules);
}
if (result.errors && result.errors.length > 0) {
console.error("Validation errors:", result.errors);
}
}
console.log("Expanded Action:", result.expandedAction);
}
Policy Validation Result
interface PolicyValidationResult {
allowed: boolean; // True if the action is allowed
matchedAllowRules: PolicyRule[]; // Allow rules that matched
matchedDenyRules: PolicyRule[]; // Deny rules that matched
expandedAction: ExpandedAction; // Action data after expansion and decoding
errors?: any[]; // Validation errors
}
Blockchain Support
EVM (Ethereum, Polygon, etc.)
The policy engine automatically decodes EVM transactions:
const exampleAction: ActionData = {
method: "signTransaction",
chain: "evm:eip155:1",
contractAddress: "0xTokenAddress",
walletAddress: "0xUserWallet",
organizationId: "org_123",
payload: {
serializedTransaction: "0x..." // Raw transaction data
},
abi: {
type: "etherscan", // Fetch ABI from Etherscan
value: "0xTokenAddress",
chain: "evm:eip155:1"
}
};
Features:
- Transaction Decoding: Automatically parses raw transactions using ABI
- Function Extraction: Extracts function names and arguments
- Address Handling: Case-insensitive address comparisons
- ABI Fetching: Supports Etherscan, Sourcify, or inline ABIs
Solana
For Solana transactions, the engine uses IDL (Interface Description Language):
const solanaAction: ActionData = {
method: "signTransaction",
chain: "solana:mainnet",
programId: "11111111111111111111111111111112",
walletAddress: "SolanaWalletAddress",
organizationId: "org_123",
payload: {
instructions: [...] // Solana instruction data
},
idl: {
type: "url",
value: "https://example.com/program.json"
}
};
Features:
- Instruction Decoding: Decodes Solana instructions using IDL
- Program Analysis: Extracts program names and instruction types
- Account Validation: Validates account addresses and permissions
Message Signing Policy
const messagePolicy = {
allow: [
// Allow signing messages from trusted domains
{
method: "signMessage",
payload: {
lengthEq: 1,
"0": {
domain: {
enum: ["myapp.com", "trusted-dapp.eth"]
},
message: {
stringStartsWith: "Login to"
}
}
}
}
],
deny: [
// Deny signing messages containing transfer instructions
{
method: "signMessage",
payload: {
lengthEq: 1,
"0": {
stringContains: "transfer"
}
}
}
]
};
Why Wallet Policies Matter
Unlike traditional APIs, blockchain transactions are:
- Irreversible: Once confirmed, cannot be undone
- Financial: Directly involve user's assets
- Complex: Smart contract interactions can have unexpected consequences
- High-stakes: Mistakes can result in total loss of funds
The policy engine prevents MCPs and LLMs from:
- 🚫 Transferring more than daily limits
- 🚫 Sending to blocked/suspicious addresses
- 🚫 Paying excessive gas fees
- 🚫 Interacting with unauthorized contracts
- 🚫 Executing transactions during off-hours
Advanced Features
ABI and IDL Fetching
The engine supports multiple ABI/IDL sources for decoding transactions:
// ActionData with ABI specification
const actionData: ActionData = {
method: "signTransaction",
chain: "evm:eip155:1",
contractAddress: "0xTokenAddress",
walletAddress: "0xUserWallet",
organizationId: "org_123",
payload: {
serializedTransaction: "0x..." // Raw transaction data
},
abi: {
type: "etherscan", // Fetch ABI from Etherscan
value: "0xTokenAddress",
chain: "evm:eip155:1"
}
};
// Alternative ABI sources:
// abi: { type: "sourcify", value: "0xContractAddress", chain: "evm:eip155:1" }
// abi: { type: "url", value: "https://example.com/abi.json" }
// abi: { type: "inline", value: [...] } // Actual ABI array
Best Practices
1. Start with Allow Lists
// ✅ GOOD: Explicit allow list
const policy = {
allow: [
{
method: "signTransaction",
decoded: {
functionName: "transfer",
args: {
"0": { addressInList: ["0xTrustedAddress1", "0xTrustedAddress2"] },
"1": { bigIntMax: "1000000000000000000" }
}
}
}
],
deny: []
};
// ❌ BAD: Implicit allow all
const badPolicy = {
allow: [], // Empty allow list means allow everything
deny: [
{
method: "signTransaction",
decoded: {
functionName: "transfer",
args: {
"0": { addressEq: "0xBadAddress" }
}
}
}
]
};
2. Use Reasonable Limits
// ✅ GOOD: Reasonable daily limits
const policy = {
allow: [
{
method: "signTransaction",
decoded: {
functionName: "transfer",
args: {
"0": { addressInList: ["0xTrustedAddress1", "0xTrustedAddress2"] },
"1": { bigIntMax: "10000000000000000000" } // 10 ETH daily limit
}
}
}
],
deny: []
};
3. Combine Multiple Constraints
// ✅ GOOD: Multiple constraints for safety
const policy = {
allow: [
{
method: "signTransaction",
contractAddress: { addressEq: "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D" },
decoded: {
functionName: "swapExactTokensForTokens",
args: {
"0": { bigIntMax: "1000000000000000000" }, // amountIn ≤ 1 ETH
"1": { bigIntMin: "900000000000000000" }, // amountOutMin ≥ 0.9 ETH
"4": { bigIntMax: Math.floor(Date.now() / 1000) + 3600 } // deadline ≤ 1 hour
}
}
}
],
deny: []
};
4. Test Your Policies
// Test policy with sample transactions
const testPolicy = {
allow: [
{
method: "signTransaction",
chain: "evm:eip155:1",
contractAddress: { addressEq: "0xTestContract" },
decoded: {
functionName: "transfer",
args: {
"0": { addressEq: "0xRecipient" },
"1": { bigIntMax: "1000000000000000000" }
}
}
}
],
deny: []
};
const testAction: ActionData = {
method: "signTransaction",
chain: "evm:eip155:1",
contractAddress: "0xTestContract",
walletAddress: "0xSender",
organizationId: "test-org",
payload: {
serializedTransaction: "0x..."
}
};
const result = await validatePolicy(testAction, testPolicy);
console.log("Policy test result:", result.allowed);