From 02ac9dda4accf01bde2512e345da964e468c2a77 Mon Sep 17 00:00:00 2001 From: jessesawa Date: Tue, 27 Jan 2026 16:36:27 -0500 Subject: [PATCH 1/4] Bump version to 2.2.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 06e72e01..f5aff138 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@predicate/contracts", - "version": "2.2.0", + "version": "2.2.1", "description": "Core contracts for Predicate AVS", "main": "index.js", "files": [ From e425847af264c52d5105a99512c94ca9b996f455 Mon Sep 17 00:00:00 2001 From: jessesawa Date: Tue, 6 Jan 2026 18:01:49 -0500 Subject: [PATCH 2/4] feat: add BasicPredicateClient and simplified vault examples - Added BasicPredicateClient for WHO-based authorization only - Added BasicVault example in inheritance folder (simplified authorization) - Added AdvancedVault example in inheritance folder (uses existing PredicateClient) - Updated README with decision guide: 'Do I need different rules based on WHAT users are doing, or just WHO?' - Removed duplicate AdvancedPredicateClient (use existing PredicateClient instead) - All examples now in src/examples/inheritance/ folder --- README.md | 292 +++++++++++++++++---- src/examples/inheritance/AdvancedVault.sol | 190 ++++++++++++++ src/examples/inheritance/BasicVault.sol | 96 +++++++ src/mixins/BasicPredicateClient.sol | 135 ++++++++++ 4 files changed, 665 insertions(+), 48 deletions(-) create mode 100644 src/examples/inheritance/AdvancedVault.sol create mode 100644 src/examples/inheritance/BasicVault.sol create mode 100644 src/mixins/BasicPredicateClient.sol diff --git a/README.md b/README.md index f4c0014f..75516acc 100644 --- a/README.md +++ b/README.md @@ -1,79 +1,275 @@ -# predicate-contracts -Predicate is programmable policy infrastructure for onchain financial products in regulated markets. It allows developers to enforce custom compliance rules at the smart contract level. This repository holds the official solidity contracts for [Predicate's](https://predicate.io) Application Compliance offering. +# predicate-contracts -## How It Works +Solidity library for creating compliant smart contracts application (e.g. Uniswap V4 hooks) using the Predicate network. -![Predicate Application Compliance Flow](https://raw.githubusercontent.com/PredicateLabs/predicate-contracts/main/images/system.png) +## Overview -**Full integration guide:** [docs.predicate.io](https://docs.predicate.io/v2/applications/smart-contracts) +Predicate Contracts v2 provides a simplified, production-ready implementation for on-chain compliance verification through attestation-based validation. This version features: -## Repository Structure +- **Simplified Architecture**: Single `PredicateRegistry` contract replacing complex ServiceManager +- **Easy Integration**: `PredicateClient` mixin for seamless integration into your contracts +- **Multiple Patterns**: Inheritance and Proxy patterns for different use cases +- **Enhanced Security**: ERC-7201 namespaced storage and statement-based validation +- **Production Ready**: Comprehensive test coverage and audit-ready code +See [OVERVIEW.md](./OVERVIEW.md) for detailed technical documentation. + +## Quick Start + +### Choosing Your PredicateClient Implementation + +Predicate v2 offers two PredicateClient implementations optimized for different use cases: + +#### **BasicPredicateClient** - Simplified for Most Use Cases +Use when your policy only needs: +- ✅ Allowlist/denylist of addresses +- ✅ Time-based restrictions +- ✅ Geographic/IP-based rules +- ✅ Simple compliance checks (KYC/AML status) + +```solidity +import {BasicPredicateClient} from "@predicate/mixins/BasicPredicateClient.sol"; +import {Attestation} from "@predicate/interfaces/IPredicateRegistry.sol"; + +contract SimpleVault is BasicPredicateClient { + constructor(address _registry, string memory _policy) { + _initPredicateClient(_registry, _policy); + } + + function withdraw(uint256 amount, Attestation calldata attestation) external { + // Simple authorization - no encoding needed! + require(_authorizeTransaction(attestation, msg.sender), "Unauthorized"); + + // Your business logic + _processWithdrawal(amount); + } +} ``` -src/ -├── PredicateRegistry.sol # Core registry contract (Predicate-owned) -│ # - Attester management -│ # - Attestation verification -│ # - UUID-based replay protection -│ -├── mixins/ -│ └── PredicateClient.sol # Inherit this in your contracts -│ # - _initPredicateClient() for setup -│ # - _authorizeTransaction() for validation -│ # - ERC-7201 namespaced storage -│ -├── interfaces/ -│ ├── IPredicateRegistry.sol # Registry interface + Statement/Attestation structs -│ └── IPredicateClient.sol # Client interface -│ -└── examples/ # Reference implementations - ├── inheritance/ # Direct inheritance pattern - └── proxy/ # Proxy pattern for separation of concerns + +**Benefits:** Lower gas costs, simpler integration, cleaner code + +#### **PredicateClient** - Full Control for Complex Policies +Use when your policy requires: +- ✅ Function-specific permissions (e.g., allow withdraw but not transfer) +- ✅ Value-based limits (e.g., max 10 ETH per transaction) +- ✅ Parameter validation (e.g., only send to whitelisted addresses) +- ✅ Different rules for different functions + +```solidity +import {PredicateClient} from "@predicate/mixins/PredicateClient.sol"; +import {Attestation} from "@predicate/interfaces/IPredicateRegistry.sol"; + +contract DeFiVault is PredicateClient { + constructor(address _registry, string memory _policy) { + _initPredicateClient(_registry, _policy); + } + + function withdrawWithLimit( + address to, + uint256 amount, + Attestation calldata attestation + ) external payable { + // Encode function details for policy validation + bytes memory encoded = abi.encodeWithSignature( + "_executeWithdraw(address,uint256)", + to, + amount + ); + + // Advanced authorization with full context + require( + _authorizeTransaction(attestation, encoded, msg.sender, msg.value), + "Unauthorized" + ); + + _executeWithdraw(to, amount); + } +} ``` +**Benefits:** Complete policy flexibility, function-aware rules, value validation + +### Quick Decision Guide + +**The key question: "Do I need to enforce different rules based on WHAT users are doing, or just WHO is doing it?"** + +If you answered "just WHO", use BasicPredicateClient - it's simpler, cheaper, and covers most use cases. + +| Question | BasicPredicateClient | PredicateClient | +|----------|---------------------|-----------------| +| Need to validate WHO can call? | ✅ | ✅ | +| Need to validate WHEN they can call? | ✅ | ✅ | +| Need to validate WHICH function? | ❌ | ✅ | +| Need to validate HOW MUCH value? | ❌ | ✅ | +| Need to validate function PARAMETERS? | ❌ | ✅ | + +See `src/examples/inheritance/` for complete working examples. + +## Integration Patterns + +Predicate v2 supports multiple integration patterns: + +### 1. Inheritance Pattern (Recommended for most use cases) +- **Location**: `src/examples/inheritance/` +- **Best for**: Direct control, minimal dependencies +- Contract directly inherits `PredicateClient` +- Lowest gas cost, most straightforward + +### 2. Proxy Pattern (Recommended for separation of concerns) +- **Location**: `src/examples/proxy/` +- **Best for**: Clean separation, upgradeability +- Separate proxy contract handles validation +- Business logic contract remains simple + +See [src/examples/README.md](./src/examples/README.md) for detailed pattern documentation. + +## Architecture + +- **PredicateRegistry**: Core registry managing attesters, policies, and validation +- **PredicateClient**: Mixin contract for customer integration +- **Statement**: Data structure representing a transaction to be validated +- **Attestation**: Signed approval from an authorized attester + +``` +User Transaction + ↓ +Your Contract (with PredicateClient) + ↓ +_authorizeTransaction() + ↓ +PredicateRegistry.validateAttestation() + ↓ +Verify signature & policy + ↓ +Execute business logic +``` + +## Key Concepts + +### Statement (formerly Task) +A `Statement` represents a claim about a transaction to be executed: +- UUID for replay protection +- Transaction parameters (sender, target, value, encoded function call) +- Policy identifier +- Expiration timestamp + +### Attestation +An `Attestation` is a signed approval from an authorized attester: +- Matching UUID from the statement +- Attester address +- ECDSA signature over the statement hash +- Expiration timestamp + +### Events for Monitoring + +Predicate v2 emits comprehensive events for off-chain monitoring: + +**PredicateRegistry events:** +- `AttesterRegistered` / `AttesterDeregistered` - Attester management +- `PolicySet` - Policy changes (emitted when client calls `setPolicyID()`) +- `StatementValidated` - Successful attestation validations + +**PredicateClient events** (from your contract): +- `PredicatePolicyIDUpdated` - Track policy changes in your contract +- `PredicateRegistryUpdated` - Alert on registry address changes (security-critical) + +**Note:** Transaction authorization is tracked via `StatementValidated` from PredicateRegistry (no duplicate event needed). + +These events enable: +- 📊 Analytics and usage tracking +- 🔍 Audit trails and compliance monitoring +- ⚠️ Security alerts (unexpected policy/registry changes) +- 🐛 Debugging and transaction analysis + +## Migration from v1 + +v2 introduces several improvements over v1: + +| Feature | v1 | v2 | +|---------|----|----| +| Architecture | Multiple ServiceManager components | Single PredicateRegistry | +| Validation | Quorum-based | Single attester signature | +| Policies | Complex objects | Simple string identifiers | +| Replay Protection | Block-based nonces | UUID-based with expiration | +| Client Integration | Direct calls | PredicateClient mixin | + +See [OVERVIEW.md](./OVERVIEW.md#migration-guide-for-v1--v2) for detailed migration guide. + ## Installation -### Foundry +This repository depends on some submodules. Please run the following command before testing. ```bash -forge install PredicateLabs/predicate-contracts +git submodule update --init --recursive ``` -### npm +### Foundry + +```shell +$ forge install PredicateLabs/predicate-contracts +``` + +### Node ```bash -npm install @predicate/contracts +npm install @predicate/predicate-contracts ``` -## Quick Example -```solidity -import {PredicateClient} from "@predicate/contracts/src/mixins/PredicateClient.sol"; -import {Attestation} from "@predicate/contracts/src/interfaces/IPredicateRegistry.sol"; +## Build -contract MyVault is PredicateClient { - constructor(address _registry, string memory _policyID) { - _initPredicateClient(_registry, _policyID); - } +```shell +$ forge build +``` - function deposit(uint256 amount, Attestation calldata attestation) external payable { - bytes memory encoded = abi.encodeWithSignature("_deposit(uint256)", amount); - require(_authorizeTransaction(attestation, encoded, msg.sender, msg.value), "Unauthorized"); - // ... business logic - } -} +### Test + +```shell +$ forge test +``` + +### Format + +```shell +$ forge fmt +``` + +### Gas Snapshots + +```shell +$ forge snapshot +``` + +### Anvil + +```shell +$ anvil +``` + +### Cast + +```shell +$ cast ``` ## Documentation -- **Integration Guide:** [docs.predicate.io/v2/applications/smart-contracts](https://docs.predicate.io/v2/applications/smart-contracts) -- **Supported Chains:** [docs.predicate.io/v2/applications/supported-chains](https://docs.predicate.io/v2/applications/supported-chains) -- **API Reference:** [docs.predicate.io/api-reference](https://docs.predicate.io/api-reference/introduction) +- **[OVERVIEW.md](./OVERVIEW.md)** - Complete technical overview of v2 architecture +- **[PLAN.md](./PLAN.md)** - Pre-deployment checklist and task tracking +- **[src/examples/README.md](./src/examples/README.md)** - Integration patterns guide +- **[src/examples/](./src/examples/)** - Working code examples + +## Contributing + +Contributions are welcome! Please ensure: +- All tests pass: `forge test` +- Code is formatted: `forge fmt` +- Changes are documented ## License See [LICENSE](./LICENSE) for details. -## Disclaimer +## Disclaimer -This software is provided as-is. Use at your own risk. +This library is provided as-is, without any guarantees or warranties. Use at your own risk. diff --git a/src/examples/inheritance/AdvancedVault.sol b/src/examples/inheritance/AdvancedVault.sol new file mode 100644 index 00000000..596cd68a --- /dev/null +++ b/src/examples/inheritance/AdvancedVault.sol @@ -0,0 +1,190 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.28; + +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {PredicateClient} from "../../mixins/PredicateClient.sol"; +import {Attestation} from "../../interfaces/IPredicateRegistry.sol"; + +/** + * @title AdvancedVault + * @author Predicate Labs, Inc (https://predicate.io) + * @notice Minimal example using PredicateClient for function and value-based authorization + * @dev Demonstrates PredicateClient when your policy needs to validate: + * - WHICH function is called (withdraw vs transfer) + * - HOW MUCH value (amount limits) + * - WHAT parameters (recipient addresses) + * + * The key difference: policies can enforce different rules per function + */ +contract AdvancedVault is PredicateClient, Ownable { + mapping(address => uint256) public balances; + + event Deposit(address indexed user, uint256 amount); + event Withdrawal(address indexed user, address indexed to, uint256 amount); + event Transfer(address indexed from, address indexed to, uint256 amount); + + constructor( + address _owner, + address _registry, + string memory _policyID + ) Ownable(_owner) { + _initPredicateClient(_registry, _policyID); + } + + /** + * @notice Deposit ETH - no attestation required + */ + function deposit() external payable { + balances[msg.sender] += msg.value; + emit Deposit(msg.sender, msg.value); + } + + /** + * @notice Withdraw with amount validation + * @dev Policy can enforce max withdrawal limits + * @param _amount Amount to withdraw (validated by policy) + * @param _attestation Must include encoded function data + */ + function withdraw( + uint256 _amount, + Attestation calldata _attestation + ) external { + require(balances[msg.sender] >= _amount, "Insufficient balance"); + + // Encode function signature and parameters for policy validation + bytes memory encoded = abi.encodeWithSignature("_executeWithdraw(address,uint256)", msg.sender, _amount); + + // Advanced authorization with function details + require(_authorizeTransaction(_attestation, encoded, msg.sender, 0), "Unauthorized"); + + _executeWithdraw(msg.sender, _amount); + } + + /** + * @notice Withdraw to specific address with full parameter validation + * @dev Policy validates BOTH recipient AND amount + * @param _to Recipient address (validated by policy) + * @param _amount Amount (validated by policy) + * @param _attestation Must include encoded function data + */ + function withdrawTo( + address _to, + uint256 _amount, + Attestation calldata _attestation + ) external { + require(balances[msg.sender] >= _amount, "Insufficient balance"); + + // Policy sees different function signature and parameters + bytes memory encoded = abi.encodeWithSignature("_executeWithdraw(address,uint256)", _to, _amount); + + require(_authorizeTransaction(_attestation, encoded, msg.sender, 0), "Unauthorized"); + + _executeWithdraw(_to, _amount); + } + + /** + * @notice Transfer between users - different policy rules than withdraw + * @dev Shows how policies can have function-specific rules + * @param _to Recipient + * @param _amount Amount to transfer + * @param _attestation Must include encoded transfer data + */ + function transfer( + address _to, + uint256 _amount, + Attestation calldata _attestation + ) external { + require(balances[msg.sender] >= _amount, "Insufficient balance"); + + // Different function = different policy rules possible + bytes memory encoded = + abi.encodeWithSignature("_executeTransfer(address,address,uint256)", msg.sender, _to, _amount); + + require(_authorizeTransaction(_attestation, encoded, msg.sender, 0), "Unauthorized"); + + _executeTransfer(msg.sender, _to, _amount); + } + + /** + * @notice Payable function example - policy validates msg.value + * @dev Shows value-based authorization for payable functions + * @param _lockPeriod Lock period parameter + * @param _attestation Must include msg.value in validation + */ + function depositAndLock( + uint256 _lockPeriod, + Attestation calldata _attestation + ) external payable { + require(msg.value > 0, "Must send ETH"); + + bytes memory encoded = + abi.encodeWithSignature("_executeLock(address,uint256,uint256)", msg.sender, msg.value, _lockPeriod); + + // Note: msg.value passed for validation + require(_authorizeTransaction(_attestation, encoded, msg.sender, msg.value), "Unauthorized"); + + balances[msg.sender] += msg.value; + emit Deposit(msg.sender, msg.value); + // Lock logic would go here + } + + // Internal execution functions + function _executeWithdraw( + address _to, + uint256 _amount + ) internal { + balances[msg.sender] -= _amount; + payable(_to).transfer(_amount); + emit Withdrawal(msg.sender, _to, _amount); + } + + function _executeTransfer( + address _from, + address _to, + uint256 _amount + ) internal { + balances[_from] -= _amount; + balances[_to] += _amount; + emit Transfer(_from, _to, _amount); + } + + function _executeLock( + address _user, + uint256 _amount, + uint256 _period + ) internal { + // Lock implementation would go here + } + + /** + * @notice Required: Set policy ID with access control + * @dev Business logic contracts MUST implement this with proper access control + */ + function setPolicyID( + string memory _policyID + ) external onlyOwner { + _setPolicyID(_policyID); + } + + /** + * @notice Required: Set registry with access control + * @dev Business logic contracts MUST implement this with proper access control + */ + function setRegistry( + address _registry + ) external onlyOwner { + _setRegistry(_registry); + } + + /** + * @notice Required: Expose policy ID getter + * @dev Inherited from PredicateClient - no implementation needed + */ + // function getPolicyID() external view returns (string memory) - inherited + + /** + * @notice Required: Expose registry getter + * @dev Inherited from PredicateClient - no implementation needed + */ + // function getRegistry() external view returns (address) - inherited +} diff --git a/src/examples/inheritance/BasicVault.sol b/src/examples/inheritance/BasicVault.sol new file mode 100644 index 00000000..d98d26ad --- /dev/null +++ b/src/examples/inheritance/BasicVault.sol @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.28; + +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {BasicPredicateClient} from "../../mixins/BasicPredicateClient.sol"; +import {Attestation} from "../../interfaces/IPredicateRegistry.sol"; + +/** + * @title BasicVault + * @author Predicate Labs, Inc (https://predicate.io) + * @notice Minimal example using BasicPredicateClient for simple WHO-based access control + * @dev Demonstrates BasicPredicateClient when your policy only validates: + * - WHO can perform actions (allowlist/denylist) + * - WHEN they can act (time-based restrictions) + * + * NOT for policies that need: + * - Function-specific rules (use AdvancedVault) + * - Value-based limits (use AdvancedVault) + * - Parameter validation (use AdvancedVault) + */ +contract BasicVault is BasicPredicateClient, Ownable { + mapping(address => uint256) public balances; + + event Deposit(address indexed user, uint256 amount); + event Withdrawal(address indexed user, uint256 amount); + + constructor( + address _owner, + address _registry, + string memory _policyID + ) Ownable(_owner) { + _initPredicateClient(_registry, _policyID); + } + + /** + * @notice Deposit ETH - no attestation required + */ + function deposit() external payable { + balances[msg.sender] += msg.value; + emit Deposit(msg.sender, msg.value); + } + + /** + * @notice Withdraw with BasicPredicateClient authorization + * @dev Policy validates WHO (sender) and target contract only. + * Cannot validate amount or function specifics. + * @param _amount Amount to withdraw + * @param _attestation Attestation from Predicate API (only from/to/chain fields) + */ + function withdraw( + uint256 _amount, + Attestation calldata _attestation + ) external { + require(balances[msg.sender] >= _amount, "Insufficient balance"); + + // Simple authorization - no encoding needed + require(_authorizeTransaction(_attestation, msg.sender), "Unauthorized"); + + balances[msg.sender] -= _amount; + payable(msg.sender).transfer(_amount); + + emit Withdrawal(msg.sender, _amount); + } + + /** + * @notice Required: Set policy ID with access control + * @dev Business logic contracts MUST implement this with proper access control + */ + function setPolicyID( + string memory _policyID + ) external onlyOwner { + _setPolicyID(_policyID); + } + + /** + * @notice Required: Set registry with access control + * @dev Business logic contracts MUST implement this with proper access control + */ + function setRegistry( + address _registry + ) external onlyOwner { + _setRegistry(_registry); + } + + /** + * @notice Required: Expose policy ID getter + * @dev Inherited from BasicPredicateClient - no implementation needed + */ + // function getPolicyID() external view returns (string memory) - inherited + + /** + * @notice Required: Expose registry getter + * @dev Inherited from BasicPredicateClient - no implementation needed + */ + // function getRegistry() external view returns (address) - inherited +} diff --git a/src/mixins/BasicPredicateClient.sol b/src/mixins/BasicPredicateClient.sol new file mode 100644 index 00000000..fdc78add --- /dev/null +++ b/src/mixins/BasicPredicateClient.sol @@ -0,0 +1,135 @@ +// SPDX-License-Identifier: BUSL-1.1 +pragma solidity 0.8.28; + +import {IPredicateRegistry, Attestation, Statement} from "../interfaces/IPredicateRegistry.sol"; +import {IPredicateClient, PredicateClient__Unauthorized} from "../interfaces/IPredicateClient.sol"; + +/** + * @title BasicPredicateClient + * @author Predicate Labs, Inc (https://predicate.io) + * @notice Simplified authorization for WHO-based policies only + * @dev Use when policies only validate sender identity, not function or value details. + * Uses canonical zero values for encodedSigAndArgs and msgValue. + */ +abstract contract BasicPredicateClient is IPredicateClient { + /// @notice Struct to contain stateful values for PredicateClient-type contracts + /// @custom:storage-location erc7201:predicate.storage.PredicateClient + struct PredicateClientStorage { + IPredicateRegistry registry; + string policy; + } + + /// @notice The storage slot for the PredicateClientStorage struct + /// @dev keccak256(abi.encode(uint256(keccak256("predicate.storage.PredicateClient")) - 1)) & ~bytes32(uint256(0xff)) + /// @dev Same slot as PredicateClient for consistency across implementations + bytes32 private constant _PREDICATE_CLIENT_STORAGE_SLOT = + 0x804776a84f3d03ad8442127b1451e2fbbb6a715c681d6a83c9e9fca787b99300; + + /// @notice Emitted when the PredicateRegistry address is updated + event PredicateRegistryUpdated(address indexed oldRegistry, address indexed newRegistry); + + /// @notice Emitted when the policy ID is updated + event PredicatePolicyIDUpdated(string oldPolicyID, string newPolicyID); + + function _getPredicateClientStorage() private pure returns (PredicateClientStorage storage $) { + assembly { + $.slot := _PREDICATE_CLIENT_STORAGE_SLOT + } + } + + /** + * @notice Initializes with registry and policy ID + * @dev Must be called in constructor + */ + function _initPredicateClient( + address _registryAddress, + string memory _policyID + ) internal { + PredicateClientStorage storage $ = _getPredicateClientStorage(); + $.registry = IPredicateRegistry(_registryAddress); + _setPolicyID(_policyID); + } + + /// @notice Updates the policy ID + function _setPolicyID( + string memory _policyID + ) internal { + PredicateClientStorage storage $ = _getPredicateClientStorage(); + string memory oldPolicyID = $.policy; + + if (keccak256(bytes(oldPolicyID)) != keccak256(bytes(_policyID))) { + $.policy = _policyID; + $.registry.setPolicyID(_policyID); + emit PredicatePolicyIDUpdated(oldPolicyID, _policyID); + } + } + + /// @notice Updates the registry address + function _setRegistry( + address _registryAddress + ) internal { + PredicateClientStorage storage $ = _getPredicateClientStorage(); + address oldRegistry = address($.registry); + + if (oldRegistry != _registryAddress) { + $.registry = IPredicateRegistry(_registryAddress); + + // Re-register cached policy with new registry + string memory cachedPolicy = $.policy; + if (bytes(cachedPolicy).length > 0) { + $.registry.setPolicyID(cachedPolicy); + } + + emit PredicateRegistryUpdated(oldRegistry, _registryAddress); + } + } + + /// @notice Returns the policy ID + function getPolicyID() external view returns (string memory policyID) { + return _getPolicyID(); + } + + function _getPolicyID() internal view returns (string memory policyID) { + return _getPredicateClientStorage().policy; + } + + /// @notice Returns the registry address + function getRegistry() external view returns (address) { + return _getRegistry(); + } + + function _getRegistry() internal view returns (address) { + return address(_getPredicateClientStorage().registry); + } + + modifier onlyPredicateRegistry() { + if (msg.sender != address(_getPredicateClientStorage().registry)) { + revert PredicateClient__Unauthorized(); + } + _; + } + + /** + * @notice Validates transaction with canonical zero values + * @dev Uses canonical msgValue=0 and encodedSigAndArgs="" for simple WHO-based validation + */ + function _authorizeTransaction( + Attestation memory _attestation, + address _msgSender + ) internal returns (bool success) { + PredicateClientStorage storage $ = _getPredicateClientStorage(); + + // Build Statement with canonical zero values + Statement memory statement = Statement({ + msgSender: _msgSender, + target: address(this), + msgValue: 0, // Canonical zero for value + encodedSigAndArgs: hex"", // Canonical empty bytes for function data + policy: $.policy, + expiration: _attestation.expiration, + uuid: _attestation.uuid + }); + + return $.registry.validateAttestation(statement, _attestation); + } +} From 85a86c860699e10cc527f76515a7c0583bcc3307 Mon Sep 17 00:00:00 2001 From: jessesawa Date: Tue, 27 Jan 2026 19:18:42 -0500 Subject: [PATCH 3/4] Update README to match main's simplified version Co-Authored-By: Claude Opus 4.5 --- README.md | 292 +++++++++--------------------------------------------- 1 file changed, 48 insertions(+), 244 deletions(-) diff --git a/README.md b/README.md index 75516acc..f4c0014f 100644 --- a/README.md +++ b/README.md @@ -1,275 +1,79 @@ -# predicate-contracts +# predicate-contracts +Predicate is programmable policy infrastructure for onchain financial products in regulated markets. It allows developers to enforce custom compliance rules at the smart contract level. This repository holds the official solidity contracts for [Predicate's](https://predicate.io) Application Compliance offering. -Solidity library for creating compliant smart contracts application (e.g. Uniswap V4 hooks) using the Predicate network. +## How It Works -## Overview +![Predicate Application Compliance Flow](https://raw.githubusercontent.com/PredicateLabs/predicate-contracts/main/images/system.png) -Predicate Contracts v2 provides a simplified, production-ready implementation for on-chain compliance verification through attestation-based validation. This version features: +**Full integration guide:** [docs.predicate.io](https://docs.predicate.io/v2/applications/smart-contracts) -- **Simplified Architecture**: Single `PredicateRegistry` contract replacing complex ServiceManager -- **Easy Integration**: `PredicateClient` mixin for seamless integration into your contracts -- **Multiple Patterns**: Inheritance and Proxy patterns for different use cases -- **Enhanced Security**: ERC-7201 namespaced storage and statement-based validation -- **Production Ready**: Comprehensive test coverage and audit-ready code +## Repository Structure -See [OVERVIEW.md](./OVERVIEW.md) for detailed technical documentation. - -## Quick Start - -### Choosing Your PredicateClient Implementation - -Predicate v2 offers two PredicateClient implementations optimized for different use cases: - -#### **BasicPredicateClient** - Simplified for Most Use Cases -Use when your policy only needs: -- ✅ Allowlist/denylist of addresses -- ✅ Time-based restrictions -- ✅ Geographic/IP-based rules -- ✅ Simple compliance checks (KYC/AML status) - -```solidity -import {BasicPredicateClient} from "@predicate/mixins/BasicPredicateClient.sol"; -import {Attestation} from "@predicate/interfaces/IPredicateRegistry.sol"; - -contract SimpleVault is BasicPredicateClient { - constructor(address _registry, string memory _policy) { - _initPredicateClient(_registry, _policy); - } - - function withdraw(uint256 amount, Attestation calldata attestation) external { - // Simple authorization - no encoding needed! - require(_authorizeTransaction(attestation, msg.sender), "Unauthorized"); - - // Your business logic - _processWithdrawal(amount); - } -} ``` - -**Benefits:** Lower gas costs, simpler integration, cleaner code - -#### **PredicateClient** - Full Control for Complex Policies -Use when your policy requires: -- ✅ Function-specific permissions (e.g., allow withdraw but not transfer) -- ✅ Value-based limits (e.g., max 10 ETH per transaction) -- ✅ Parameter validation (e.g., only send to whitelisted addresses) -- ✅ Different rules for different functions - -```solidity -import {PredicateClient} from "@predicate/mixins/PredicateClient.sol"; -import {Attestation} from "@predicate/interfaces/IPredicateRegistry.sol"; - -contract DeFiVault is PredicateClient { - constructor(address _registry, string memory _policy) { - _initPredicateClient(_registry, _policy); - } - - function withdrawWithLimit( - address to, - uint256 amount, - Attestation calldata attestation - ) external payable { - // Encode function details for policy validation - bytes memory encoded = abi.encodeWithSignature( - "_executeWithdraw(address,uint256)", - to, - amount - ); - - // Advanced authorization with full context - require( - _authorizeTransaction(attestation, encoded, msg.sender, msg.value), - "Unauthorized" - ); - - _executeWithdraw(to, amount); - } -} +src/ +├── PredicateRegistry.sol # Core registry contract (Predicate-owned) +│ # - Attester management +│ # - Attestation verification +│ # - UUID-based replay protection +│ +├── mixins/ +│ └── PredicateClient.sol # Inherit this in your contracts +│ # - _initPredicateClient() for setup +│ # - _authorizeTransaction() for validation +│ # - ERC-7201 namespaced storage +│ +├── interfaces/ +│ ├── IPredicateRegistry.sol # Registry interface + Statement/Attestation structs +│ └── IPredicateClient.sol # Client interface +│ +└── examples/ # Reference implementations + ├── inheritance/ # Direct inheritance pattern + └── proxy/ # Proxy pattern for separation of concerns ``` -**Benefits:** Complete policy flexibility, function-aware rules, value validation - -### Quick Decision Guide - -**The key question: "Do I need to enforce different rules based on WHAT users are doing, or just WHO is doing it?"** - -If you answered "just WHO", use BasicPredicateClient - it's simpler, cheaper, and covers most use cases. - -| Question | BasicPredicateClient | PredicateClient | -|----------|---------------------|-----------------| -| Need to validate WHO can call? | ✅ | ✅ | -| Need to validate WHEN they can call? | ✅ | ✅ | -| Need to validate WHICH function? | ❌ | ✅ | -| Need to validate HOW MUCH value? | ❌ | ✅ | -| Need to validate function PARAMETERS? | ❌ | ✅ | - -See `src/examples/inheritance/` for complete working examples. - -## Integration Patterns - -Predicate v2 supports multiple integration patterns: - -### 1. Inheritance Pattern (Recommended for most use cases) -- **Location**: `src/examples/inheritance/` -- **Best for**: Direct control, minimal dependencies -- Contract directly inherits `PredicateClient` -- Lowest gas cost, most straightforward - -### 2. Proxy Pattern (Recommended for separation of concerns) -- **Location**: `src/examples/proxy/` -- **Best for**: Clean separation, upgradeability -- Separate proxy contract handles validation -- Business logic contract remains simple - -See [src/examples/README.md](./src/examples/README.md) for detailed pattern documentation. - -## Architecture - -- **PredicateRegistry**: Core registry managing attesters, policies, and validation -- **PredicateClient**: Mixin contract for customer integration -- **Statement**: Data structure representing a transaction to be validated -- **Attestation**: Signed approval from an authorized attester - -``` -User Transaction - ↓ -Your Contract (with PredicateClient) - ↓ -_authorizeTransaction() - ↓ -PredicateRegistry.validateAttestation() - ↓ -Verify signature & policy - ↓ -Execute business logic -``` - -## Key Concepts - -### Statement (formerly Task) -A `Statement` represents a claim about a transaction to be executed: -- UUID for replay protection -- Transaction parameters (sender, target, value, encoded function call) -- Policy identifier -- Expiration timestamp - -### Attestation -An `Attestation` is a signed approval from an authorized attester: -- Matching UUID from the statement -- Attester address -- ECDSA signature over the statement hash -- Expiration timestamp - -### Events for Monitoring - -Predicate v2 emits comprehensive events for off-chain monitoring: - -**PredicateRegistry events:** -- `AttesterRegistered` / `AttesterDeregistered` - Attester management -- `PolicySet` - Policy changes (emitted when client calls `setPolicyID()`) -- `StatementValidated` - Successful attestation validations - -**PredicateClient events** (from your contract): -- `PredicatePolicyIDUpdated` - Track policy changes in your contract -- `PredicateRegistryUpdated` - Alert on registry address changes (security-critical) - -**Note:** Transaction authorization is tracked via `StatementValidated` from PredicateRegistry (no duplicate event needed). - -These events enable: -- 📊 Analytics and usage tracking -- 🔍 Audit trails and compliance monitoring -- ⚠️ Security alerts (unexpected policy/registry changes) -- 🐛 Debugging and transaction analysis - -## Migration from v1 - -v2 introduces several improvements over v1: - -| Feature | v1 | v2 | -|---------|----|----| -| Architecture | Multiple ServiceManager components | Single PredicateRegistry | -| Validation | Quorum-based | Single attester signature | -| Policies | Complex objects | Simple string identifiers | -| Replay Protection | Block-based nonces | UUID-based with expiration | -| Client Integration | Direct calls | PredicateClient mixin | - -See [OVERVIEW.md](./OVERVIEW.md#migration-guide-for-v1--v2) for detailed migration guide. - ## Installation -This repository depends on some submodules. Please run the following command before testing. +### Foundry ```bash -git submodule update --init --recursive +forge install PredicateLabs/predicate-contracts ``` -### Foundry - -```shell -$ forge install PredicateLabs/predicate-contracts -``` - -### Node +### npm ```bash -npm install @predicate/predicate-contracts +npm install @predicate/contracts ``` +## Quick Example -## Build - -```shell -$ forge build -``` - -### Test - -```shell -$ forge test -``` - -### Format - -```shell -$ forge fmt -``` - -### Gas Snapshots - -```shell -$ forge snapshot -``` - -### Anvil - -```shell -$ anvil -``` +```solidity +import {PredicateClient} from "@predicate/contracts/src/mixins/PredicateClient.sol"; +import {Attestation} from "@predicate/contracts/src/interfaces/IPredicateRegistry.sol"; -### Cast +contract MyVault is PredicateClient { + constructor(address _registry, string memory _policyID) { + _initPredicateClient(_registry, _policyID); + } -```shell -$ cast + function deposit(uint256 amount, Attestation calldata attestation) external payable { + bytes memory encoded = abi.encodeWithSignature("_deposit(uint256)", amount); + require(_authorizeTransaction(attestation, encoded, msg.sender, msg.value), "Unauthorized"); + // ... business logic + } +} ``` ## Documentation -- **[OVERVIEW.md](./OVERVIEW.md)** - Complete technical overview of v2 architecture -- **[PLAN.md](./PLAN.md)** - Pre-deployment checklist and task tracking -- **[src/examples/README.md](./src/examples/README.md)** - Integration patterns guide -- **[src/examples/](./src/examples/)** - Working code examples - -## Contributing - -Contributions are welcome! Please ensure: -- All tests pass: `forge test` -- Code is formatted: `forge fmt` -- Changes are documented +- **Integration Guide:** [docs.predicate.io/v2/applications/smart-contracts](https://docs.predicate.io/v2/applications/smart-contracts) +- **Supported Chains:** [docs.predicate.io/v2/applications/supported-chains](https://docs.predicate.io/v2/applications/supported-chains) +- **API Reference:** [docs.predicate.io/api-reference](https://docs.predicate.io/api-reference/introduction) ## License See [LICENSE](./LICENSE) for details. -## Disclaimer +## Disclaimer -This library is provided as-is, without any guarantees or warranties. Use at your own risk. +This software is provided as-is. Use at your own risk. From 4af5c40f2fc248d4ef0510a8775cc7d6a0c06277 Mon Sep 17 00:00:00 2001 From: jessesawa Date: Tue, 27 Jan 2026 19:21:09 -0500 Subject: [PATCH 4/4] docs: add BasicPredicateClient to README --- README.md | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f4c0014f..958f3a50 100644 --- a/README.md +++ b/README.md @@ -17,10 +17,12 @@ src/ │ # - UUID-based replay protection │ ├── mixins/ -│ └── PredicateClient.sol # Inherit this in your contracts -│ # - _initPredicateClient() for setup -│ # - _authorizeTransaction() for validation -│ # - ERC-7201 namespaced storage +│ ├── PredicateClient.sol # Full-featured client (WHO + WHAT validation) +│ │ # - _authorizeTransaction(attestation, encoded, sender, value) +│ │ +│ └── BasicPredicateClient.sol # Simplified client (WHO-only validation) +│ # - _authorizeTransaction(attestation, sender) +│ # - Use when policies only validate sender identity │ ├── interfaces/ │ ├── IPredicateRegistry.sol # Registry interface + Statement/Attestation structs @@ -47,6 +49,26 @@ npm install @predicate/contracts ## Quick Example +**BasicPredicateClient** - Use when your policy only validates WHO is calling (sender identity): + +```solidity +import {BasicPredicateClient} from "@predicate/contracts/src/mixins/BasicPredicateClient.sol"; +import {Attestation} from "@predicate/contracts/src/interfaces/IPredicateRegistry.sol"; + +contract MyVault is BasicPredicateClient { + constructor(address _registry, string memory _policyID) { + _initPredicateClient(_registry, _policyID); + } + + function deposit(uint256 amount, Attestation calldata attestation) external payable { + require(_authorizeTransaction(attestation, msg.sender), "Unauthorized"); + // ... business logic + } +} +``` + +**PredicateClient** - Use when your policy also validates WHAT is being done (function, args, value): + ```solidity import {PredicateClient} from "@predicate/contracts/src/mixins/PredicateClient.sol"; import {Attestation} from "@predicate/contracts/src/interfaces/IPredicateRegistry.sol";