OsirisOsiris

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 permitted
  • deny rules: Define conditions under which an action is explicitly forbidden

An action is ultimately allowed if:

  1. It does not match any deny rules
  2. It matches at least one allow rule (if allow 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 array
  • pattern: String value must match the provided regular expression
  • minLength, maxLength: For strings and arrays
  • minimum, maximum: For numbers
  • properties, required: For objects
  • allOf, 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);

Next Steps