diff --git a/.gitignore b/.gitignore
index 48bbf22..563bfa3 100644
--- a/.gitignore
+++ b/.gitignore
@@ -9,5 +9,8 @@ node_modules
test-ledger
.yarn-error
+# Yarn - ignore entire yarn directories
+.yarn/
+
lib
.crates
\ No newline at end of file
diff --git a/.yarnrc.yml b/.yarnrc.yml
new file mode 100644
index 0000000..3186f3f
--- /dev/null
+++ b/.yarnrc.yml
@@ -0,0 +1 @@
+nodeLinker: node-modules
diff --git a/Anchor.toml b/Anchor.toml
index e698be8..6bc0b58 100644
--- a/Anchor.toml
+++ b/Anchor.toml
@@ -23,5 +23,9 @@ url = "https://api.devnet.solana.com"
address = "8UuqDAqe9UQx9e9Sjj4Gs3msrWGfzb4CJHGK3U3tcCEX"
filename = "tests/fixtures/pre-rent-collector/multisig-account.json"
+[[test.genesis]]
+address = "noopb9bkMVfRPU8AsbpTUg8AQkHtKwMYZiFUjNRtMmV"
+program = "tests/fixtures/noop.so"
+
[scripts]
test = "npx mocha --node-option require=ts-node/register --extension ts -t 1000000 tests/index.ts"
diff --git a/Cargo.lock b/Cargo.lock
index d46d37f..b98dabe 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -495,11 +495,11 @@ dependencies = [
[[package]]
name = "borsh"
-version = "1.5.3"
+version = "1.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2506947f73ad44e344215ccd6403ac2ae18cd8e046e581a441bf8d199f257f03"
+checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce"
dependencies = [
- "borsh-derive 1.5.3",
+ "borsh-derive 1.5.7",
"cfg_aliases",
]
@@ -531,9 +531,9 @@ dependencies = [
[[package]]
name = "borsh-derive"
-version = "1.5.3"
+version = "1.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c2593a3b8b938bd68373196c9832f516be11fa487ef4ae745eb282e6a56a7244"
+checksum = "fdd1d3c0c2f5833f22386f252fe8ed005c7f59fdcddeef025c01b4c3b9fd9ac3"
dependencies = [
"once_cell",
"proc-macro-crate 3.2.0",
@@ -1879,7 +1879,7 @@ dependencies = [
"blake3",
"borsh 0.10.4",
"borsh 0.9.3",
- "borsh 1.5.3",
+ "borsh 1.5.7",
"bs58 0.4.0",
"bv",
"bytemuck",
@@ -1928,7 +1928,7 @@ dependencies = [
"base64 0.21.7",
"bincode",
"bitflags",
- "borsh 1.5.3",
+ "borsh 1.5.7",
"bs58 0.4.0",
"bytemuck",
"byteorder",
diff --git a/README.md b/README.md
index 2f84cf1..b8501dc 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# Squads Smart Account Program v0.1
+# Squads Smart Account Program
@@ -8,122 +8,514 @@
[version-image]: https://img.shields.io/badge/version-0.1.0-blue.svg?style=flat
[license-image]: https://img.shields.io/badge/license-AGPL_3.0-blue.svg?style=flat
-We are developing the Smart Account Program to address programmability and cost limitations related to deploying smart contract wallets at scale on Solana.
+High-performance smart wallet infrastructure for Solana. Built for stablecoin systems, programmable wallets, and financial applications at scale.
-In order to deliver on this promise it builds on the following innovations:
+## Table of Contents
-- **Rent free wallet creation**: deploy wallets for as little as 0,0000025 SOL without paying rent until the account needs to execute transactions. Allowing developers to generate addresses for users at scale without worrying about deployment costs.
-- **Atomic policy enforcement and transaction execution**: this program empowers developers with optimized smart contract wallets fit for building fully onchain PSPs, stablecoin banks and programmable wallets alike.
-- **Archivable accounts**: recoup rent costs from inactive accounts without compromising on security using state compression. Enabling developers to confidently cover fees for their user's accounts by removing the burden of costs associated with inactive users.
-- **Policies**: set rules on which an account can execute transactions and extend your account's functionality by creating your own policy programs.
+- [Overview](#overview)
+- [Grid: The Easiest Way to Build](#grid-the-easiest-way-to-build)
+- [Quick Reference](#quick-reference)
+- [Core Concepts](#core-concepts)
+ - [Settings Account](#settings-account)
+ - [Sub-Accounts (Vaults)](#sub-accounts-vaults)
+ - [Signers & Permissions](#signers--permissions)
+ - [Time Lock](#time-lock)
+ - [Stale Transaction Protection](#stale-transaction-protection)
+ - [Account Types](#account-types)
+- [Execution Modes](#execution-modes)
+ - [Consensus Transactions (Async)](#consensus-transactions-async)
+ - [Synchronous Transactions](#synchronous-transactions)
+- [Policy Framework](#policy-framework)
+ - [Program Interaction Policy (Smart Transactions)](#program-interaction-policy-smart-transactions)
+ - [Spending Limit Policy](#spending-limit-policy)
+ - [Internal Fund Transfer Policy](#internal-fund-transfer-policy)
+ - [Settings Change Policy](#settings-change-policy)
+- [Legacy Spending Limits](#legacy-spending-limits)
+- [Developers](#developers)
+- [Building](#building)
+- [Testing](#testing)
+- [Verifying the Program](#verifying-the-program)
+- [Responsibility](#responsibility)
+- [Security](#security)
+- [License](#license)
-The Smart Account Program is in active development, with regular updates posted on [squads.so/blog](http://squads.so/blog) and [@SquadsProtocol](https://x.com/SquadsProtocol) on X.
+## Overview
-## Content
+Smart contract wallets have traditionally forced developers to choose between programmability, cost, and security. The Smart Account Program eliminates this trilemma:
-This repository contains:
+- **Programmability**: Multi-signature governance, granular permissions, and flexible policies (spending limits, time locks, program interaction rules)
+- **Cost**: Atomic policy enforcement and transaction execution in a single operation. Rent-free account creation allows deploying wallets for as little as 0.0000025 SOL, with rent deferred until the account executes transactions
+- **Security**: Audited by OtterSec and formally verified by Certora. Supports native keypairs, MPC/TEE signing, with passkeys and alternative signature schemes coming soon
-- The Squads Smart Account v0.1 program.
-- The `@sqds/smart-account` Typescript SDK to interact with the smart account program.
+For the full announcement, see [Smart Account Program: Live on Mainnet](https://squads.xyz/blog/squads-smart-account-program-live-on-mainnet).
-## Program (Smart contract) Addresses
+## Grid: The Easiest Way to Build
-The Squads Smart Account Program v0.1 is deployed to:
+**Don't want to integrate directly with the program?** [Grid](https://squads.xyz/grid) is our developer platform that abstracts the Smart Account Program into a simple API.
-- Solana Mainnet-beta: `SMRTzfY6DfH5ik3TKiyLFfXexV8uSG3d2UksSCYdunG`
-- Solana Devnet: `SMRTzfY6DfH5ik3TKiyLFfXexV8uSG3d2UksSCYdunG`
+With Grid you get:
+- **Stablecoin accounts** - policy-ready, fault-tolerant, proven in production
+- **Payments** - stablecoins + ACH, Wire, SEPA rails
+- **Card infrastructure** - programmable spend controls
+- **Yield integrations** - RWA and DeFi strategies
+- **Gas abstraction** - seamless UX without native tokens
+- **Reconciliation** - unified ledger across all rails
-Both deployments can be verified using the [Ellipsis Labs verifiable build](https://github.com/Ellipsis-Labs/solana-verifiable-build) tool.
+Grid is trusted by 400+ teams securing $15B+. If you're building fintech, neobanks, or consumer apps, start with [Grid's documentation](https://grid.squads.xyz/welcome).
-## Responsibility
+For the full vision, see [Grid: A Stablecoin API for Accounts, Payments, Cards and Yield](https://squads.xyz/blog/grid-a-stablecoin-api-for-accounts-payments-cards-and-yield).
-By interacting with this program, users acknowledge and accept full personal responsibility for any consequences, regardless of their nature. This includes both potential risks inherent to the smart contract, also referred to as program, as well as any losses resulting from user errors or misjudgment.
+---
-By using a smart account, it is important to acknowledge certain concepts. Here are some that could be misunderstood by users:
+*The rest of this README documents the underlying Smart Account Program for developers who need direct protocol access.*
-- Loss of Private Keys: If a participant loses their private key, the smart account may not be able to execute transactions if a threshold number of signatures is required.
-- Single Point of Failure with Keys: If all keys are stored in the same location or device, a single breach can compromise the smart account.
-- Forgetting the Threshold: Misremembering the number of signatures required can result in a deadlock, where funds cannot be accessed.
-- No Succession Planning: If keyholders become unavailable (e.g., due to accident, death), without a plan for transition, funds may be locked forever.
-- Transfer of funds to wrong address: Funds should always be sent to the smart account account, and not the smart account settingsaddress. Due to the design of the Squads Protocol program, funds deposited to the smart account may not be recoverable.
-- If the settings_authority of a smart account is compromised, an attacker can change smart account settings, potentially reducing the required threshold for transaction execution or instantly being able to remove and add new members.
-- If the underlying SVM compatible blockchain undergoes a fork and a user had sent funds to the orphaned chain, the state of the blockchain may not interpret the owner of funds to be original one.
-- Users might inadvertently set long or permanent time-locks in their smart account, preventing access to their funds for that period of time.
-- Smart account participants might not have enough of the native token of the underlying SVM blockchain to pay for transaction and state fees.
+## Quick Reference
-## Developers
+| Network | Address |
+|---------|---------|
+| Mainnet | `SMRTzfY6DfH5ik3TKiyLFfXexV8uSG3d2UksSCYdunG` |
+| Devnet | `SMRTzfY6DfH5ik3TKiyLFfXexV8uSG3d2UksSCYdunG` |
-You can interact with the Squads Smart Account Program via our SDKs.
+## Core Concepts
-List of SDKs:
+### Settings Account
-- Typescript SDK: `@sqds/smart-account` (not yet published)
+The Settings account is the central configuration for a Smart Account.
-## Compiling and testing
+**PDA Derivation:**
+```
+seeds = ["smart_account", "settings", seed]
+```
-You can compile the code with Anchor.
+**Account Structure:**
+```rust
+Settings {
+ seed: u128, // Unique identifier assigned by program config
+ settings_authority: Pubkey, // Controls settings (default = autonomous)
+ threshold: u16, // Required votes for approval
+ time_lock: u32, // Seconds between approval and execution
+ transaction_index: u64, // Latest transaction number
+ stale_transaction_index: u64, // Staleness boundary
+ archival_authority: Option, // Reserved for compression feature
+ archivable_after: u64, // Timestamp for archival eligibility
+ bump: u8, // PDA bump
+ signers: Vec, // Signers with permissions
+ account_utilization: u8, // Number of sub-accounts in use
+ policy_seed: Option, // Counter for deterministic policy creation
+}
+
+SmartAccountSigner {
+ key: Pubkey,
+ permissions: Permissions { mask: u8 },
+}
+```
+### Sub-Accounts (Vaults)
+
+Assets are held in sub-accounts derived from the Settings account. Each sub-account is identified by an `account_index` (u8), allowing up to 256 vaults per Smart Account.
+
+**PDA Derivation:**
```
-anchor build
+seeds = ["smart_account", settings_key, "smart_account", account_index]
```
-If you do not have the Solana Anchor framework CLI installed, you can do so by following [this guide](https://www.anchor-lang.com/docs/installation).
+Sub-account 0 is typically the primary vault.
+
+### Signers & Permissions
+
+Each signer has a pubkey and a permission bitmask controlling what actions they can perform:
+
+| Permission | Bit | Description |
+|------------|-----|-------------|
+| Initiate | `0b001` | Create transactions and proposals |
+| Vote | `0b010` | Approve or reject proposals |
+| Execute | `0b100` | Execute approved transactions |
+
+Permissions combine freely. Examples:
+- `0b111` (7) - Full access: can initiate, vote, and execute
+- `0b011` (3) - Can initiate and vote, but not execute
+- `0b110` (6) - Can vote and execute, but not initiate
+
+**Threshold** defines how many Vote permissions are required for approval. A 2-of-3 multisig means threshold=2 with at least 3 signers having Vote permission.
+
+**Invariants enforced by the program:**
+- At least one signer must have Initiate permission
+- At least one signer must have Vote permission
+- At least one signer must have Execute permission
+- Threshold must be > 0 and <= number of voters
+
+### Time Lock
+
+Configurable delay (in seconds) between proposal approval and execution. Range: 0 to 90 days.
+
+- Time lock of 0 enables synchronous transactions (immediate execution)
+- Non-zero time lock requires waiting period after approval
+
+### Stale Transaction Protection
+
+When signers, threshold, or time lock change, the program updates `stale_transaction_index` to the current `transaction_index`. This invalidates all pending settings transactions, preventing outdated proposals from executing under new governance rules.
+
+### Account Types
+
+**Autonomous:** `settings_authority = Pubkey::default()`
+- All configuration changes go through proposal voting
+- Fully self-governed
-To deploy the program on a local validator instance for testing or development purposes, you can create a local instance by running this command from the [Solana CLI](https://docs.solana.com/cli/install-solana-cli-tools).
+**Controlled:** `settings_authority = `
+- The settings authority can modify configuration directly
+- Useful for managed/custodial setups
+## Execution Modes
+
+### Consensus Transactions (Async)
+
+Full governance flow with voting and time lock. Use when signers aren't available simultaneously or when you need a deliberation period.
+
+**Flow:**
```
-solana-test-validator
+Create Transaction → Create Proposal → Vote (until threshold) → [Time Lock] → Execute
```
-To run the tests, first install the node modules for the repository.
+**Accounts involved:**
+
+```rust
+Transaction {
+ settings: Pubkey, // Parent settings account
+ creator: Pubkey, // Who created it
+ index: u64, // Transaction index
+ bump: u8,
+ account_index: u8, // Which vault executes this
+ ephemeral_signer_bumps: Vec,
+ message: SmartAccountMessage, // The actual instruction(s)
+}
+
+Proposal {
+ settings: Pubkey,
+ transaction_index: u64,
+ status: ProposalStatus, // Active, Approved, Rejected, Cancelled, Executed
+ bump: u8,
+ approved: Vec, // Signers who approved
+ rejected: Vec, // Signers who rejected
+ cancelled: Vec, // Signers who cancelled
+ activation_timestamp: i64, // When time lock ends
+}
+```
+
+**Lifecycle:**
+1. **Create Transaction**: Store the instruction(s) to execute
+2. **Create Proposal**: Initialize voting state (status = Active)
+3. **Vote**: Signers approve/reject. Once threshold reached, status = Approved and `activation_timestamp` is set
+4. **Time Lock**: Wait until `current_time >= activation_timestamp`
+5. **Execute**: Run the transaction. Status = Executed
+
+**Settings Transactions** follow the same flow but modify the Settings account itself (add/remove signers, change threshold, etc.). They have additional staleness checks: if the settings changed after proposal creation, execution fails.
+
+### Synchronous Transactions
+
+Atomic execution when all required signers are present. No intermediate accounts, no waiting.
+**Requirements:**
+- `time_lock = 0` on the Settings account
+- All signing keys present in the transaction
+- Combined permissions satisfy Initiate + Vote + Execute
+- Number of voters meets threshold
+
+**Flow:**
```
-yarn
+Single transaction with all signatures → Validate → Execute
```
-or
+This is the most gas-efficient path when coordination is possible. Ideal for automated systems, MPC wallets, or situations where all parties are online.
+
+## Policy Framework
+
+Policies are scoped permission sets with their own governance. Each policy has independent signers, threshold, and time lock from the main Settings account.
+**PDA Derivation:**
```
-npm install
+seeds = ["smart_account", "policy", settings_key, policy_seed]
```
-And run these tests with this command:
+**Account Structure:**
+```rust
+Policy {
+ settings: Pubkey, // Parent settings account
+ seed: u64, // Unique seed (auto-incremented)
+ bump: u8,
+ transaction_index: u64, // For stale protection
+ stale_transaction_index: u64,
+ signers: Vec, // Policy-specific signers
+ threshold: u16,
+ time_lock: u32,
+ policy_state: PolicyState, // Type-specific configuration
+ start: i64, // Activation timestamp
+ expiration: Option,
+ rent_collector: Pubkey, // Receives rent on close
+}
+```
+
+**Policy Types:**
+
+| Type | Purpose |
+|------|---------|
+| `SpendingLimit` | Token transfers with amount/time constraints |
+| `InternalFundTransfer` | Move funds between sub-accounts |
+| `ProgramInteraction` | Constrained external program calls (Smart Transactions) |
+| `SettingsChange` | Delegated configuration changes |
+
+**Expiration Modes:**
+- `Timestamp(i64)` - Expires at Unix timestamp
+- `SettingsState([u8; 32])` - Expires when settings hash changes (signers/threshold/time_lock modified)
+Policies are created via `SettingsAction::PolicyCreate`, `PolicyUpdate`, and `PolicyRemove`.
+
+### Program Interaction Policy (Smart Transactions)
+
+The Program Interaction Policy enables autonomous, rules-based transaction execution. Instead of deploying custom programs, you can define constraints that are enforced atomically onchain.
+
+**Why onchain rule enforcement matters:** Off-chain validation introduces a trust layer between intent and execution. If checks fail or are bypassed, transactions still execute. Onchain enforcement eliminates this since invalid rules make the transaction itself invalid.
+
+**Constraint Types:**
+
+```rust
+ProgramInteractionPolicy {
+ account_index: u8, // Which vault executes
+ instructions_constraints: Vec,
+ pre_hook: Option, // Called before execution
+ post_hook: Option, // Called after execution
+ spending_limits: Vec, // Balance tracking
+}
+
+InstructionConstraint {
+ program_id: Pubkey, // Allowed program
+ account_constraints: Vec, // Required accounts
+ data_constraints: Vec, // Instruction data rules
+}
```
-yarn test
+
+**What you can constrain:**
+- **Allowed programs** - Whitelist by program ID
+- **Allowed instructions** - Filter by discriminator
+- **Required accounts** - Mandate specific accounts in the transaction
+- **Account data validation** - Check balances, flags, pubkeys, numeric thresholds
+- **Spending limits** - Track and cap balance changes per period
+
+**Data Operators:**
+```rust
+DataOperator::Equals | NotEquals | GreaterThan | GreaterThanOrEqualTo | LessThan | LessThanOrEqualTo
```
-### Verifying the code
+**Use cases:** DCA strategies, yield routing, conditional escrow, agentic trading, automated treasury management.
+
+For the full vision, see [Smart Transactions: A Primitive for Autonomous Finance](https://squads.xyz/blog/grid-smart-transactions-a-primitive-for-autonomous-finance-on-solana).
-First, compile the programs code from the `Squads-Protocol/smart-account-program` Github repository to get its bytecode.
+#### Hooks (Pre & Post Execution)
+Hooks allow external programs to be invoked before and/or after the main transaction executes. This enables custom validation, logging, side effects, or integration with external protocols.
+
+```rust
+Hook {
+ program_id: Pubkey, // Program to invoke
+ num_extra_accounts: u8, // Additional accounts beyond program ID
+ account_constraints: Vec, // Constraints on hook accounts
+ instruction_data: Vec, // Data passed to the hook
+ pass_inner_instructions: bool, // Forward inner tx instructions to hook
+}
```
-git clone https://github.com/Squads-Protocol/smart-account-program.git
+
+**Execution flow:**
+```
+[Pre-Hook] → Constraint Validation → Execute Transaction → [Post-Hook]
```
+**Key features:**
+- **Pre-hooks** run before the main transaction - useful for validation, price checks, or state snapshots
+- **Post-hooks** run after execution - useful for logging, notifications, or post-condition verification
+- **`pass_inner_instructions`** - when true, the hook receives serialized inner instructions and their accounts, enabling the hook to inspect what's being executed
+- Hooks are invoked via CPI with a dedicated **hook authority** PDA as signer
+
+**Use cases:**
+- Oracle price validation before swaps
+- Compliance checks before transfers
+- Event emission for off-chain indexing
+- Post-execution accounting or reconciliation
+
+### Spending Limit Policy
+
+Token transfers with periodic limits and optional accumulation.
+
+```rust
+SpendingLimitPolicy {
+ source_account_index: u8,
+ destinations: Vec, // Empty = any destination
+ spending_limit: SpendingLimitV2 {
+ mint: Pubkey, // Token mint (default = SOL)
+ time_constraints: TimeConstraints {
+ start: i64,
+ period: Period, // Daily, Weekly, Monthly, etc.
+ expiration: Option,
+ accumulate_unused: bool, // Roll over unused amounts
+ },
+ quantity_constraints: QuantityConstraints {
+ max_per_period: u64,
+ max_per_use: u64,
+ enforce_exact_quantity: bool,
+ },
+ usage: UsageState {
+ remaining_in_period: u64,
+ last_reset: i64,
+ },
+ },
+}
```
+
+### Internal Fund Transfer Policy
+
+Move assets between sub-accounts within the same Smart Account.
+
+```rust
+InternalFundTransferPolicy {
+ source_account_mask: [u8; 32], // Bitmask of allowed source indices
+ destination_account_mask: [u8; 32], // Bitmask of allowed destination indices
+ allowed_mints: Vec, // Empty = any mint
+}
+```
+
+Uses bitmasks for efficient storage - supports all 256 possible sub-account indices.
+
+### Settings Change Policy
+
+Delegate specific configuration changes without full governance.
+
+```rust
+SettingsChangePolicy {
+ allowed_actions: Vec,
+}
+```
+
+Allows policies to modify Settings (add/remove signers, change threshold, etc.) within defined bounds.
+
+## Legacy Spending Limits
+
+Before the Policy Framework, spending limits were standalone accounts managed directly via Settings actions. These remain supported for backwards compatibility.
+
+**PDA Derivation:**
+```
+seeds = ["smart_account", settings_key, "spending_limit", seed]
+```
+
+```rust
+SpendingLimit {
+ settings: Pubkey,
+ seed: Pubkey, // User-provided seed for PDA
+ account_index: u8, // Which vault
+ mint: Pubkey, // Token mint (default = SOL)
+ amount: u64, // Max per period
+ period: Period, // OneTime, Day, Week, Month
+ remaining_amount: u64, // Current period remaining
+ last_reset: i64, // Last reset timestamp
+ bump: u8,
+ signers: Vec, // Any one can use the limit
+ destinations: Vec, // Allowed recipients (empty = any)
+ expiration: i64, // Expiration timestamp
+}
+```
+
+Created via `SettingsAction::AddSpendingLimit`, removed via `SettingsAction::RemoveSpendingLimit`.
+
+**Key differences from Spending Limit Policy:**
+- No independent governance (uses Settings signers)
+- Simpler structure (no time constraints, no accumulation)
+- User-provided seed vs auto-incremented
+
+## Developers
+
+You can interact with the Squads Smart Account Program via our SDKs.
+
+**SDKs:**
+- TypeScript: [`@sqds/smart-account`](https://www.npmjs.com/package/@sqds/smart-account)
+
+## Building
+
+This program requires Solana 1.18.16. Install it via Agave:
+
+```bash
+agave-install init 1.18.16
+```
+
+Compile the program with Anchor:
+
+```bash
anchor build
```
-Now, install the [Ellipsis Labs verifiable build](https://crates.io/crates/solana-verify) crate.
+If you don't have Anchor CLI installed, follow [this guide](https://www.anchor-lang.com/docs/installation).
+
+After building the program, rebuild the SDK to generate updated types:
+```bash
+yarn build
```
-cargo install solana-verify
+
+## Testing
+
+To deploy the program on a local validator for testing:
+
+```bash
+solana-test-validator
```
-Get the executable hash of the bytecode from the Squads program that was compiled.
+Install dependencies and run tests:
+```bash
+yarn
+yarn test
```
-solana-verify get-executable-hash target/deploy/squads_smart_account_program.so
+
+## Verifying the Program
+
+Clone and build the program:
+
+```bash
+git clone https://github.com/Squads-Protocol/smart-account-program.git
+anchor build
+```
+
+Install [solana-verify](https://crates.io/crates/solana-verify):
+
+```bash
+cargo install solana-verify
```
-Get the hash from the bytecode of the on-chain Squads program you want to verify.
+Get the hash of your locally compiled program:
+```bash
+solana-verify get-executable-hash target/deploy/squads_smart_account_program.so
```
-solana-verify get-program-hash -u SMRTe6bnZAgJmXt9aJin7XgAzDn1XMHGNy95QATyzpk
+
+Compare with the on-chain program:
+
+```bash
+solana-verify get-program-hash -u SMRTzfY6DfH5ik3TKiyLFfXexV8uSG3d2UksSCYdunG
```
-If the hash outputs of those two commands match, the code in the repository matches the on-chain programs code.
+If the hashes match, the repository code matches the deployed program.
+
+## Responsibility
+
+By interacting with this program, users acknowledge and accept full personal responsibility for any consequences, regardless of their nature. This includes both potential risks inherent to the smart contract, also referred to as program, as well as any losses resulting from user errors or misjudgment.
+
+By using a smart account, it is important to acknowledge certain concepts. Here are some that could be misunderstood by users:
+
+- Loss of Private Keys: If a participant loses their private key, the smart account may not be able to execute transactions if a threshold number of signatures is required.
+- Single Point of Failure with Keys: If all keys are stored in the same location or device, a single breach can compromise the smart account.
+- Forgetting the Threshold: Misremembering the number of signatures required can result in a deadlock, where funds cannot be accessed.
+- No Succession Planning: If keyholders become unavailable (e.g., due to accident, death), without a plan for transition, funds may be locked forever.
+- Transfer of funds to wrong address: Funds should always be sent to the smart account account, and not the smart account settingsaddress. Due to the design of the Squads Protocol program, funds deposited to the smart account may not be recoverable.
+- If the settings_authority of a smart account is compromised, an attacker can change smart account settings, potentially reducing the required threshold for transaction execution or instantly being able to remove and add new members.
+- If the underlying SVM compatible blockchain undergoes a fork and a user had sent funds to the orphaned chain, the state of the blockchain may not interpret the owner of funds to be original one.
+- Users might inadvertently set long or permanent time-locks in their smart account, preventing access to their funds for that period of time.
+- Smart account participants might not have enough of the native token of the underlying SVM blockchain to pay for transaction and state fees.
## Security
diff --git a/package.json b/package.json
index 404b0d6..c79e856 100644
--- a/package.json
+++ b/package.json
@@ -1,33 +1,34 @@
{
- "private": true,
- "workspaces": [
- "sdk/*"
- ],
- "scripts": {
- "build": "turbo run build",
- "test:detached": "turbo run build && anchor test --detach -- --features=testing && echo \"\n⚠️ Don't forget to recompile the .so file before deployment\n\"",
- "test": "turbo run build && anchor test -- --features=testing && echo \"\n⚠️ Don't forget to recompile the .so file before deployment\n\"",
- "pretest": "mkdir -p target/deploy && cp ./test-program-keypair.json ./target/deploy/squads_smart_account_program-keypair.json",
- "ts": "turbo run ts && yarn tsc --noEmit"
- },
- "devDependencies": {
- "@solana/spl-memo": "^0.2.3",
- "@solana/spl-token": "*",
- "@types/bn.js": "5.1.0",
- "@types/mocha": "10.0.1",
- "@types/node-fetch": "2.6.2",
- "mocha": "10.2.0",
- "prettier": "2.6.2",
- "ts-node": "10.9.1",
- "turbo": "1.6.3",
- "typescript": "*"
- },
- "resolutions": {
- "@solana/web3.js": "1.70.3",
- "@solana/spl-token": "0.3.6",
- "typescript": "4.9.4"
- },
- "dependencies": {
- "@solana/web3.js": "^1.95.5"
- }
+ "private": true,
+ "workspaces": [
+ "sdk/*"
+ ],
+ "scripts": {
+ "build": "turbo run build",
+ "test:detached": "turbo run build && anchor test --detach -- --features=testing && echo \"\n⚠️ Don't forget to recompile the .so file before deployment\n\"",
+ "test:detached:nb": "anchor test --detach -- --features=testing && echo \"\n⚠️ Don't forget to recompile the .so file before deployment\n\"",
+ "test": "turbo run build && anchor test -- --features=testing && echo \"\n⚠️ Don't forget to recompile the .so file before deployment\n\"",
+ "pretest": "mkdir -p target/deploy && cp ./test-program-keypair.json ./target/deploy/squads_smart_account_program-keypair.json",
+ "ts": "turbo run ts && yarn tsc --noEmit"
+ },
+ "devDependencies": {
+ "@solana/spl-memo": "^0.2.3",
+ "@solana/spl-token": "*",
+ "@types/mocha": "10.0.1",
+ "@types/node-fetch": "2.6.2",
+ "mocha": "10.2.0",
+ "prettier": "2.6.2",
+ "ts-node": "10.9.1",
+ "turbo": "1.6.3",
+ "typescript": "*"
+ },
+ "resolutions": {
+ "@solana/web3.js": "1.70.3",
+ "@solana/spl-token": "0.3.6",
+ "typescript": "4.9.4"
+ },
+ "dependencies": {
+ "@solana/web3.js": "^1.95.5"
+ },
+ "packageManager": "yarn@1.22.22"
}
diff --git a/programs/squads_smart_account_program/src/allocator.rs b/programs/squads_smart_account_program/src/allocator.rs
index 61c7128..c0baad2 100644
--- a/programs/squads_smart_account_program/src/allocator.rs
+++ b/programs/squads_smart_account_program/src/allocator.rs
@@ -129,5 +129,6 @@ unsafe impl std::alloc::GlobalAlloc for BumpAllocator {
// Only use the allocator if we're not in a no-entrypoint context
#[cfg(not(feature = "no-entrypoint"))]
+#[cfg(not(test))]
#[global_allocator]
static A: BumpAllocator = BumpAllocator;
diff --git a/programs/squads_smart_account_program/src/errors.rs b/programs/squads_smart_account_program/src/errors.rs
index 5596594..cf177a8 100644
--- a/programs/squads_smart_account_program/src/errors.rs
+++ b/programs/squads_smart_account_program/src/errors.rs
@@ -60,8 +60,6 @@ pub enum SmartAccountError {
SpendingLimitExceeded,
#[msg("Decimals don't match the mint")]
DecimalsMismatch,
- #[msg("Spending limit is expired")]
- SpendingLimitExpired,
#[msg("Signer has unknown permission")]
UnknownPermission,
#[msg("Account is protected, it cannot be passed into a CPI as writable")]
@@ -110,4 +108,186 @@ pub enum SmartAccountError {
TimeLockNotZero,
#[msg("Feature not implemented")]
NotImplemented,
+ #[msg("Invalid cadence configuration")]
+ SpendingLimitInvalidCadenceConfiguration,
+ #[msg("Invalid data constraint")]
+ InvalidDataConstraint,
+
+
+ #[msg("Invalid payload")]
+ InvalidPayload,
+ #[msg("Protected instruction")]
+ ProtectedInstruction,
+ #[msg("Placeholder error")]
+ PlaceholderError,
+
+ // ===============================================
+ // Overall Policy Errors
+ // ===============================================
+ #[msg("Invalid policy payload")]
+ InvalidPolicyPayload,
+ #[msg("Invalid empty policy")]
+ InvalidEmptyPolicy,
+ #[msg("Transaction is for another policy")]
+ TransactionForAnotherPolicy,
+
+ // ===============================================
+ // Program Interaction Policy Errors
+ // ===============================================
+ #[msg("Program interaction sync payload not allowed with async transaction")]
+ ProgramInteractionAsyncPayloadNotAllowedWithSyncTransaction,
+ #[msg("Program interaction sync payload not allowed with sync transaction")]
+ ProgramInteractionSyncPayloadNotAllowedWithAsyncTransaction,
+ #[msg("Program interaction data constraint failed: instruction data too short")]
+ ProgramInteractionDataTooShort,
+ #[msg("Program interaction data constraint failed: invalid numeric value")]
+ ProgramInteractionInvalidNumericValue,
+ #[msg("Program interaction data constraint failed: invalid byte sequence")]
+ ProgramInteractionInvalidByteSequence,
+ #[msg("Program interaction data constraint failed: unsupported operator for byte slice")]
+ ProgramInteractionUnsupportedSliceOperator,
+ #[msg("Program interaction constraint failed: instruction data parsing error")]
+ ProgramInteractionDataParsingError,
+ #[msg("Program interaction constraint failed: program ID mismatch")]
+ ProgramInteractionProgramIdMismatch,
+ #[msg("Program interaction constraint violation: account constraint")]
+ ProgramInteractionAccountConstraintViolated,
+ #[msg("Program interaction constraint violation: instruction constraint index out of bounds")]
+ ProgramInteractionConstraintIndexOutOfBounds,
+ #[msg("Program interaction constraint violation: instruction count mismatch")]
+ ProgramInteractionInstructionCountMismatch,
+ #[msg("Program interaction constraint violation: insufficient remaining lamport allowance")]
+ ProgramInteractionInsufficientLamportAllowance,
+ #[msg("Program interaction constraint violation: insufficient remaining token allowance")]
+ ProgramInteractionInsufficientTokenAllowance,
+ #[msg("Program interaction constraint violation: modified illegal balance")]
+ ProgramInteractionModifiedIllegalBalance,
+ #[msg("Program interaction constraint violation: illegal token account modification")]
+ ProgramInteractionIllegalTokenAccountModification,
+ #[msg("Program interaction invariant violation: duplicate spending limit for the same mint")]
+ ProgramInteractionDuplicateSpendingLimit,
+ #[msg("Program interaction constraint violation: too many instruction constraints. Max is 20")]
+ ProgramInteractionTooManyInstructionConstraints,
+ #[msg("Program interaction constraint violation: too many spending limits. Max is 10")]
+ ProgramInteractionTooManySpendingLimits,
+ #[msg("Program interaction constraint violation: invalid pubkey table index")]
+ ProgramInteractionInvalidPubkeyTableIndex,
+ #[msg("Program interaction constraint violation: too many unique pubkeys. Max is 240 (indices 240-255 reserved for builtin programs)")]
+ ProgramInteractionTooManyUniquePubkeys,
+ #[msg("Program interaction hook violation: template hook error")]
+ ProgramInteractionTemplateHookError,
+ #[msg("Program interaction hook violation: hook authority cannot be part of hook accounts")]
+ ProgramInteractionHookAuthorityCannotBePartOfHookAccounts,
+
+ // ===============================================
+ // Spending Limit Policy Errors
+ // ===============================================
+ #[msg("Spending limit is not active")]
+ SpendingLimitNotActive,
+ #[msg("Spending limit is expired")]
+ SpendingLimitExpired,
+ #[msg("Spending limit policy invariant violation: usage state cannot be Some() if accumulate_unused is true")]
+ SpendingLimitPolicyInvariantAccumulateUnused,
+ #[msg("Amount violates exact quantity constraint")]
+ SpendingLimitViolatesExactQuantityConstraint,
+ #[msg("Amount violates max per use constraint")]
+ SpendingLimitViolatesMaxPerUseConstraint,
+ #[msg("Spending limit is insufficient")]
+ SpendingLimitInsufficientRemainingAmount,
+ #[msg("Spending limit invariant violation: max per period must be non-zero")]
+ SpendingLimitInvariantMaxPerPeriodZero,
+ #[msg("Spending limit invariant violation: start time must be positive")]
+ SpendingLimitInvariantStartTimePositive,
+ #[msg("Spending limit invariant violation: expiration must be greater than start")]
+ SpendingLimitInvariantExpirationSmallerThanStart,
+ #[msg("Spending limit invariant violation: overflow enabled must have expiration")]
+ SpendingLimitInvariantOverflowEnabledMustHaveExpiration,
+ #[msg("Spending limit invariant violation: one time period cannot have overflow enabled")]
+ SpendingLimitInvariantOneTimePeriodCannotHaveOverflowEnabled,
+ #[msg("Spending limit invariant violation: remaining amount must be less than max amount")]
+ SpendingLimitInvariantOverflowRemainingAmountGreaterThanMaxAmount,
+ #[msg("Spending limit invariant violation: remaining amount must be less than or equal to max per period")]
+ SpendingLimitInvariantRemainingAmountGreaterThanMaxPerPeriod,
+ #[msg("Spending limit invariant violation: exact quantity must have max per use non-zero")]
+ SpendingLimitInvariantExactQuantityMaxPerUseZero,
+ #[msg("Spending limit invariant violation: max per use must be less than or equal to max per period")]
+ SpendingLimitInvariantMaxPerUseGreaterThanMaxPerPeriod,
+ #[msg("Spending limit invariant violation: custom period must be positive")]
+ SpendingLimitInvariantCustomPeriodNegative,
+ #[msg("Spending limit policy invariant violation: cannot have duplicate destinations for the same mint")]
+ SpendingLimitPolicyInvariantDuplicateDestinations,
+ #[msg("Spending limit invariant violation: last reset must be between start and expiration")]
+ SpendingLimitInvariantLastResetOutOfBounds,
+ #[msg("Spending limit invariant violation: last reset must be greater than start")]
+ SpendingLimitInvariantLastResetSmallerThanStart,
+
+ // ===============================================
+ // Internal Fund Transfer Policy Errors
+ // ===============================================
+ #[msg("Internal fund transfer policy invariant violation: source account index is not allowed")]
+ InternalFundTransferPolicyInvariantSourceAccountIndexNotAllowed,
+ #[msg("Internal fund transfer policy invariant violation: destination account index is not allowed")]
+ InternalFundTransferPolicyInvariantDestinationAccountIndexNotAllowed,
+ #[msg("Internal fund transfer policy invariant violation: source and destination cannot be the same")]
+ InternalFundTransferPolicyInvariantSourceAndDestinationCannotBeTheSame,
+ #[msg("Internal fund transfer policy invariant violation: mint is not allowed")]
+ InternalFundTransferPolicyInvariantMintNotAllowed,
+ #[msg("Internal fund transfer policy invariant violation: amount must be greater than 0")]
+ InternalFundTransferPolicyInvariantAmountZero,
+ #[msg("Internal fund transfer policy invariant violation: cannot have duplicate mints")]
+ InternalFundTransferPolicyInvariantDuplicateMints,
+
+ // ===============================================
+ // Consensus Account Errors
+ // ===============================================
+ #[msg("Consensus account is not a settings")]
+ ConsensusAccountNotSettings,
+ #[msg("Consensus account is not a policy")]
+ ConsensusAccountNotPolicy,
+
+ // ===============================================
+ // Settings Change Policy Errors
+ // ===============================================
+ #[msg("Settings change policy invariant violation: actions must be non-zero")]
+ SettingsChangePolicyActionsMustBeNonZero,
+ #[msg("Settings change policy violation: submitted settings account must match policy settings key")]
+ SettingsChangeInvalidSettingsKey,
+ #[msg("Settings change policy violation: submitted settings account must be writable")]
+ SettingsChangeInvalidSettingsAccount,
+ #[msg("Settings change policy violation: rent payer must be writable and signer")]
+ SettingsChangeInvalidRentPayer,
+ #[msg("Settings change policy violation: system program must be the system program")]
+ SettingsChangeInvalidSystemProgram,
+ #[msg("Settings change policy violation: signer does not match allowed signer")]
+ SettingsChangeAddSignerViolation,
+ #[msg("Settings change policy violation: signer permissions does not match allowed signer permissions")]
+ SettingsChangeAddSignerPermissionsViolation,
+ #[msg("Settings change policy violation: signer removal does not mach allowed signer removal")]
+ SettingsChangeRemoveSignerViolation,
+ #[msg("Settings change policy violation: time lock does not match allowed time lock")]
+ SettingsChangeChangeTimelockViolation,
+ #[msg("Settings change policy violation: action does not match allowed action")]
+ SettingsChangeActionMismatch,
+ #[msg("Settings change policy invariant violation: cannot have duplicate actions")]
+ SettingsChangePolicyInvariantDuplicateActions,
+ #[msg("Settings change policy invariant violation: action indices must match actions length")]
+ SettingsChangePolicyInvariantActionIndicesActionsLengthMismatch,
+ #[msg("Settings change policy invariant violation: action index out of bounds")]
+ SettingsChangePolicyInvariantActionIndexOutOfBounds,
+
+ // ===============================================
+ // Policy Expiration Errors
+ // ===============================================
+ #[msg("Policy is not active yet")]
+ PolicyNotActiveYet,
+ #[msg("Policy invariant violation: invalid policy expiration")]
+ PolicyInvariantInvalidExpiration,
+ #[msg("Policy expiration violation: submitted settings key does not match policy settings key")]
+ PolicyExpirationViolationPolicySettingsKeyMismatch,
+ #[msg("Policy expiration violation: state expiration requires the settings to be submitted")]
+ PolicyExpirationViolationSettingsAccountNotPresent,
+ #[msg("Policy expiration violation: state hash has expired")]
+ PolicyExpirationViolationHashExpired,
+ #[msg("Policy expiration violation: timestamp has expired")]
+ PolicyExpirationViolationTimestampExpired,
}
diff --git a/programs/squads_smart_account_program/src/events/account_events.rs b/programs/squads_smart_account_program/src/events/account_events.rs
index 8f90125..8d689f1 100644
--- a/programs/squads_smart_account_program/src/events/account_events.rs
+++ b/programs/squads_smart_account_program/src/events/account_events.rs
@@ -1,15 +1,36 @@
+use crate::{
+ consensus_trait::ConsensusAccountType, state::SettingsAction, LimitedSettingsAction, Policy,
+ PolicyPayload, Proposal, Settings, SettingsTransaction, SmartAccountCompiledInstruction,
+ SpendingLimit, Transaction,
+};
use anchor_lang::prelude::*;
use borsh::{BorshDeserialize, BorshSerialize};
-use crate::{state::SettingsAction, Settings, SmartAccountCompiledInstruction, SmartAccountSigner, SpendingLimit};
-
-
#[derive(BorshSerialize, BorshDeserialize)]
pub struct CreateSmartAccountEvent {
pub new_settings_pubkey: Pubkey,
pub new_settings_content: Settings,
}
+#[derive(BorshSerialize, BorshDeserialize)]
+pub struct SynchronousTransactionEventV2 {
+ pub consensus_account: Pubkey,
+ pub consensus_account_type: ConsensusAccountType,
+ pub signers: Vec,
+ pub payload: SynchronousTransactionEventPayload,
+ pub instruction_accounts: Vec,
+}
+#[derive(BorshSerialize, BorshDeserialize)]
+pub enum SynchronousTransactionEventPayload {
+ TransactionPayload {
+ account_index: u8,
+ instructions: Vec,
+ },
+ PolicyPayload {
+ policy_payload: PolicyPayload,
+ },
+}
+
#[derive(BorshSerialize, BorshDeserialize)]
pub struct SynchronousTransactionEvent {
pub settings_pubkey: Pubkey,
@@ -19,13 +40,12 @@ pub struct SynchronousTransactionEvent {
pub instruction_accounts: Vec,
}
-
#[derive(BorshSerialize, BorshDeserialize)]
pub struct SynchronousSettingsTransactionEvent {
pub settings_pubkey: Pubkey,
pub signers: Vec,
pub settings: Settings,
- pub changes: Vec
+ pub changes: Vec,
}
#[derive(BorshSerialize, BorshDeserialize)]
@@ -41,6 +61,22 @@ pub struct RemoveSpendingLimitEvent {
pub spending_limit_pubkey: Pubkey,
}
+#[derive(BorshSerialize, BorshDeserialize)]
+pub struct PolicyEvent {
+ pub event_type: PolicyEventType,
+ pub settings_pubkey: Pubkey,
+ pub policy_pubkey: Pubkey,
+ pub policy: Option,
+}
+
+#[derive(BorshSerialize, BorshDeserialize)]
+pub enum PolicyEventType {
+ Create,
+ Update,
+ UpdateDuringExecution,
+ Remove,
+}
+
#[derive(BorshSerialize, BorshDeserialize)]
pub struct UseSpendingLimitEvent {
pub settings_pubkey: Pubkey,
@@ -61,7 +97,7 @@ pub struct AuthoritySettingsEvent {
pub settings: Settings,
pub settings_pubkey: Pubkey,
pub authority: Pubkey,
- pub change: SettingsAction
+ pub change: SettingsAction,
}
#[derive(BorshSerialize, BorshDeserialize)]
@@ -69,5 +105,63 @@ pub struct AuthorityChangeEvent {
pub settings: Settings,
pub settings_pubkey: Pubkey,
pub authority: Pubkey,
- pub new_authority: Option
-}
\ No newline at end of file
+ pub new_authority: Option,
+}
+
+#[derive(BorshSerialize, BorshDeserialize)]
+pub struct TransactionEvent {
+ pub consensus_account: Pubkey,
+ pub consensus_account_type: ConsensusAccountType,
+ pub event_type: TransactionEventType,
+ pub transaction_pubkey: Pubkey,
+ pub transaction_index: u64,
+ pub signer: Option,
+ pub memo: Option,
+ pub transaction_content: Option,
+}
+
+#[derive(BorshSerialize, BorshDeserialize)]
+pub enum TransactionContent {
+ Transaction(Transaction),
+ SettingsTransaction {
+ settings: Settings,
+ transaction: SettingsTransaction,
+ changes: Vec,
+ },
+}
+
+#[derive(BorshSerialize, BorshDeserialize)]
+pub enum TransactionEventType {
+ Create,
+ Execute,
+ Close,
+}
+
+#[derive(BorshSerialize, BorshDeserialize)]
+pub struct ProposalEvent {
+ pub consensus_account: Pubkey,
+ pub consensus_account_type: ConsensusAccountType,
+ pub event_type: ProposalEventType,
+ pub proposal_pubkey: Pubkey,
+ pub transaction_index: u64,
+ pub signer: Option,
+ pub memo: Option,
+ pub proposal: Option,
+}
+
+#[derive(BorshSerialize, BorshDeserialize)]
+pub enum ProposalEventType {
+ Create,
+ Approve,
+ Reject,
+ Cancel,
+ Execute,
+ Close,
+}
+
+#[derive(BorshSerialize, BorshDeserialize)]
+pub struct SettingsChangePolicyEvent {
+ pub settings_pubkey: Pubkey,
+ pub settings: Settings,
+ pub changes: Vec,
+}
diff --git a/programs/squads_smart_account_program/src/events/mod.rs b/programs/squads_smart_account_program/src/events/mod.rs
index 63693a2..d4f260e 100644
--- a/programs/squads_smart_account_program/src/events/mod.rs
+++ b/programs/squads_smart_account_program/src/events/mod.rs
@@ -2,7 +2,7 @@ use anchor_lang::{
prelude::borsh::*, prelude::*, solana_program::program::invoke_signed, Discriminator,
};
-use crate::LogEventArgs;
+use crate::{LogEventArgsV2};
pub mod account_events;
pub use account_events::*;
@@ -16,7 +16,12 @@ pub enum SmartAccountEvent {
RemoveSpendingLimitEvent(RemoveSpendingLimitEvent),
UseSpendingLimitEvent(UseSpendingLimitEvent),
AuthoritySettingsEvent(AuthoritySettingsEvent),
- AuthorityChangeEvent(AuthorityChangeEvent)
+ AuthorityChangeEvent(AuthorityChangeEvent),
+ TransactionEvent(TransactionEvent),
+ ProposalEvent(ProposalEvent),
+ SynchronousTransactionEventV2(SynchronousTransactionEventV2),
+ SettingsChangePolicyEvent(SettingsChangePolicyEvent),
+ PolicyEvent(PolicyEvent),
}
pub struct LogAuthorityInfo<'info> {
pub authority: AccountInfo<'info>,
@@ -34,13 +39,11 @@ impl SmartAccountEvent {
let bump_slice = &[authority_info.bump];
signer_seeds.push(bump_slice);
- let data = LogEventArgs {
- account_seeds: authority_info.authority_seeds.clone(),
- bump: authority_info.bump,
+ let data = LogEventArgsV2 {
event: self.try_to_vec()?,
};
let mut instruction_data =
- Vec::with_capacity(8 + 4 + authority_info.authority_seeds.len() + 4 + data.event.len());
+ Vec::with_capacity(8 + 4 + data.event.len());
instruction_data.extend_from_slice(&crate::instruction::LogEvent::DISCRIMINATOR);
instruction_data.extend_from_slice(&data.try_to_vec()?);
diff --git a/programs/squads_smart_account_program/src/instructions/activate_proposal.rs b/programs/squads_smart_account_program/src/instructions/activate_proposal.rs
index 3b547d2..aa3a86c 100644
--- a/programs/squads_smart_account_program/src/instructions/activate_proposal.rs
+++ b/programs/squads_smart_account_program/src/instructions/activate_proposal.rs
@@ -1,5 +1,6 @@
use anchor_lang::prelude::*;
+use crate::consensus_trait::Consensus;
use crate::errors::*;
use crate::state::*;
diff --git a/programs/squads_smart_account_program/src/instructions/authority_settings_transaction_execute.rs b/programs/squads_smart_account_program/src/instructions/authority_settings_transaction_execute.rs
index 5b562eb..6eecb9e 100644
--- a/programs/squads_smart_account_program/src/instructions/authority_settings_transaction_execute.rs
+++ b/programs/squads_smart_account_program/src/instructions/authority_settings_transaction_execute.rs
@@ -1,8 +1,7 @@
use anchor_lang::prelude::*;
use crate::{
- errors::*, program::SquadsSmartAccountProgram, state::*, AuthorityChangeEvent,
- AuthoritySettingsEvent, LogAuthorityInfo, SmartAccountEvent,
+ consensus_trait::Consensus, errors::*, program::SquadsSmartAccountProgram, state::*, AuthorityChangeEvent, AuthoritySettingsEvent, LogAuthorityInfo, SmartAccountEvent
};
#[derive(AnchorSerialize, AnchorDeserialize)]
diff --git a/programs/squads_smart_account_program/src/instructions/authority_spending_limit_add.rs b/programs/squads_smart_account_program/src/instructions/authority_spending_limit_add.rs
index df4c775..bb6a792 100644
--- a/programs/squads_smart_account_program/src/instructions/authority_spending_limit_add.rs
+++ b/programs/squads_smart_account_program/src/instructions/authority_spending_limit_add.rs
@@ -1,8 +1,8 @@
use anchor_lang::prelude::*;
use crate::program::SquadsSmartAccountProgram;
-use crate::{state::*, LogAuthorityInfo, SmartAccountEvent};
use crate::{errors::*, AuthoritySettingsEvent};
+use crate::{state::*, LogAuthorityInfo, SmartAccountEvent};
#[derive(AnchorSerialize, AnchorDeserialize)]
pub struct AddSpendingLimitArgs {
diff --git a/programs/squads_smart_account_program/src/instructions/batch_add_transaction.rs b/programs/squads_smart_account_program/src/instructions/batch_add_transaction.rs
index 99c4667..241bea2 100644
--- a/programs/squads_smart_account_program/src/instructions/batch_add_transaction.rs
+++ b/programs/squads_smart_account_program/src/instructions/batch_add_transaction.rs
@@ -1,5 +1,5 @@
use anchor_lang::prelude::*;
-
+use crate::consensus_trait::Consensus;
use crate::errors::*;
use crate::state::*;
use crate::TransactionMessage;
@@ -17,7 +17,7 @@ pub struct AddTransactionToBatch<'info> {
/// Settings account this batch belongs to.
#[account(
seeds = [SEED_PREFIX, SEED_SETTINGS, settings.seed.to_le_bytes().as_ref()],
- bump = settings.bump,
+ bump
)]
pub settings: Account<'info, Settings>,
diff --git a/programs/squads_smart_account_program/src/instructions/batch_create.rs b/programs/squads_smart_account_program/src/instructions/batch_create.rs
index 61c5c48..8a3c262 100644
--- a/programs/squads_smart_account_program/src/instructions/batch_create.rs
+++ b/programs/squads_smart_account_program/src/instructions/batch_create.rs
@@ -1,5 +1,6 @@
use anchor_lang::prelude::*;
+use crate::consensus_trait::Consensus;
use crate::errors::*;
use crate::state::*;
@@ -15,7 +16,7 @@ pub struct CreateBatch<'info> {
#[account(
mut,
seeds = [SEED_PREFIX, SEED_SETTINGS, settings.seed.to_le_bytes().as_ref()],
- bump = settings.bump,
+ bump
)]
pub settings: Account<'info, Settings>,
@@ -46,7 +47,9 @@ pub struct CreateBatch<'info> {
impl CreateBatch<'_> {
fn validate(&self) -> Result<()> {
let Self {
- settings, creator, ..
+ settings,
+ creator,
+ ..
} = self;
// creator
@@ -72,7 +75,10 @@ impl CreateBatch<'_> {
let settings_key = settings.key();
// Increment the transaction index.
- let index = settings.transaction_index.checked_add(1).expect("overflow");
+ let index = settings
+ .transaction_index
+ .checked_add(1)
+ .expect("overflow");
let smart_account_seeds = &[
SEED_PREFIX,
@@ -95,8 +101,8 @@ impl CreateBatch<'_> {
batch.invariant()?;
- // Updated last transaction index in the settings account.
- settings.transaction_index = index;
+ // Updated last transaction index in the consensus account.
+ settings.set_transaction_index(index)?;
settings.invariant()?;
diff --git a/programs/squads_smart_account_program/src/instructions/batch_execute_transaction.rs b/programs/squads_smart_account_program/src/instructions/batch_execute_transaction.rs
index 8375c05..686f9fd 100644
--- a/programs/squads_smart_account_program/src/instructions/batch_execute_transaction.rs
+++ b/programs/squads_smart_account_program/src/instructions/batch_execute_transaction.rs
@@ -1,5 +1,6 @@
use anchor_lang::prelude::*;
+use crate::consensus_trait::Consensus;
use crate::errors::*;
use crate::state::*;
use crate::utils::*;
@@ -9,7 +10,7 @@ pub struct ExecuteBatchTransaction<'info> {
/// Settings account this batch belongs to.
#[account(
seeds = [SEED_PREFIX, SEED_SETTINGS, settings.seed.to_le_bytes().as_ref()],
- bump = settings.bump,
+ bump
)]
pub settings: Account<'info, Settings>,
diff --git a/programs/squads_smart_account_program/src/instructions/log_event.rs b/programs/squads_smart_account_program/src/instructions/log_event.rs
index 3d36990..a38e441 100644
--- a/programs/squads_smart_account_program/src/instructions/log_event.rs
+++ b/programs/squads_smart_account_program/src/instructions/log_event.rs
@@ -1,35 +1,130 @@
-use anchor_lang::prelude::*;
+use crate::{
+ instruction::LogEvent as LogEventInstruction, state::ProgramConfig,
+ Policy, Proposal, Settings, SettingsTransaction, Transaction,
+};
+use anchor_lang::{prelude::*, Discriminator};
+use solana_program::instruction::Instruction;
+use crate::errors::SmartAccountError;
+
+// ===== Log Event Instruction =====
+// This custom log instruction saves bytes by not requiring a custom event authority.
+// Instead it relies on the log authority to be some signing account with non-zero data in
+// it owned by our program.
+//
+// This means even if you assign a random keypair to the smart account via
+// `assign` and `allocate`, it won't be able to log events as it's data will be
+// either empty, or zero-initialized to its data length
+// ================================
+
+// Allowed discriminators for logging events
+const ALLOWED_DISCRIMINATORS: [[u8; 8]; 6] = [
+ Settings::DISCRIMINATOR,
+ Policy::DISCRIMINATOR,
+ Proposal::DISCRIMINATOR,
+ Transaction::DISCRIMINATOR,
+ SettingsTransaction::DISCRIMINATOR,
+ ProgramConfig::DISCRIMINATOR,
+];
+
+// Legacy log event instruction args.
+#[allow(dead_code)]
#[derive(AnchorSerialize, AnchorDeserialize, Debug)]
pub struct LogEventArgs {
pub account_seeds: Vec>,
pub bump: u8,
pub event: Vec,
}
+#[derive(AnchorSerialize, AnchorDeserialize, Debug)]
+pub struct LogEventArgsV2 {
+ pub event: Vec,
+}
#[derive(Accounts)]
-#[instruction(args: LogEventArgs)]
+#[instruction(args: LogEventArgsV2)]
pub struct LogEvent<'info> {
#[account(
- // Any account owned by the Smart Account Program, except for individual smart accounts, should be able to
- // log data. Smart accounts are the only accounts owned by the System Program.
+ // Any Account owned by our program can log, as long as it's discriminator is allowed
+ constraint = Self::validate_log_authority(&log_authority).is_ok() @ SmartAccountError::ProtectedInstruction,
owner = crate::id(),
)]
pub log_authority: Signer<'info>,
}
impl<'info> LogEvent<'info> {
- fn validate(&self, args: &LogEventArgs) -> Result<()> {
- let mut collected_seeds: Vec<&[u8]> =
- args.account_seeds.iter().map(|v| v.as_slice()).collect();
- let bump_slice = &[args.bump];
- collected_seeds.push(bump_slice);
-
- let derived_address =
- Pubkey::create_program_address(collected_seeds.as_slice(), &crate::ID).unwrap();
- assert_eq!(&derived_address, self.log_authority.key);
+ pub fn log_event(
+ _ctx: Context<'_, '_, 'info, 'info, Self>,
+ _args: LogEventArgsV2,
+ ) -> Result<()> {
+ Ok(())
+ }
+}
+
+impl<'info> LogEvent<'info> {
+ // Validates that a given log authority is an account that has actual data
+ // in it. I.e has to have been mutated by the smart account program.
+ pub fn validate_log_authority(log_authority: &Signer<'info>) -> Result<()> {
+ // Get the data of the log authority
+ let data = log_authority.try_borrow_data()?;
+
+ // Extract the discriminator
+ let discriminator: &[u8; 8] = data
+ .get(0..8)
+ .and_then(|slice| slice.try_into().ok())
+ .ok_or(SmartAccountError::ProtectedInstruction)?;
+
+ // Check if the discriminator is allowed
+ require!(
+ ALLOWED_DISCRIMINATORS.contains(discriminator),
+ SmartAccountError::ProtectedInstruction
+ );
+
Ok(())
}
- #[access_control(ctx.accounts.validate(&args))]
- pub fn log_event(ctx: Context<'_, '_, 'info, 'info, Self>, args: LogEventArgs) -> Result<()> {
+ // Util fn to help check we're not invoking the log event instruction
+ // from our own program during arbitrary instruction execution.
+ pub fn check_instruction(ix: &Instruction) -> Result<()> {
+ // Make sure we're not calling self logging instruction
+ if ix.program_id == crate::ID {
+ if let Some(discriminator) = ix.data.get(0..8) {
+ // Check if the discriminator is the log event discriminator
+ require!(
+ discriminator != LogEventInstruction::DISCRIMINATOR,
+ SmartAccountError::ProtectedInstruction
+ );
+ }
+ }
Ok(())
}
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_check_instruction() {
+ // Invalid instruction
+ let ix = Instruction {
+ program_id: crate::ID,
+ accounts: vec![],
+ data: vec![0x05, 0x09, 0x5a, 0x8d, 0xdf, 0x86, 0x39, 0xd9],
+ };
+ assert!(LogEvent::check_instruction(&ix).is_err());
+
+ // Valid instruction
+ let ix = Instruction {
+ // Valid since we're not calling our program
+ program_id: Pubkey::default(),
+ accounts: vec![],
+ data: vec![0x05, 0x09, 0x5a, 0x8d, 0xdf, 0x86, 0x39, 0xd9],
+ };
+ assert!(LogEvent::check_instruction(&ix).is_ok());
+
+ // Valid instruction since we're not calling the log event instruction
+ let ix = Instruction {
+ program_id: crate::ID,
+ accounts: vec![],
+ data: vec![0x05, 0x09, 0x5a, 0x8d, 0xdf, 0x86, 0x39, 0x39],
+ };
+ assert!(LogEvent::check_instruction(&ix).is_ok());
+ }
+}
diff --git a/programs/squads_smart_account_program/src/instructions/mod.rs b/programs/squads_smart_account_program/src/instructions/mod.rs
index 6b589e5..07a1f8f 100644
--- a/programs/squads_smart_account_program/src/instructions/mod.rs
+++ b/programs/squads_smart_account_program/src/instructions/mod.rs
@@ -1,51 +1,53 @@
pub use activate_proposal::*;
+pub use authority_settings_transaction_execute::*;
+pub use authority_spending_limit_add::*;
+pub use authority_spending_limit_remove::*;
pub use batch_add_transaction::*;
pub use batch_create::*;
pub use batch_execute_transaction::*;
+pub use log_event::*;
+pub use program_config_change::*;
+pub use program_config_init::*;
pub use proposal_create::*;
+pub use proposal_vote::*;
pub use settings_transaction_create::*;
-pub use smart_account_create::*;
-pub use transaction_create::*;
pub use settings_transaction_execute::*;
-pub use transaction_execute::*;
-pub use program_config_init::*;
-pub use authority_spending_limit_add::*;
-pub use authority_settings_transaction_execute::*;
-pub use authority_spending_limit_remove::*;
-pub use program_config_change::*;
-pub use proposal_vote::*;
-pub use use_spending_limit::*;
pub use settings_transaction_sync::*;
-pub use transaction_close::*;
+pub use smart_account_create::*;
pub use transaction_buffer_close::*;
pub use transaction_buffer_create::*;
pub use transaction_buffer_extend::*;
+pub use transaction_close::*;
+pub use transaction_create::*;
pub use transaction_create_from_buffer::*;
+pub use transaction_execute::*;
pub use transaction_execute_sync::*;
-pub use log_event::*;
+pub use transaction_execute_sync_legacy::*;
+pub use use_spending_limit::*;
mod activate_proposal;
+mod authority_settings_transaction_execute;
+mod authority_spending_limit_add;
+mod authority_spending_limit_remove;
mod batch_add_transaction;
mod batch_create;
mod batch_execute_transaction;
+mod log_event;
+mod program_config_change;
+mod program_config_init;
mod proposal_create;
+mod proposal_vote;
mod settings_transaction_create;
-mod smart_account_create;
-mod transaction_create;
mod settings_transaction_execute;
-mod transaction_execute;
-mod program_config_init;
-mod authority_spending_limit_add;
-mod authority_settings_transaction_execute;
-mod authority_spending_limit_remove;
-mod program_config_change;
-mod proposal_vote;
-mod use_spending_limit;
mod settings_transaction_sync;
-mod transaction_close;
+mod smart_account_create;
mod transaction_buffer_close;
mod transaction_buffer_create;
mod transaction_buffer_extend;
+mod transaction_close;
+mod transaction_create;
mod transaction_create_from_buffer;
+mod transaction_execute;
mod transaction_execute_sync;
-mod log_event;
\ No newline at end of file
+mod transaction_execute_sync_legacy;
+mod use_spending_limit;
diff --git a/programs/squads_smart_account_program/src/instructions/proposal_create.rs b/programs/squads_smart_account_program/src/instructions/proposal_create.rs
index f71b983..782e610 100644
--- a/programs/squads_smart_account_program/src/instructions/proposal_create.rs
+++ b/programs/squads_smart_account_program/src/instructions/proposal_create.rs
@@ -1,6 +1,10 @@
use anchor_lang::prelude::*;
+use crate::consensus_trait::Consensus;
use crate::errors::*;
+use crate::interface::consensus::ConsensusAccount;
+use crate::events::*;
+use crate::program::SquadsSmartAccountProgram;
use crate::state::*;
#[derive(AnchorSerialize, AnchorDeserialize)]
@@ -15,18 +19,17 @@ pub struct CreateProposalArgs {
#[instruction(args: CreateProposalArgs)]
pub struct CreateProposal<'info> {
#[account(
- seeds = [SEED_PREFIX, SEED_SETTINGS, settings.seed.to_le_bytes().as_ref()],
- bump = settings.bump,
+ constraint = consensus_account.check_derivation(consensus_account.key()).is_ok()
)]
- pub settings: Account<'info, Settings>,
+ pub consensus_account: InterfaceAccount<'info, ConsensusAccount>,
#[account(
init,
payer = rent_payer,
- space = Proposal::size(settings.signers.len()),
+ space = Proposal::size(consensus_account.signers_len()),
seeds = [
SEED_PREFIX,
- settings.key().as_ref(),
+ consensus_account.key().as_ref(),
SEED_TRANSACTION,
&args.transaction_index.to_le_bytes(),
SEED_PROPOSAL,
@@ -43,41 +46,44 @@ pub struct CreateProposal<'info> {
pub rent_payer: Signer<'info>,
pub system_program: Program<'info, System>,
+ pub program: Program<'info, SquadsSmartAccountProgram>,
}
impl CreateProposal<'_> {
- fn validate(&self, args: &CreateProposalArgs) -> Result<()> {
+ fn validate(&self, ctx: &Context, args: &CreateProposalArgs) -> Result<()> {
let Self {
- settings, creator, ..
+ consensus_account, creator, ..
} = self;
let creator_key = creator.key();
+ // Check if the consensus account is active
+ consensus_account.is_active(&ctx.remaining_accounts)?;
+
// args
// We can only create a proposal for an existing transaction.
require!(
- args.transaction_index <= settings.transaction_index,
+ args.transaction_index <= consensus_account.transaction_index(),
SmartAccountError::InvalidTransactionIndex
);
// We can't create a proposal for a stale transaction.
require!(
- args.transaction_index > settings.stale_transaction_index,
+ args.transaction_index > consensus_account.stale_transaction_index(),
SmartAccountError::StaleProposal
);
// creator
// Has to be a signer on the smart account.
require!(
- self.settings.is_signer(self.creator.key()).is_some(),
+ consensus_account.is_signer(creator.key()).is_some(),
SmartAccountError::NotASigner
);
// Must have at least one of the following permissions: Initiate or Vote.
require!(
- self.settings
+ consensus_account
.signer_has_permission(creator_key, Permission::Initiate)
- || self
- .settings
+ || consensus_account
.signer_has_permission(creator_key, Permission::Vote),
SmartAccountError::Unauthorized
);
@@ -86,13 +92,13 @@ impl CreateProposal<'_> {
}
/// Create a new proposal.
- #[access_control(ctx.accounts.validate(&args))]
+ #[access_control(ctx.accounts.validate(&ctx, &args))]
pub fn create_proposal(ctx: Context, args: CreateProposalArgs) -> Result<()> {
let proposal = &mut ctx.accounts.proposal;
- let settings = &ctx.accounts.settings;
+ let consensus_account = &ctx.accounts.consensus_account;
let rent_payer = &mut ctx.accounts.rent_payer;
- proposal.settings = settings.key();
+ proposal.settings = consensus_account.key();
proposal.transaction_index = args.transaction_index;
proposal.rent_collector = rent_payer.key();
proposal.status = if args.draft {
@@ -109,6 +115,25 @@ impl CreateProposal<'_> {
proposal.rejected = vec![];
proposal.cancelled = vec![];
+ // Log the event
+ let event = ProposalEvent {
+ event_type: ProposalEventType::Create,
+ consensus_account: consensus_account.key(),
+ consensus_account_type: consensus_account.account_type(),
+ proposal_pubkey: proposal.key(),
+ transaction_index: args.transaction_index,
+ signer: Some(ctx.accounts.creator.key()),
+ proposal: Some(proposal.clone().into_inner()),
+ memo: None,
+ };
+ let log_authority_info = LogAuthorityInfo {
+ authority: consensus_account.to_account_info(),
+ authority_seeds: consensus_account.get_signer_seeds(),
+ bump: consensus_account.bump(),
+ program: ctx.accounts.program.to_account_info(),
+ };
+ SmartAccountEvent::ProposalEvent(event).log(&log_authority_info)?;
+
Ok(())
}
}
diff --git a/programs/squads_smart_account_program/src/instructions/proposal_vote.rs b/programs/squads_smart_account_program/src/instructions/proposal_vote.rs
index 1bf6ea5..335448c 100644
--- a/programs/squads_smart_account_program/src/instructions/proposal_vote.rs
+++ b/programs/squads_smart_account_program/src/instructions/proposal_vote.rs
@@ -1,6 +1,11 @@
use anchor_lang::prelude::*;
+use crate::consensus_trait::Consensus;
use crate::errors::*;
+use crate::events::*;
+use crate::interface::consensus::ConsensusAccount;
+use crate::program::SquadsSmartAccountProgram;
+
use crate::state::*;
#[derive(AnchorSerialize, AnchorDeserialize)]
@@ -11,10 +16,9 @@ pub struct VoteOnProposalArgs {
#[derive(Accounts)]
pub struct VoteOnProposal<'info> {
#[account(
- seeds = [SEED_PREFIX, SEED_SETTINGS, settings.seed.to_le_bytes().as_ref()],
- bump = settings.bump,
+ constraint = consensus_account.check_derivation(consensus_account.key()).is_ok()
)]
- pub settings: Account<'info, Settings>,
+ pub consensus_account: InterfaceAccount<'info, ConsensusAccount>,
#[account(mut)]
pub signer: Signer<'info>,
@@ -23,7 +27,7 @@ pub struct VoteOnProposal<'info> {
mut,
seeds = [
SEED_PREFIX,
- settings.key().as_ref(),
+ consensus_account.key().as_ref(),
SEED_TRANSACTION,
&proposal.transaction_index.to_le_bytes(),
SEED_PROPOSAL,
@@ -34,24 +38,28 @@ pub struct VoteOnProposal<'info> {
// Only required for cancelling a proposal.
pub system_program: Option>,
+ pub program: Program<'info, SquadsSmartAccountProgram>,
}
impl VoteOnProposal<'_> {
- fn validate(&self, vote: Vote) -> Result<()> {
+ fn validate(&self, ctx: &Context, vote: Vote) -> Result<()> {
let Self {
- settings,
+ consensus_account,
proposal,
signer,
..
} = self;
+ // Check if the consensus account is active
+ consensus_account.is_active(&ctx.remaining_accounts)?;
+
// signer
require!(
- settings.is_signer(signer.key()).is_some(),
+ consensus_account.is_signer(signer.key()).is_some(),
SmartAccountError::NotASigner
);
require!(
- settings.signer_has_permission(signer.key(), Permission::Vote),
+ consensus_account.signer_has_permission(signer.key(), Permission::Vote),
SmartAccountError::Unauthorized
);
@@ -64,7 +72,7 @@ impl VoteOnProposal<'_> {
);
// CANNOT approve or reject a stale proposal
require!(
- proposal.transaction_index > settings.stale_transaction_index,
+ proposal.transaction_index > consensus_account.stale_transaction_index(),
SmartAccountError::StaleProposal
);
}
@@ -82,37 +90,76 @@ impl VoteOnProposal<'_> {
/// Approve a smart account proposal on behalf of the `signer`.
/// The proposal must be `Active`.
- #[access_control(ctx.accounts.validate(Vote::Approve))]
- pub fn approve_proposal(ctx: Context, _args: VoteOnProposalArgs) -> Result<()> {
- let settings = &mut ctx.accounts.settings;
+ #[access_control(ctx.accounts.validate(&ctx, Vote::Approve))]
+ pub fn approve_proposal(ctx: Context, args: VoteOnProposalArgs) -> Result<()> {
+ let consensus_account = &mut ctx.accounts.consensus_account;
+
let proposal = &mut ctx.accounts.proposal;
let signer = &mut ctx.accounts.signer;
- proposal.approve(signer.key(), usize::from(settings.threshold))?;
+ proposal.approve(signer.key(), usize::from(consensus_account.threshold()))?;
+
+ // Log the vote event with proposal state
+ let vote_event = ProposalEvent {
+ event_type: ProposalEventType::Approve,
+ consensus_account: consensus_account.key(),
+ consensus_account_type: consensus_account.account_type(),
+ proposal_pubkey: proposal.key(),
+ transaction_index: proposal.transaction_index,
+ signer: Some(signer.key()),
+ memo: args.memo,
+ proposal: Some(Proposal::try_from_slice(&proposal.try_to_vec()?)?),
+ };
+ let log_authority_info = LogAuthorityInfo {
+ authority: consensus_account.to_account_info(),
+ authority_seeds: consensus_account.get_signer_seeds(),
+ bump: consensus_account.bump(),
+ program: ctx.accounts.program.to_account_info(),
+ };
+ SmartAccountEvent::ProposalEvent(vote_event).log(&log_authority_info)?;
Ok(())
}
/// Reject a smart account proposal on behalf of the `signer`.
/// The proposal must be `Active`.
- #[access_control(ctx.accounts.validate(Vote::Reject))]
- pub fn reject_proposal(ctx: Context, _args: VoteOnProposalArgs) -> Result<()> {
- let settings = &mut ctx.accounts.settings;
+ #[access_control(ctx.accounts.validate(&ctx, Vote::Reject))]
+ pub fn reject_proposal(ctx: Context, args: VoteOnProposalArgs) -> Result<()> {
+ let consensus_account = &mut ctx.accounts.consensus_account;
let proposal = &mut ctx.accounts.proposal;
let signer = &mut ctx.accounts.signer;
- let cutoff = Settings::cutoff(&settings);
+ let cutoff = consensus_account.cutoff();
proposal.reject(signer.key(), cutoff)?;
+ // Log the vote event with proposal state
+ let vote_event = ProposalEvent {
+ event_type: ProposalEventType::Reject,
+ consensus_account: consensus_account.key(),
+ consensus_account_type: consensus_account.account_type(),
+ proposal_pubkey: proposal.key(),
+ transaction_index: proposal.transaction_index,
+ signer: Some(signer.key()),
+ memo: args.memo,
+ proposal: Some(proposal.clone().into_inner()),
+ };
+ let log_authority_info = LogAuthorityInfo {
+ authority: consensus_account.to_account_info(),
+ authority_seeds: consensus_account.get_signer_seeds(),
+ bump: consensus_account.bump(),
+ program: ctx.accounts.program.to_account_info(),
+ };
+ SmartAccountEvent::ProposalEvent(vote_event).log(&log_authority_info)?;
+
Ok(())
}
/// Cancel a smart account proposal on behalf of the `signer`.
/// The proposal must be `Approved`.
- #[access_control(ctx.accounts.validate(Vote::Cancel))]
- pub fn cancel_proposal(ctx: Context, _args: VoteOnProposalArgs) -> Result<()> {
- let settings = &mut ctx.accounts.settings;
+ #[access_control(ctx.accounts.validate(&ctx, Vote::Cancel))]
+ pub fn cancel_proposal(ctx: Context, args: VoteOnProposalArgs) -> Result<()> {
+ let consensus_account = &mut ctx.accounts.consensus_account;
let proposal = &mut ctx.accounts.proposal;
let signer = &mut ctx.accounts.signer;
let system_program = &ctx
@@ -123,17 +170,36 @@ impl VoteOnProposal<'_> {
proposal
.cancelled
- .retain(|k| settings.is_signer(*k).is_some());
+ .retain(|k| consensus_account.is_signer(*k).is_some());
- proposal.cancel(signer.key(), usize::from(settings.threshold))?;
+ proposal.cancel(signer.key(), usize::from(consensus_account.threshold()))?;
Proposal::realloc_if_needed(
proposal.to_account_info().clone(),
- settings.signers.len(),
+ consensus_account.signers_len(),
Some(signer.to_account_info().clone()),
Some(system_program.to_account_info().clone()),
)?;
+ // Log the vote event with proposal state
+ let vote_event = ProposalEvent {
+ event_type: ProposalEventType::Cancel,
+ consensus_account: consensus_account.key(),
+ consensus_account_type: consensus_account.account_type(),
+ proposal_pubkey: proposal.key(),
+ transaction_index: proposal.transaction_index,
+ signer: Some(signer.key()),
+ memo: args.memo,
+ proposal: Some(proposal.clone().into_inner()),
+ };
+ let log_authority_info = LogAuthorityInfo {
+ authority: consensus_account.to_account_info(),
+ authority_seeds: consensus_account.get_signer_seeds(),
+ bump: consensus_account.bump(),
+ program: ctx.accounts.program.to_account_info(),
+ };
+ SmartAccountEvent::ProposalEvent(vote_event).log(&log_authority_info)?;
+
Ok(())
}
}
diff --git a/programs/squads_smart_account_program/src/instructions/settings_transaction_create.rs b/programs/squads_smart_account_program/src/instructions/settings_transaction_create.rs
index c1f6b7a..5d51098 100644
--- a/programs/squads_smart_account_program/src/instructions/settings_transaction_create.rs
+++ b/programs/squads_smart_account_program/src/instructions/settings_transaction_create.rs
@@ -1,8 +1,11 @@
use anchor_lang::prelude::*;
-use crate::errors::*;
-use crate::state::*;
+use crate::consensus_trait::{Consensus, ConsensusAccountType};
+use crate::program::SquadsSmartAccountProgram;
+use crate::{state::*, SmartAccountEvent};
use crate::utils::validate_settings_actions;
+use crate::LogAuthorityInfo;
+use crate::{errors::*, TransactionContent, TransactionEvent, TransactionEventType};
#[derive(AnchorSerialize, AnchorDeserialize)]
pub struct CreateSettingsTransactionArgs {
@@ -42,6 +45,8 @@ pub struct CreateSettingsTransaction<'info> {
pub rent_payer: Signer<'info>,
pub system_program: Program<'info, System>,
+
+ pub program: Program<'info, SquadsSmartAccountProgram>,
}
impl CreateSettingsTransaction<'_> {
@@ -91,16 +96,37 @@ impl CreateSettingsTransaction<'_> {
transaction.rent_collector = rent_payer.key();
transaction.index = transaction_index;
transaction.bump = ctx.bumps.transaction;
- transaction.actions = args.actions;
+ transaction.actions = args.actions.clone();
// Updated last transaction index in the settings account.
settings.transaction_index = transaction_index;
settings.invariant()?;
- // Logs for indexing.
- msg!("transaction index: {}", transaction_index);
-
+ // Log event authority info
+ let log_authority_info = LogAuthorityInfo {
+ authority: settings.to_account_info().clone(),
+ authority_seeds: get_settings_signer_seeds(settings.seed),
+ bump: settings.bump,
+ program: ctx.accounts.program.to_account_info(),
+ };
+
+ // Log the event
+ let event = TransactionEvent {
+ event_type: TransactionEventType::Create,
+ consensus_account: settings.key(),
+ consensus_account_type: ConsensusAccountType::Settings,
+ transaction_pubkey: transaction.key(),
+ transaction_index,
+ signer: Some(creator.key()),
+ transaction_content: Some(TransactionContent::SettingsTransaction {
+ settings: settings.clone().into_inner(),
+ transaction: transaction.clone().into_inner(),
+ changes: args.actions,
+ }),
+ memo: None,
+ };
+ SmartAccountEvent::TransactionEvent(event).log(&log_authority_info)?;
Ok(())
}
}
diff --git a/programs/squads_smart_account_program/src/instructions/settings_transaction_execute.rs b/programs/squads_smart_account_program/src/instructions/settings_transaction_execute.rs
index 6aa173f..d2b1af5 100644
--- a/programs/squads_smart_account_program/src/instructions/settings_transaction_execute.rs
+++ b/programs/squads_smart_account_program/src/instructions/settings_transaction_execute.rs
@@ -1,7 +1,17 @@
use anchor_lang::prelude::*;
+use crate::consensus_trait::Consensus;
+use crate::consensus_trait::ConsensusAccountType;
use crate::errors::*;
+use crate::program::SquadsSmartAccountProgram;
use crate::state::*;
+use crate::LogAuthorityInfo;
+use crate::ProposalEvent;
+use crate::ProposalEventType;
+use crate::SmartAccountEvent;
+use crate::TransactionContent;
+use crate::TransactionEvent;
+use crate::TransactionEventType;
#[derive(Accounts)]
pub struct ExecuteSettingsTransaction<'info> {
@@ -50,6 +60,9 @@ pub struct ExecuteSettingsTransaction<'info> {
/// We might need it in case reallocation is needed.
pub system_program: Option>,
+
+ pub program: Program<'info, SquadsSmartAccountProgram>,
+
// In case the transaction contains Add(Remove)SpendingLimit actions,
// `remaining_accounts` must contain the SpendingLimit accounts to be initialized/closed.
// remaining_accounts
@@ -117,6 +130,14 @@ impl<'info> ExecuteSettingsTransaction<'info> {
let rent = Rent::get()?;
+ // Log event authority info
+ let log_authority_info = LogAuthorityInfo {
+ authority: settings.to_account_info().clone(),
+ authority_seeds: get_settings_signer_seeds(settings.seed),
+ bump: settings.bump,
+ program: ctx.accounts.program.to_account_info(),
+ };
+
// Execute the actions one by one.
for action in transaction.actions.iter() {
settings.modify_with_action(
@@ -127,6 +148,7 @@ impl<'info> ExecuteSettingsTransaction<'info> {
&ctx.accounts.system_program,
&ctx.remaining_accounts,
&ctx.program_id,
+ Some(&log_authority_info),
)?;
}
@@ -152,6 +174,37 @@ impl<'info> ExecuteSettingsTransaction<'info> {
timestamp: Clock::get()?.unix_timestamp,
};
+ // Transaction event
+ let event = TransactionEvent {
+ event_type: TransactionEventType::Execute,
+ consensus_account: settings.key(),
+ consensus_account_type: ConsensusAccountType::Settings,
+ transaction_pubkey: transaction.key(),
+ transaction_index: transaction.index,
+ signer: Some(ctx.accounts.signer.key()),
+ transaction_content: Some(TransactionContent::SettingsTransaction {
+ settings: settings.clone().into_inner(),
+ transaction: transaction.clone().into_inner(),
+ changes: transaction.actions.clone(),
+ }),
+ memo: None,
+ };
+
+ // Proposal event
+ let proposal_event = ProposalEvent {
+ event_type: ProposalEventType::Execute,
+ consensus_account: settings.key(),
+ consensus_account_type: ConsensusAccountType::Settings,
+ proposal_pubkey: proposal.key(),
+ transaction_index: transaction.index,
+ signer: Some(ctx.accounts.signer.key()),
+ memo: None,
+ proposal: Some(proposal.clone().into_inner()),
+ };
+
+ SmartAccountEvent::TransactionEvent(event).log(&log_authority_info)?;
+ SmartAccountEvent::ProposalEvent(proposal_event).log(&log_authority_info)?;
+
Ok(())
}
}
diff --git a/programs/squads_smart_account_program/src/instructions/settings_transaction_sync.rs b/programs/squads_smart_account_program/src/instructions/settings_transaction_sync.rs
index 148888d..c607688 100644
--- a/programs/squads_smart_account_program/src/instructions/settings_transaction_sync.rs
+++ b/programs/squads_smart_account_program/src/instructions/settings_transaction_sync.rs
@@ -1,7 +1,6 @@
-use account_events::{AddSpendingLimitEvent, RemoveSpendingLimitEvent};
use anchor_lang::prelude::*;
-use crate::{errors::*, events::*, program::SquadsSmartAccountProgram, state::*, utils::*};
+use crate::{consensus::ConsensusAccount, consensus_trait::{Consensus, ConsensusAccountType}, errors::*, events::*, program::SquadsSmartAccountProgram, state::*, utils::*};
#[derive(AnchorSerialize, AnchorDeserialize)]
pub struct SyncSettingsTransactionArgs {
@@ -16,10 +15,10 @@ pub struct SyncSettingsTransactionArgs {
pub struct SyncSettingsTransaction<'info> {
#[account(
mut,
- seeds = [SEED_PREFIX, SEED_SETTINGS, settings.seed.to_le_bytes().as_ref()],
- bump = settings.bump,
+ constraint = consensus_account.check_derivation(consensus_account.key()).is_ok(),
+ constraint = consensus_account.account_type() == ConsensusAccountType::Settings
)]
- pub settings: Box>,
+ pub consensus_account: Box>,
/// The account that will be charged/credited in case the settings transaction causes space reallocation,
/// for example when adding a new signer, adding or removing a spending limit.
@@ -41,7 +40,9 @@ impl<'info> SyncSettingsTransaction<'info> {
args: &SyncSettingsTransactionArgs,
remaining_accounts: &[AccountInfo],
) -> Result<()> {
- let Self { settings, .. } = self;
+ let Self { consensus_account, .. } = self;
+ // Get the settings
+ let settings = consensus_account.read_only_settings()?;
// Settings must not be controlled
require_keys_eq!(
@@ -54,7 +55,7 @@ impl<'info> SyncSettingsTransaction<'info> {
validate_settings_actions(&args.actions)?;
// Validates synchronous consensus across the signers
- validate_synchronous_consensus(settings, args.num_signers, remaining_accounts)?;
+ validate_synchronous_consensus(&consensus_account, args.num_signers, remaining_accounts)?;
Ok(())
}
@@ -64,10 +65,23 @@ impl<'info> SyncSettingsTransaction<'info> {
ctx: Context<'_, '_, 'info, 'info, Self>,
args: SyncSettingsTransactionArgs,
) -> Result<()> {
- let settings = &mut ctx.accounts.settings;
- let settings_key = settings.key();
+ // Wrapper consensus account
+ let consensus_account = &mut ctx.accounts.consensus_account;
+ let settings_key = consensus_account.key();
+ let settings_account_info = consensus_account.to_account_info();
+
+ let settings = consensus_account.settings()?;
+
let rent = Rent::get()?;
+ // Build the log authority info
+ let log_authority_info = LogAuthorityInfo {
+ authority: settings_account_info.clone(),
+ authority_seeds: get_settings_signer_seeds(settings.seed),
+ bump: settings.bump,
+ program: ctx.accounts.program.to_account_info(),
+ };
+
// Execute the actions one by one
for action in args.actions.iter() {
settings.modify_with_action(
@@ -78,12 +92,13 @@ impl<'info> SyncSettingsTransaction<'info> {
&ctx.accounts.system_program,
&ctx.remaining_accounts,
&ctx.program_id,
+ Some(&log_authority_info),
)?;
}
// Make sure the smart account can fit the updated state: added signers or newly set archival_authority.
Settings::realloc_if_needed(
- settings.to_account_info(),
+ settings_account_info,
settings.signers.len(),
ctx.accounts
.rent_payer
@@ -98,65 +113,19 @@ impl<'info> SyncSettingsTransaction<'info> {
// Make sure the settings state is valid after applying the actions
settings.invariant()?;
- // Log the events
+ // Log the event
let event = SynchronousSettingsTransactionEvent {
settings_pubkey: settings_key,
signers: ctx.remaining_accounts[..args.num_signers as usize]
.iter()
.map(|acc| acc.key.clone())
.collect::>(),
- settings: Settings::try_from_slice(&settings.try_to_vec()?)?,
+ settings: settings.clone(),
changes: args.actions.clone(),
};
- let log_authority_info = LogAuthorityInfo {
- authority: settings.to_account_info(),
- authority_seeds: get_settings_signer_seeds(settings.seed),
- bump: settings.bump,
- program: ctx.accounts.program.to_account_info(),
- };
+
SmartAccountEvent::SynchronousSettingsTransactionEvent(event).log(&log_authority_info)?;
- for action in args.actions.iter() {
- match action {
- SettingsAction::AddSpendingLimit { seed, .. } => {
- let spending_limit_pubkey = Pubkey::find_program_address(
- &[
- SEED_PREFIX,
- settings_key.as_ref(),
- SEED_SPENDING_LIMIT,
- seed.as_ref(),
- ],
- &ctx.accounts.program.key(),
- )
- .0;
-
- let spending_limit_data = ctx
- .remaining_accounts
- .iter()
- .find(|acc| acc.key == &spending_limit_pubkey)
- .ok_or(SmartAccountError::MissingAccount)?
- .try_borrow_data()?;
-
-
- let event = AddSpendingLimitEvent {
- settings_pubkey: settings_key,
- spending_limit_pubkey: spending_limit_pubkey,
- spending_limit: SpendingLimit::try_from_slice(&spending_limit_data[8..])?,
- };
- SmartAccountEvent::AddSpendingLimitEvent(event).log(&log_authority_info)?;
- }
- SettingsAction::RemoveSpendingLimit { spending_limit, .. } => {
- let event = RemoveSpendingLimitEvent {
- settings_pubkey: settings_key,
- spending_limit_pubkey: spending_limit.key(),
- };
- SmartAccountEvent::RemoveSpendingLimitEvent(event).log(&log_authority_info)?;
- }
- _ => {
- continue;
- }
- }
- }
Ok(())
}
}
diff --git a/programs/squads_smart_account_program/src/instructions/smart_account_create.rs b/programs/squads_smart_account_program/src/instructions/smart_account_create.rs
index dfaac67..b35a824 100644
--- a/programs/squads_smart_account_program/src/instructions/smart_account_create.rs
+++ b/programs/squads_smart_account_program/src/instructions/smart_account_create.rs
@@ -1,5 +1,3 @@
-#![allow(deprecated)]
-use std::borrow::Borrow;
use account_events::CreateSmartAccountEvent;
use anchor_lang::prelude::*;
@@ -97,7 +95,7 @@ impl<'info> CreateSmartAccount<'info> {
bump: settings_bump,
signers,
account_utilization: 0,
- _reserved1: 0,
+ policy_seed: Some(0),
_reserved2: 0,
};
@@ -128,7 +126,6 @@ impl<'info> CreateSmartAccount<'info> {
),
creation_fee,
)?;
- msg!("Creation fee: {}", creation_fee / LAMPORTS_PER_SOL);
}
// Increment the smart account index.
diff --git a/programs/squads_smart_account_program/src/instructions/transaction_buffer_close.rs b/programs/squads_smart_account_program/src/instructions/transaction_buffer_close.rs
index f3f4cb7..31693b9 100644
--- a/programs/squads_smart_account_program/src/instructions/transaction_buffer_close.rs
+++ b/programs/squads_smart_account_program/src/instructions/transaction_buffer_close.rs
@@ -1,15 +1,16 @@
use anchor_lang::prelude::*;
+use crate::consensus_trait::Consensus;
use crate::errors::*;
+use crate::interface::consensus::ConsensusAccount;
use crate::state::*;
#[derive(Accounts)]
pub struct CloseTransactionBuffer<'info> {
#[account(
- seeds = [SEED_PREFIX, SEED_SETTINGS, settings.seed.to_le_bytes().as_ref()],
- bump = settings.bump,
+ constraint = consensus_account.check_derivation(consensus_account.key()).is_ok()
)]
- pub settings: Account<'info, Settings>,
+ pub consensus_account: InterfaceAccount<'info, ConsensusAccount>,
#[account(
mut,
@@ -21,7 +22,7 @@ pub struct CloseTransactionBuffer<'info> {
// current settings transaction index
seeds = [
SEED_PREFIX,
- settings.key().as_ref(),
+ consensus_account.key().as_ref(),
SEED_TRANSACTION_BUFFER,
creator.key().as_ref(),
&transaction_buffer.buffer_index.to_le_bytes()
diff --git a/programs/squads_smart_account_program/src/instructions/transaction_buffer_create.rs b/programs/squads_smart_account_program/src/instructions/transaction_buffer_create.rs
index 21b587e..57efb4f 100644
--- a/programs/squads_smart_account_program/src/instructions/transaction_buffer_create.rs
+++ b/programs/squads_smart_account_program/src/instructions/transaction_buffer_create.rs
@@ -1,6 +1,8 @@
use anchor_lang::prelude::*;
+use crate::consensus_trait::Consensus;
use crate::errors::*;
+use crate::interface::consensus::ConsensusAccount;
use crate::state::MAX_BUFFER_SIZE;
use crate::state::*;
@@ -22,10 +24,9 @@ pub struct CreateTransactionBufferArgs {
#[instruction(args: CreateTransactionBufferArgs)]
pub struct CreateTransactionBuffer<'info> {
#[account(
- seeds = [SEED_PREFIX, SEED_SETTINGS, settings.seed.to_le_bytes().as_ref()],
- bump = settings.bump,
+ constraint = consensus_account.check_derivation(consensus_account.key()).is_ok()
)]
- pub settings: Account<'info, Settings>,
+ pub consensus_account: InterfaceAccount<'info, ConsensusAccount>,
#[account(
init,
@@ -33,7 +34,7 @@ pub struct CreateTransactionBuffer<'info> {
space = TransactionBuffer::size(args.final_buffer_size)?,
seeds = [
SEED_PREFIX,
- settings.key().as_ref(),
+ consensus_account.key().as_ref(),
SEED_TRANSACTION_BUFFER,
creator.key().as_ref(),
&args.buffer_index.to_le_bytes(),
@@ -55,17 +56,17 @@ pub struct CreateTransactionBuffer<'info> {
impl CreateTransactionBuffer<'_> {
fn validate(&self, args: &CreateTransactionBufferArgs) -> Result<()> {
let Self {
- settings, creator, ..
+ consensus_account, creator, ..
} = self;
// creator is a signer on the smart account
require!(
- settings.is_signer(creator.key()).is_some(),
+ consensus_account.is_signer(creator.key()).is_some(),
SmartAccountError::NotASigner
);
// creator has initiate permissions
require!(
- settings.signer_has_permission(creator.key(), Permission::Initiate),
+ consensus_account.signer_has_permission(creator.key(), Permission::Initiate),
SmartAccountError::Unauthorized
);
@@ -86,14 +87,14 @@ impl CreateTransactionBuffer<'_> {
// Readonly Accounts
let transaction_buffer = &mut ctx.accounts.transaction_buffer;
- let settings = &ctx.accounts.settings;
+ let consensus_account = &ctx.accounts.consensus_account;
let creator = &mut ctx.accounts.creator;
// Get the buffer index.
let buffer_index = args.buffer_index;
// Initialize the transaction fields.
- transaction_buffer.settings = settings.key();
+ transaction_buffer.settings = consensus_account.key();
transaction_buffer.creator = creator.key();
transaction_buffer.account_index = args.account_index;
transaction_buffer.buffer_index = buffer_index;
diff --git a/programs/squads_smart_account_program/src/instructions/transaction_buffer_extend.rs b/programs/squads_smart_account_program/src/instructions/transaction_buffer_extend.rs
index 6c1037a..ae9162a 100644
--- a/programs/squads_smart_account_program/src/instructions/transaction_buffer_extend.rs
+++ b/programs/squads_smart_account_program/src/instructions/transaction_buffer_extend.rs
@@ -1,6 +1,8 @@
use anchor_lang::prelude::*;
+use crate::consensus_trait::Consensus;
use crate::errors::*;
+use crate::interface::consensus::ConsensusAccount;
use crate::state::*;
#[derive(AnchorSerialize, AnchorDeserialize)]
@@ -13,10 +15,9 @@ pub struct ExtendTransactionBufferArgs {
#[instruction(args: ExtendTransactionBufferArgs)]
pub struct ExtendTransactionBuffer<'info> {
#[account(
- seeds = [SEED_PREFIX, SEED_SETTINGS, settings.seed.to_le_bytes().as_ref()],
- bump = settings.bump,
+ constraint = consensus_account.check_derivation(consensus_account.key()).is_ok()
)]
- pub settings: Account<'info, Settings>,
+ pub consensus_account: InterfaceAccount<'info, ConsensusAccount>,
#[account(
mut,
@@ -24,7 +25,7 @@ pub struct ExtendTransactionBuffer<'info> {
constraint = transaction_buffer.creator == creator.key() @ SmartAccountError::Unauthorized,
seeds = [
SEED_PREFIX,
- settings.key().as_ref(),
+ consensus_account.key().as_ref(),
SEED_TRANSACTION_BUFFER,
creator.key().as_ref(),
&transaction_buffer.buffer_index.to_le_bytes()
@@ -40,7 +41,7 @@ pub struct ExtendTransactionBuffer<'info> {
impl ExtendTransactionBuffer<'_> {
fn validate(&self, args: &ExtendTransactionBufferArgs) -> Result<()> {
let Self {
- settings,
+ consensus_account,
creator,
transaction_buffer,
..
@@ -48,13 +49,13 @@ impl ExtendTransactionBuffer<'_> {
// creator is still a signer on the smart account
require!(
- settings.is_signer(creator.key()).is_some(),
+ consensus_account.is_signer(creator.key()).is_some(),
SmartAccountError::NotASigner
);
// creator still has initiate permissions
require!(
- settings.signer_has_permission(creator.key(), Permission::Initiate),
+ consensus_account.signer_has_permission(creator.key(), Permission::Initiate),
SmartAccountError::Unauthorized
);
diff --git a/programs/squads_smart_account_program/src/instructions/transaction_close.rs b/programs/squads_smart_account_program/src/instructions/transaction_close.rs
index 3b8fb3f..b63244b 100644
--- a/programs/squads_smart_account_program/src/instructions/transaction_close.rs
+++ b/programs/squads_smart_account_program/src/instructions/transaction_close.rs
@@ -10,10 +10,18 @@
//! allows adding the `close` attribute only to `Account<'info, XXX>` types, which forces us
//! into having 3 different `Accounts` structs.
use anchor_lang::prelude::*;
+use anchor_lang::system_program;
+use crate::consensus::ConsensusAccount;
+use crate::consensus_trait::Consensus;
+use crate::consensus_trait::ConsensusAccountType;
use crate::errors::*;
+use crate::program::SquadsSmartAccountProgram;
use crate::state::*;
-use crate::utils;
+use crate::LogAuthorityInfo;
+use crate::SmartAccountEvent;
+use crate::TransactionEvent;
+use crate::TransactionEventType;
#[derive(Accounts)]
pub struct CloseSettingsTransaction<'info> {
@@ -60,6 +68,7 @@ pub struct CloseSettingsTransaction<'info> {
pub transaction_rent_collector: AccountInfo<'info>,
pub system_program: Program<'info, System>,
+ pub program: Program<'info, SquadsSmartAccountProgram>,
}
impl CloseSettingsTransaction<'_> {
@@ -111,11 +120,19 @@ impl CloseSettingsTransaction<'_> {
require!(can_close, SmartAccountError::InvalidProposalStatus);
+ let log_authority_info = LogAuthorityInfo {
+ authority: settings.to_account_info(),
+ authority_seeds: get_settings_signer_seeds(settings.seed),
+ bump: settings.bump,
+ program: ctx.accounts.program.to_account_info(),
+ };
// Close the `proposal` account if exists.
Proposal::close_if_exists(
proposal_account,
proposal.to_account_info(),
proposal_rent_collector.clone(),
+ &log_authority_info,
+ ConsensusAccountType::Settings,
)?;
// Anchor will close the `transaction` account for us.
@@ -126,10 +143,9 @@ impl CloseSettingsTransaction<'_> {
#[derive(Accounts)]
pub struct CloseTransaction<'info> {
#[account(
- seeds = [SEED_PREFIX, SEED_SETTINGS, settings.seed.to_le_bytes().as_ref()],
- bump = settings.bump,
+ constraint = consensus_account.check_derivation(consensus_account.key()).is_ok()
)]
- pub settings: Account<'info, Settings>,
+ pub consensus_account: InterfaceAccount<'info, ConsensusAccount>,
/// CHECK: `seeds` and `bump` verify that the account is the canonical Proposal,
/// the logic within `transaction_close` does the rest of the checks.
@@ -137,7 +153,7 @@ pub struct CloseTransaction<'info> {
mut,
seeds = [
SEED_PREFIX,
- settings.key().as_ref(),
+ consensus_account.key().as_ref(),
SEED_TRANSACTION,
&transaction.index.to_le_bytes(),
SEED_PROPOSAL,
@@ -149,7 +165,7 @@ pub struct CloseTransaction<'info> {
/// Transaction corresponding to the `proposal`.
#[account(
mut,
- has_one = settings @ SmartAccountError::TransactionForAnotherSmartAccount,
+ has_one = consensus_account @ SmartAccountError::TransactionForAnotherSmartAccount,
close = transaction_rent_collector
)]
pub transaction: Account<'info, Transaction>,
@@ -168,6 +184,7 @@ pub struct CloseTransaction<'info> {
pub transaction_rent_collector: AccountInfo<'info>,
pub system_program: Program<'info, System>,
+ pub program: Program<'info, SquadsSmartAccountProgram>,
}
impl CloseTransaction<'_> {
@@ -176,12 +193,12 @@ impl CloseTransaction<'_> {
/// - the `proposal` is in a terminal state: `Executed`, `Rejected`, or `Cancelled`.
/// - the `proposal` is stale and not `Approved`.
pub fn close_transaction(ctx: Context) -> Result<()> {
- let settings = &ctx.accounts.settings;
+ let consensus_account = &ctx.accounts.consensus_account;
let transaction = &ctx.accounts.transaction;
let proposal = &mut ctx.accounts.proposal;
let proposal_rent_collector = &ctx.accounts.proposal_rent_collector;
- let is_stale = transaction.index <= settings.stale_transaction_index;
+ let is_stale = transaction.index <= consensus_account.stale_transaction_index();
let proposal_account = if proposal.data.borrow().is_empty() {
None
@@ -219,13 +236,33 @@ impl CloseTransaction<'_> {
require!(can_close, SmartAccountError::InvalidProposalStatus);
+ let log_authority_info = LogAuthorityInfo {
+ authority: consensus_account.to_account_info(),
+ authority_seeds: consensus_account.get_signer_seeds(),
+ bump: consensus_account.bump(),
+ program: ctx.accounts.program.to_account_info(),
+ };
// Close the `proposal` account if exists.
Proposal::close_if_exists(
proposal_account,
proposal.to_account_info(),
proposal_rent_collector.clone(),
+ &log_authority_info,
+ consensus_account.account_type(),
)?;
+ let event = TransactionEvent {
+ event_type: TransactionEventType::Close,
+ consensus_account: consensus_account.key(),
+ consensus_account_type: consensus_account.account_type(),
+ transaction_pubkey: transaction.key(),
+ transaction_index: transaction.index,
+ signer: None,
+ transaction_content: None,
+ memo: None,
+ };
+
+ SmartAccountEvent::TransactionEvent(event).log(&log_authority_info)?;
// Anchor will close the `transaction` account for us.
Ok(())
}
@@ -403,6 +440,7 @@ pub struct CloseBatch<'info> {
pub batch_rent_collector: AccountInfo<'info>,
pub system_program: Program<'info, System>,
+ pub program: Program<'info, SquadsSmartAccountProgram>,
}
impl CloseBatch<'_> {
@@ -458,15 +496,116 @@ impl CloseBatch<'_> {
// Batch must be empty.
require_eq!(batch.size, 0, SmartAccountError::BatchNotEmpty);
+ let log_authority_info = LogAuthorityInfo {
+ authority: settings.to_account_info(),
+ authority_seeds: get_settings_signer_seeds(settings.seed),
+ bump: settings.bump,
+ program: ctx.accounts.program.to_account_info(),
+ };
// Close the `proposal` account if exists.
Proposal::close_if_exists(
proposal_account,
proposal.to_account_info(),
proposal_rent_collector.clone(),
+ &log_authority_info,
+ ConsensusAccountType::Settings,
)?;
// Anchor will close the `batch` account for us.
Ok(())
}
}
-//endregion
+
+#[derive(Accounts)]
+pub struct CloseEmptyPolicyTransaction<'info> {
+ /// Global program config account. (Just using this for logging purposes,
+ /// since we no longer have the consensus account)
+ #[account(mut, seeds = [SEED_PREFIX, SEED_PROGRAM_CONFIG], bump)]
+ pub program_config: Account<'info, ProgramConfig>,
+
+
+ /// CHECK: We only need to validate the address.
+ #[account(
+ constraint = empty_policy.data_is_empty() @ SmartAccountError::InvalidEmptyPolicy,
+ constraint = empty_policy.owner == &system_program::ID @ SmartAccountError::InvalidEmptyPolicy,
+ )]
+ pub empty_policy: AccountInfo<'info>,
+
+ /// CHECK: `seeds` and `bump` verify that the account is the canonical Proposal,
+ /// the logic within `close_empty_policy_transaction` does the rest of the checks.
+ #[account(
+ mut,
+ seeds = [
+ SEED_PREFIX,
+ empty_policy.key().as_ref(),
+ SEED_TRANSACTION,
+ &transaction.index.to_le_bytes(),
+ SEED_PROPOSAL,
+ ],
+ bump,
+ )]
+ pub proposal: AccountInfo<'info>,
+
+ /// Transaction corresponding to the `proposal`.
+ #[account(
+ mut,
+ constraint = transaction.consensus_account == empty_policy.key() @ SmartAccountError::TransactionForAnotherPolicy,
+ close = transaction_rent_collector
+ )]
+ pub transaction: Account<'info, Transaction>,
+
+ /// The rent collector for the proposal account.
+ /// CHECK: validated later inside of `close_empty_policy_transaction`.
+ #[account(mut)]
+ pub proposal_rent_collector: AccountInfo<'info>,
+
+ /// The rent collector.
+ /// CHECK: We only need to validate the address.
+ #[account(
+ mut,
+ address = transaction.rent_collector @ SmartAccountError::InvalidRentCollector,
+ )]
+ pub transaction_rent_collector: AccountInfo<'info>,
+
+ pub system_program: Program<'info, System>,
+
+ pub program: Program<'info, SquadsSmartAccountProgram>,
+}
+
+impl CloseEmptyPolicyTransaction<'_> {
+ /// Closes a `Transaction` and the corresponding `Proposal` for
+ /// empty/deleted policies.
+ ///
+ /// Since a policy can never exist at the same address again after being
+ /// closed, any transaction & proposal associated with it can be closed safely.
+ pub fn close_empty_policy_transaction(ctx: Context) -> Result<()> {
+ let proposal = &mut ctx.accounts.proposal;
+ let proposal_rent_collector = &ctx.accounts.proposal_rent_collector;
+
+ let proposal_account = if proposal.data.borrow().is_empty() {
+ None
+ } else {
+ Some(Proposal::try_deserialize(
+ &mut &**proposal.data.borrow_mut(),
+ )?)
+ };
+
+ let log_authority_info = LogAuthorityInfo {
+ authority: ctx.accounts.program_config.to_account_info(),
+ authority_seeds: vec![SEED_PREFIX.to_vec(), SEED_PROGRAM_CONFIG.to_vec()],
+ bump: ctx.bumps.program_config,
+ program: ctx.accounts.program.to_account_info(),
+ };
+ // Close the `proposal` account if exists.
+ Proposal::close_if_exists(
+ proposal_account,
+ proposal.to_account_info(),
+ proposal_rent_collector.clone(),
+ &log_authority_info,
+ ConsensusAccountType::Policy,
+ )?;
+
+ // Anchor will close the `transaction` account for us.
+ Ok(())
+ }
+}
diff --git a/programs/squads_smart_account_program/src/instructions/transaction_create.rs b/programs/squads_smart_account_program/src/instructions/transaction_create.rs
index 0ed20d5..c7b7a35 100644
--- a/programs/squads_smart_account_program/src/instructions/transaction_create.rs
+++ b/programs/squads_smart_account_program/src/instructions/transaction_create.rs
@@ -1,38 +1,56 @@
use anchor_lang::prelude::*;
+use crate::consensus_trait::Consensus;
use crate::errors::*;
+use crate::interface::consensus::ConsensusAccount;
+use crate::interface::consensus_trait::ConsensusAccountType;
+use crate::events::*;
+use crate::program::SquadsSmartAccountProgram;
use crate::state::*;
use crate::utils::*;
-#[derive(AnchorSerialize, AnchorDeserialize)]
-pub struct CreateTransactionArgs {
- /// Index of the smart account this transaction belongs to.
+#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
+pub struct TransactionPayload {
pub account_index: u8,
- /// Number of ephemeral signing PDAs required by the transaction.
pub ephemeral_signers: u8,
pub transaction_message: Vec,
pub memo: Option,
}
+#[derive(AnchorSerialize, AnchorDeserialize)]
+pub enum CreateTransactionArgs {
+ TransactionPayload(TransactionPayload),
+ PolicyPayload {
+ /// The payload of the policy transaction.
+ payload: PolicyPayload,
+ },
+}
+
#[derive(Accounts)]
#[instruction(args: CreateTransactionArgs)]
pub struct CreateTransaction<'info> {
#[account(
mut,
- seeds = [SEED_PREFIX, SEED_SETTINGS, settings.seed.to_le_bytes().as_ref()],
- bump = settings.bump,
+ constraint = consensus_account.check_derivation(consensus_account.key()).is_ok()
)]
- pub settings: Account<'info, Settings>,
+ pub consensus_account: InterfaceAccount<'info, ConsensusAccount>,
#[account(
init,
payer = rent_payer,
- space = Transaction::size(args.ephemeral_signers, &args.transaction_message)?,
+ space = match &args {
+ CreateTransactionArgs::TransactionPayload(TransactionPayload { ephemeral_signers, transaction_message, .. }) => {
+ Transaction::size_for_transaction(*ephemeral_signers, transaction_message)?
+ },
+ CreateTransactionArgs::PolicyPayload { payload } => {
+ Transaction::size_for_policy(payload)?
+ }
+ },
seeds = [
SEED_PREFIX,
- settings.key().as_ref(),
+ consensus_account.key().as_ref(),
SEED_TRANSACTION,
- &settings.transaction_index.checked_add(1).unwrap().to_le_bytes(),
+ &consensus_account.transaction_index().checked_add(1).unwrap().to_le_bytes(),
],
bump
)]
@@ -46,21 +64,49 @@ pub struct CreateTransaction<'info> {
pub rent_payer: Signer<'info>,
pub system_program: Program<'info, System>,
+ pub program: Program<'info, SquadsSmartAccountProgram>,
}
impl<'info> CreateTransaction<'info> {
- pub fn validate(&self) -> Result<()> {
+ pub fn validate(&self, ctx: &Context, args: &CreateTransactionArgs) -> Result<()> {
let Self {
- settings, creator, ..
+ consensus_account,
+ creator,
+ ..
} = self;
+ // Check if the consensus account is active
+ consensus_account.is_active(&ctx.remaining_accounts)?;
+
+ // Validate the transaction payload
+ match consensus_account.account_type() {
+ ConsensusAccountType::Settings => {
+ assert!(matches!(
+ args,
+ CreateTransactionArgs::TransactionPayload { .. }
+ ));
+ }
+ ConsensusAccountType::Policy => {
+ let policy = consensus_account.read_only_policy()?;
+ // Validate that the args match the policy type
+ match args {
+ CreateTransactionArgs::PolicyPayload { payload } => {
+ // Validate the policy payload against the policy state
+ policy.validate_payload(PolicyExecutionContext::Asynchronous, payload)?;
+ }
+ _ => {
+ return Err(SmartAccountError::InvalidTransactionMessage.into());
+ }
+ }
+ }
+ }
// creator
require!(
- settings.is_signer(creator.key()).is_some(),
+ consensus_account.is_signer(creator.key()).is_some(),
SmartAccountError::NotASigner
);
require!(
- settings.signer_has_permission(creator.key(), Permission::Initiate),
+ consensus_account.signer_has_permission(creator.key(), Permission::Initiate),
SmartAccountError::Unauthorized
);
@@ -68,64 +114,94 @@ impl<'info> CreateTransaction<'info> {
}
/// Create a new vault transaction.
- #[access_control(ctx.accounts.validate())]
+ #[access_control(ctx.accounts.validate(&ctx, &args))]
pub fn create_transaction(ctx: Context, args: CreateTransactionArgs) -> Result<()> {
- let settings = &mut ctx.accounts.settings;
+ let consensus_account = &mut ctx.accounts.consensus_account;
let transaction = &mut ctx.accounts.transaction;
let creator = &mut ctx.accounts.creator;
let rent_payer = &mut ctx.accounts.rent_payer;
- let transaction_message =
- TransactionMessage::deserialize(&mut args.transaction_message.as_slice())?;
-
- let settings_key = settings.key();
let transaction_key = transaction.key();
- let smart_account_seeds = &[
- SEED_PREFIX,
- settings_key.as_ref(),
- SEED_SMART_ACCOUNT,
- &args.account_index.to_le_bytes(),
- ];
- let (_, smart_account_bump) =
- Pubkey::find_program_address(smart_account_seeds, ctx.program_id);
-
- let ephemeral_signer_bumps: Vec = (0..args.ephemeral_signers)
- .map(|ephemeral_signer_index| {
- let ephemeral_signer_seeds = &[
- SEED_PREFIX,
- transaction_key.as_ref(),
- SEED_EPHEMERAL_SIGNER,
- &ephemeral_signer_index.to_le_bytes(),
- ];
-
- let (_, bump) =
- Pubkey::find_program_address(ephemeral_signer_seeds, ctx.program_id);
- bump
- })
- .collect();
-
// Increment the transaction index.
- let transaction_index = settings.transaction_index.checked_add(1).unwrap();
+ let transaction_index = consensus_account
+ .transaction_index()
+ .checked_add(1)
+ .unwrap();
// Initialize the transaction fields.
- transaction.settings = settings_key;
+ transaction.consensus_account = consensus_account.key();
transaction.creator = creator.key();
transaction.rent_collector = rent_payer.key();
transaction.index = transaction_index;
- transaction.bump = ctx.bumps.transaction;
- transaction.account_index = args.account_index;
- transaction.account_bump = smart_account_bump;
- transaction.ephemeral_signer_bumps = ephemeral_signer_bumps;
- transaction.message = transaction_message.try_into()?;
+ match (args, consensus_account.account_type()) {
+ (
+ CreateTransactionArgs::TransactionPayload(TransactionPayload {
+ account_index,
+ ephemeral_signers,
+ transaction_message,
+ memo: _,
+ }),
+ ConsensusAccountType::Settings,
+ ) => {
+ let transaction_message_parsed =
+ TransactionMessage::deserialize(&mut transaction_message.as_slice())?;
+
+ let ephemeral_signer_bumps: Vec = (0..ephemeral_signers)
+ .map(|ephemeral_signer_index| {
+ let ephemeral_signer_seeds = &[
+ SEED_PREFIX,
+ transaction_key.as_ref(),
+ SEED_EPHEMERAL_SIGNER,
+ &ephemeral_signer_index.to_le_bytes(),
+ ];
+
+ let (_, bump) =
+ Pubkey::find_program_address(ephemeral_signer_seeds, ctx.program_id);
+ bump
+ })
+ .collect();
+
+ transaction.payload = Payload::TransactionPayload(TransactionPayloadDetails {
+ account_index: account_index,
+ ephemeral_signer_bumps,
+ message: transaction_message_parsed.try_into()?,
+ });
+ }
+ (CreateTransactionArgs::PolicyPayload { payload }, ConsensusAccountType::Policy) => {
+ transaction.payload =
+ Payload::PolicyPayload(PolicyActionPayloadDetails { payload: payload });
+ }
+ _ => {
+ return Err(SmartAccountError::InvalidTransactionMessage.into());
+ }
+ }
// Updated last transaction index in the settings account.
- settings.transaction_index = transaction_index;
-
- settings.invariant()?;
-
- // Logs for indexing.
- msg!("transaction index: {}", transaction_index);
+ consensus_account.set_transaction_index(transaction_index)?;
+
+ consensus_account.invariant()?;
+
+ // Transaction event
+ let event = TransactionEvent {
+ event_type: TransactionEventType::Create,
+ consensus_account: consensus_account.key(),
+ consensus_account_type: consensus_account.account_type(),
+ transaction_pubkey: transaction.key(),
+ transaction_index,
+ signer: Some(creator.key()),
+ transaction_content: Some(TransactionContent::Transaction(transaction.clone().into_inner())),
+ memo: None,
+ };
+
+ // Log event authority info
+ let log_authority_info = LogAuthorityInfo {
+ authority: consensus_account.to_account_info(),
+ authority_seeds: consensus_account.get_signer_seeds(),
+ bump: consensus_account.bump(),
+ program: ctx.accounts.program.to_account_info(),
+ };
+ SmartAccountEvent::TransactionEvent(event).log(&log_authority_info)?;
Ok(())
}
diff --git a/programs/squads_smart_account_program/src/instructions/transaction_create_from_buffer.rs b/programs/squads_smart_account_program/src/instructions/transaction_create_from_buffer.rs
index 22aff95..db6b503 100644
--- a/programs/squads_smart_account_program/src/instructions/transaction_create_from_buffer.rs
+++ b/programs/squads_smart_account_program/src/instructions/transaction_create_from_buffer.rs
@@ -16,7 +16,7 @@ pub struct CreateTransactionFromBuffer<'info> {
constraint = transaction_buffer.creator == creator.key() @ SmartAccountError::Unauthorized,
seeds = [
SEED_PREFIX,
- transaction_create.settings.key().as_ref(),
+ transaction_create.consensus_account.key().as_ref(),
SEED_TRANSACTION_BUFFER,
creator.key().as_ref(),
&transaction_buffer.buffer_index.to_le_bytes(),
@@ -38,11 +38,21 @@ impl<'info> CreateTransactionFromBuffer<'info> {
pub fn validate(&self, args: &CreateTransactionArgs) -> Result<()> {
let transaction_buffer_account = &self.transaction_buffer;
- // Check that the transaction message is "empty"
- require!(
- args.transaction_message == vec![0, 0, 0, 0, 0, 0],
- SmartAccountError::InvalidInstructionArgs
- );
+ // Check that the transaction message is "empty" and this is a TransactionPayload
+ match args {
+ CreateTransactionArgs::PolicyPayload { .. } => {
+ return Err(SmartAccountError::InvalidInstructionArgs.into())
+ }
+ CreateTransactionArgs::TransactionPayload(TransactionPayload {
+ transaction_message,
+ ..
+ }) => {
+ require!(
+ transaction_message == &vec![0, 0, 0, 0, 0, 0],
+ SmartAccountError::InvalidInstructionArgs
+ );
+ }
+ }
// Validate that the final hash matches the buffer
transaction_buffer_account.validate_hash()?;
@@ -63,11 +73,7 @@ impl<'info> CreateTransactionFromBuffer<'info> {
.transaction_create
.transaction
.to_account_info();
- let rent_payer_account_info = &ctx
- .accounts
- .transaction_create
- .rent_payer
- .to_account_info();
+ let rent_payer_account_info = &ctx.accounts.transaction_create.rent_payer.to_account_info();
let system_program = &ctx
.accounts
@@ -80,8 +86,17 @@ impl<'info> CreateTransactionFromBuffer<'info> {
// Calculate the new required length of the transaction account,
// since it was initialized with an empty transaction message
- let new_len =
- Transaction::size(args.ephemeral_signers, transaction_buffer.buffer.as_slice())?;
+ let new_len = match &args {
+ CreateTransactionArgs::TransactionPayload(TransactionPayload {
+ ephemeral_signers,
+ ..
+ }) => {
+ Transaction::size_for_transaction(*ephemeral_signers, &transaction_buffer.buffer)?
+ }
+ CreateTransactionArgs::PolicyPayload { .. } => {
+ return Err(SmartAccountError::InvalidInstructionArgs.into())
+ }
+ };
// Calculate the rent exemption for new length
let rent_exempt_lamports = Rent::get().unwrap().minimum_balance(new_len).max(1);
@@ -105,11 +120,21 @@ impl<'info> CreateTransactionFromBuffer<'info> {
AccountInfo::realloc(&transaction_account_info, new_len, true)?;
// Create the args for the `create_transaction` instruction
- let create_args = CreateTransactionArgs {
- account_index: args.account_index,
- ephemeral_signers: args.ephemeral_signers,
- transaction_message: transaction_buffer.buffer.clone(),
- memo: args.memo,
+ let create_args = match &args {
+ CreateTransactionArgs::TransactionPayload(TransactionPayload {
+ account_index,
+ ephemeral_signers,
+ memo,
+ ..
+ }) => CreateTransactionArgs::TransactionPayload(TransactionPayload {
+ account_index: *account_index,
+ ephemeral_signers: *ephemeral_signers,
+ transaction_message: transaction_buffer.buffer.clone(),
+ memo: memo.clone(),
+ }),
+ CreateTransactionArgs::PolicyPayload { .. } => {
+ return Err(SmartAccountError::InvalidInstructionArgs.into())
+ }
};
// Create the context for the `create_transaction` instruction
let context = Context::new(
diff --git a/programs/squads_smart_account_program/src/instructions/transaction_execute.rs b/programs/squads_smart_account_program/src/instructions/transaction_execute.rs
index 739910f..206d8e9 100644
--- a/programs/squads_smart_account_program/src/instructions/transaction_execute.rs
+++ b/programs/squads_smart_account_program/src/instructions/transaction_execute.rs
@@ -1,23 +1,28 @@
use anchor_lang::prelude::*;
+use crate::consensus_trait::Consensus;
+use crate::consensus_trait::ConsensusAccountType;
use crate::errors::*;
+use crate::events::*;
+use crate::interface::consensus::ConsensusAccount;
+use crate::program::SquadsSmartAccountProgram;
use crate::state::*;
use crate::utils::*;
#[derive(Accounts)]
pub struct ExecuteTransaction<'info> {
#[account(
- seeds = [SEED_PREFIX, SEED_SETTINGS, settings.seed.to_le_bytes().as_ref()],
- bump = settings.bump,
+ mut,
+ constraint = consensus_account.check_derivation(consensus_account.key()).is_ok()
)]
- pub settings: Box>,
+ pub consensus_account: InterfaceAccount<'info, ConsensusAccount>,
/// The proposal account associated with the transaction.
#[account(
mut,
seeds = [
SEED_PREFIX,
- settings.key().as_ref(),
+ consensus_account.key().as_ref(),
SEED_TRANSACTION,
&transaction.index.to_le_bytes(),
SEED_PROPOSAL,
@@ -30,37 +35,47 @@ pub struct ExecuteTransaction<'info> {
#[account(
seeds = [
SEED_PREFIX,
- settings.key().as_ref(),
+ consensus_account.key().as_ref(),
SEED_TRANSACTION,
&transaction.index.to_le_bytes(),
],
- bump = transaction.bump,
+ bump
)]
pub transaction: Account<'info, Transaction>,
pub signer: Signer<'info>,
- // `remaining_accounts` must include the following accounts in the exact order:
+ pub program: Program<'info, SquadsSmartAccountProgram>,
+ // `remaining_accounts` must include the following accounts in the exact
+ // order:
+ // For transaction execution:
// 1. AddressLookupTable accounts in the order they appear in `message.address_table_lookups`.
// 2. Accounts in the order they appear in `message.account_keys`.
// 3. Accounts in the order they appear in `message.address_table_lookups`.
+ //
+ // For policy execution:
+ // 1. Settings account if the policy has a settings state expiration
+ // 2. Any remaining accounts associated with the policy
}
-impl ExecuteTransaction<'_> {
- fn validate(&self) -> Result<()> {
+impl<'info> ExecuteTransaction<'info> {
+ fn validate(&self, ctx: &Context>) -> Result<()> {
let Self {
- settings,
+ consensus_account,
proposal,
signer,
..
} = self;
+ // Check if the consensus account is active
+ consensus_account.is_active(&ctx.remaining_accounts)?;
+
// signer
require!(
- settings.is_signer(signer.key()).is_some(),
+ consensus_account.is_signer(signer.key()).is_some(),
SmartAccountError::NotASigner
);
require!(
- settings.signer_has_permission(signer.key(), Permission::Execute),
+ consensus_account.signer_has_permission(signer.key(), Permission::Execute),
SmartAccountError::Unauthorized
);
@@ -68,7 +83,8 @@ impl ExecuteTransaction<'_> {
match proposal.status {
ProposalStatus::Approved { timestamp } => {
require!(
- Clock::get()?.unix_timestamp - timestamp >= i64::from(settings.time_lock),
+ Clock::get()?.unix_timestamp - timestamp
+ >= i64::from(consensus_account.time_lock()),
SmartAccountError::TimeLockNotReleased
);
}
@@ -84,73 +100,154 @@ impl ExecuteTransaction<'_> {
/// Execute the smart account transaction.
/// The transaction must be `Approved`.
- #[access_control(ctx.accounts.validate())]
- pub fn execute_transaction(ctx: Context) -> Result<()> {
- let settings = &mut ctx.accounts.settings;
+ #[access_control(ctx.accounts.validate(&ctx))]
+ pub fn execute_transaction(ctx: Context<'_, '_, 'info, 'info, Self>) -> Result<()> {
+ let consensus_account = &mut ctx.accounts.consensus_account;
let proposal = &mut ctx.accounts.proposal;
- // NOTE: After `take()` is called, the Transaction is reduced to
- // its default empty value, which means it should no longer be referenced or
- // used after this point to avoid faulty behavior.
- // Instead only make use of the returned `transaction` value.
- let transaction = ctx.accounts.transaction.take();
+ let transaction = &ctx.accounts.transaction;
- let settings_key = settings.key();
- let transaction_key = ctx.accounts.transaction.key();
+ let consensus_account_key = consensus_account.key();
+ let transaction_key = transaction.key();
+ let transaction_payload = &transaction.payload;
- let smart_account_seeds = &[
- SEED_PREFIX,
- settings_key.as_ref(),
- SEED_SMART_ACCOUNT,
- &transaction.account_index.to_le_bytes(),
- &[transaction.account_bump],
- ];
-
- let transaction_message = transaction.message;
- let num_lookups = transaction_message.address_table_lookups.len();
-
- let message_account_infos = ctx
- .remaining_accounts
- .get(num_lookups..)
- .ok_or(SmartAccountError::InvalidNumberOfAccounts)?;
- let address_lookup_table_account_infos = ctx
- .remaining_accounts
- .get(..num_lookups)
- .ok_or(SmartAccountError::InvalidNumberOfAccounts)?;
-
- let smart_account_pubkey =
- Pubkey::create_program_address(smart_account_seeds, ctx.program_id).unwrap();
-
- let (ephemeral_signer_keys, ephemeral_signer_seeds) =
- derive_ephemeral_signers(transaction_key, &transaction.ephemeral_signer_bumps);
-
- let executable_message = ExecutableTransactionMessage::new_validated(
- transaction_message,
- message_account_infos,
- address_lookup_table_account_infos,
- &smart_account_pubkey,
- &ephemeral_signer_keys,
- )?;
-
- let protected_accounts = &[proposal.key()];
-
- // Execute the transaction message instructions one-by-one.
- // NOTE: `execute_message()` calls `self.to_instructions_and_accounts()`
- // which in turn calls `take()` on
- // `self.message.instructions`, therefore after this point no more
- // references or usages of `self.message` should be made to avoid
- // faulty behavior.
- executable_message.execute_message(
- smart_account_seeds,
- &ephemeral_signer_seeds,
- protected_accounts,
- )?;
+ // Log authority info
+ let log_authority_info = LogAuthorityInfo {
+ authority: consensus_account.to_account_info(),
+ authority_seeds: consensus_account.get_signer_seeds(),
+ bump: consensus_account.bump(),
+ program: ctx.accounts.program.to_account_info(),
+ };
+
+ match consensus_account.account_type() {
+ ConsensusAccountType::Settings => {
+ let transaction_payload = transaction_payload.transaction_payload()?;
+ let smart_account_seeds = &[
+ SEED_PREFIX,
+ consensus_account_key.as_ref(),
+ SEED_SMART_ACCOUNT,
+ &transaction_payload.account_index.to_le_bytes(),
+ ];
+
+ let (smart_account_key, smart_account_bump) =
+ Pubkey::find_program_address(smart_account_seeds, &ctx.program_id);
+
+ let smart_account_signer_seeds = &[
+ smart_account_seeds[0],
+ smart_account_seeds[1],
+ smart_account_seeds[2],
+ smart_account_seeds[3],
+ &[smart_account_bump],
+ ];
+
+ let num_lookups = transaction_payload.message.address_table_lookups.len();
+
+ let message_account_infos = &ctx
+ .remaining_accounts
+ .get(num_lookups..)
+ .ok_or(SmartAccountError::InvalidNumberOfAccounts)?;
+ let address_lookup_table_account_infos = &ctx
+ .remaining_accounts
+ .get(..num_lookups)
+ .ok_or(SmartAccountError::InvalidNumberOfAccounts)?;
+
+ let (ephemeral_signer_keys, ephemeral_signer_seeds) = derive_ephemeral_signers(
+ transaction_key,
+ &transaction_payload.ephemeral_signer_bumps,
+ );
+
+ let executable_message = ExecutableTransactionMessage::new_validated(
+ transaction_payload.message.clone(),
+ message_account_infos,
+ address_lookup_table_account_infos,
+ &smart_account_key,
+ &ephemeral_signer_keys,
+ )?;
+
+ let protected_accounts = &[proposal.key()];
+
+ // Execute the transaction message instructions one-by-one.
+ // NOTE: `execute_message()` calls `self.to_instructions_and_accounts()`
+ // which in turn calls `take()` on
+ // `self.message.instructions`, therefore after this point no more
+ // references or usages of `self.message` should be made to avoid
+ // faulty behavior.
+ executable_message.execute_message(
+ smart_account_signer_seeds,
+ &ephemeral_signer_seeds,
+ protected_accounts,
+ )?;
+ }
+ ConsensusAccountType::Policy => {
+ let policy_payload = transaction_payload.policy_payload()?;
+ // Extract the policy from the consensus account and execute using dispatch
+ let policy = consensus_account.policy()?;
+ // Determine account offset based on policy expiration type
+ let account_offset = policy
+ .expiration
+ .as_ref()
+ .map(|exp| match exp {
+ // The settings is the first extra remaining account
+ PolicyExpiration::SettingsState(_) => 1,
+ _ => 0,
+ })
+ .unwrap_or(0);
+
+ let remaining_accounts = &ctx.remaining_accounts[account_offset..];
+
+ policy.execute(
+ Some(&transaction),
+ Some(&proposal),
+ &policy_payload.payload,
+ remaining_accounts,
+ )?;
+
+ // Policy may updated during execution, log the event
+ let policy_event = PolicyEvent {
+ event_type: PolicyEventType::UpdateDuringExecution,
+ settings_pubkey: policy.settings,
+ policy_pubkey: consensus_account_key,
+ policy: Some(policy.clone()),
+ };
+ SmartAccountEvent::PolicyEvent(policy_event).log(&log_authority_info)?;
+ }
+ }
// Mark the proposal as executed.
proposal.status = ProposalStatus::Executed {
timestamp: Clock::get()?.unix_timestamp,
};
+ // Check the account invariants
+ consensus_account.invariant()?;
+
+
+ // Log the execution event
+ let execute_event = TransactionEvent {
+ consensus_account: consensus_account.key(),
+ consensus_account_type: consensus_account.account_type(),
+ event_type: TransactionEventType::Execute,
+ transaction_pubkey: ctx.accounts.transaction.key(),
+ transaction_index: transaction.index,
+ signer: Some(ctx.accounts.signer.key()),
+ memo: None,
+ transaction_content: Some(TransactionContent::Transaction(transaction.clone().into_inner())),
+ };
+
+ // Log the proposal vote event with execution state
+ let proposal_event = ProposalEvent {
+ event_type: ProposalEventType::Execute,
+ consensus_account: consensus_account.key(),
+ consensus_account_type: consensus_account.account_type(),
+ proposal_pubkey: proposal.key(),
+ transaction_index: transaction.index,
+ signer: Some(ctx.accounts.signer.key()),
+ memo: None,
+ proposal: Some(proposal.clone().into_inner()),
+ };
+ SmartAccountEvent::TransactionEvent(execute_event).log(&log_authority_info)?;
+ SmartAccountEvent::ProposalEvent(proposal_event).log(&log_authority_info)?;
+
Ok(())
}
}
diff --git a/programs/squads_smart_account_program/src/instructions/transaction_execute_sync.rs b/programs/squads_smart_account_program/src/instructions/transaction_execute_sync.rs
index c5d8ec8..a4bdf9f 100644
--- a/programs/squads_smart_account_program/src/instructions/transaction_execute_sync.rs
+++ b/programs/squads_smart_account_program/src/instructions/transaction_execute_sync.rs
@@ -1,7 +1,8 @@
-use account_events::SynchronousTransactionEvent;
use anchor_lang::prelude::*;
use crate::{
+ consensus::ConsensusAccount,
+ consensus_trait::{Consensus, ConsensusAccountType},
errors::*,
events::*,
program::SquadsSmartAccountProgram,
@@ -12,104 +13,233 @@ use crate::{
use super::CompiledInstruction;
+#[derive(AnchorSerialize, AnchorDeserialize)]
+pub enum SyncPayload {
+ Transaction(Vec),
+ Policy(PolicyPayload),
+}
+
+impl SyncPayload {
+ pub fn to_transaction_payload(&self) -> Result<&Vec> {
+ match self {
+ SyncPayload::Transaction(payload) => Ok(payload),
+ _ => err!(SmartAccountError::InvalidPayload),
+ }
+ }
+
+ pub fn to_policy_payload(&self) -> Result<&PolicyPayload> {
+ match self {
+ SyncPayload::Policy(payload) => Ok(payload),
+ _ => err!(SmartAccountError::InvalidPayload),
+ }
+ }
+}
#[derive(AnchorSerialize, AnchorDeserialize)]
pub struct SyncTransactionArgs {
- /// The index of the smart account this transaction is for
pub account_index: u8,
- /// The number of signers to reach threshold and adequate permissions
pub num_signers: u8,
- /// Expected to be serialized as a SmallVec
- pub instructions: Vec,
+ pub payload: SyncPayload,
}
#[derive(Accounts)]
pub struct SyncTransaction<'info> {
#[account(
- seeds = [SEED_PREFIX, SEED_SETTINGS, settings.seed.to_le_bytes().as_ref()],
- bump = settings.bump,
+ mut,
+ constraint = consensus_account.check_derivation(consensus_account.key()).is_ok(),
)]
- pub settings: Box>,
+ pub consensus_account: Box>,
pub program: Program<'info, SquadsSmartAccountProgram>,
// `remaining_accounts` must include the following accounts in the exact order:
// 1. The exact amount of signers required to reach the threshold
- // 2. Any remaining accounts associated with the instructions
+ // 2. For transaction execution:
+ // 2.1. Any remaining accounts associated with the instructions
+ // 3. For policy execution:
+ // 3.1 Settings account if the policy has a settings state expiration
+ // 3.2 Any remaining accounts associated with the policy
}
-impl SyncTransaction<'_> {
- #[access_control(validate_synchronous_consensus( &ctx.accounts.settings, args.num_signers, &ctx.remaining_accounts))]
- pub fn sync_transaction(ctx: Context, args: SyncTransactionArgs) -> Result<()> {
+impl<'info> SyncTransaction<'info> {
+ fn validate(
+ &self,
+ args: &SyncTransactionArgs,
+ remaining_accounts: &[AccountInfo],
+ ) -> Result<()> {
+ let Self {
+ consensus_account, ..
+ } = self;
+
+ // Check that the consensus account is active (policy)
+ consensus_account.is_active(&remaining_accounts[args.num_signers as usize..])?;
+
+ // Validate policy payload if necessary
+ if consensus_account.account_type() == ConsensusAccountType::Policy {
+ let policy = consensus_account.read_only_policy()?;
+ match &args.payload {
+ SyncPayload::Policy(payload) => {
+ // Validate the payload against the policy state
+ policy.validate_payload(PolicyExecutionContext::Synchronous, payload)?;
+ }
+ _ => {
+ return Err(SmartAccountError::ProgramInteractionAsyncPayloadNotAllowedWithSyncTransaction.into());
+ }
+ }
+ }
+
+ // Synchronous consensus validation
+ validate_synchronous_consensus(&consensus_account, args.num_signers, remaining_accounts)
+ }
+}
+
+impl<'info> SyncTransaction<'info> {
+ #[access_control(ctx.accounts.validate(&args, &ctx.remaining_accounts))]
+ pub fn sync_transaction(
+ ctx: Context<'_, '_, 'info, 'info, Self>,
+ args: SyncTransactionArgs,
+ ) -> Result<()> {
// Readonly Accounts
- let settings = &ctx.accounts.settings;
-
- let settings_key = settings.key();
- // Deserialize the instructions
- let compiled_instructions =
- SmallVec::::try_from_slice(&args.instructions)
- .map_err(|_| SmartAccountError::InvalidInstructionArgs)?;
- // Convert to SmartAccountCompiledInstruction
- let settings_compiled_instructions: Vec =
- Vec::from(compiled_instructions)
- .into_iter()
- .map(SmartAccountCompiledInstruction::from)
- .collect();
-
- let smart_account_seeds = &[
- SEED_PREFIX,
- settings_key.as_ref(),
- SEED_SMART_ACCOUNT,
- &args.account_index.to_le_bytes(),
- ];
-
- let (smart_account_pubkey, smart_account_bump) =
- Pubkey::find_program_address(smart_account_seeds, ctx.program_id);
-
- // Get the signer seeds for the smart account
- let smart_account_signer_seeds = &[
- smart_account_seeds[0],
- smart_account_seeds[1],
- smart_account_seeds[2],
- smart_account_seeds[3],
- &[smart_account_bump],
- ];
-
- let executable_message = SynchronousTransactionMessage::new_validated(
- &settings.key(),
- &settings,
- &smart_account_pubkey,
- settings_compiled_instructions,
- &ctx.remaining_accounts,
- )?;
-
- // Execute the transaction message instructions one-by-one.
- // NOTE: `execute_message()` calls `self.to_instructions_and_accounts()`
- // which in turn calls `take()` on
- // `self.message.instructions`, therefore after this point no more
- // references or usages of `self.message` should be made to avoid
- // faulty behavior.
- executable_message.execute(smart_account_signer_seeds)?;
-
- // Log the event
- let event = SynchronousTransactionEvent {
- settings_pubkey: settings.key(),
- signers: ctx.remaining_accounts[..args.num_signers as usize]
- .iter()
- .map(|acc| acc.key.clone())
- .collect(),
- account_index: args.account_index,
- instructions: executable_message.instructions,
- instruction_accounts: executable_message
- .accounts
- .iter()
- .map(|a| a.key.clone())
- .collect(),
- };
+ let consensus_account = &mut ctx.accounts.consensus_account;
+ // Remove the signers from the remaining accounts
+ let remaining_accounts = &ctx.remaining_accounts[args.num_signers as usize..];
+
+ let consensus_account_key = consensus_account.key();
+
+ // Log authority info
let log_authority_info = LogAuthorityInfo {
- authority: settings.to_account_info(),
- authority_seeds: get_settings_signer_seeds(settings.seed),
- bump: settings.bump,
+ authority: consensus_account.to_account_info(),
+ authority_seeds: consensus_account.get_signer_seeds(),
+ bump: consensus_account.bump(),
program: ctx.accounts.program.to_account_info(),
};
- SmartAccountEvent::SynchronousTransactionEvent(event).log(&log_authority_info)?;
+ let event = match consensus_account.account_type() {
+ ConsensusAccountType::Settings => {
+ // Get the payload
+ let payload = args.payload.to_transaction_payload()?;
+
+ let settings = consensus_account.read_only_settings()?;
+ let settings_key = consensus_account_key;
+ // Deserialize the instructions
+ let compiled_instructions =
+ SmallVec::::try_from_slice(&payload)
+ .map_err(|_| SmartAccountError::InvalidInstructionArgs)?;
+ // Convert to SmartAccountCompiledInstruction
+ let settings_compiled_instructions: Vec =
+ Vec::from(compiled_instructions)
+ .into_iter()
+ .map(SmartAccountCompiledInstruction::from)
+ .collect();
+
+ let smart_account_seeds = &[
+ SEED_PREFIX,
+ settings_key.as_ref(),
+ SEED_SMART_ACCOUNT,
+ &args.account_index.to_le_bytes(),
+ ];
+
+ let (smart_account_pubkey, smart_account_bump) =
+ Pubkey::find_program_address(smart_account_seeds, ctx.program_id);
+
+ // Get the signer seeds for the smart account
+ let smart_account_signer_seeds = &[
+ smart_account_seeds[0],
+ smart_account_seeds[1],
+ smart_account_seeds[2],
+ smart_account_seeds[3],
+ &[smart_account_bump],
+ ];
+
+ let executable_message = SynchronousTransactionMessage::new_validated(
+ &settings_key,
+ &smart_account_pubkey,
+ &settings.signers,
+ &settings_compiled_instructions,
+ &remaining_accounts,
+ )?;
+
+ // Execute the transaction message instructions one-by-one.
+ // NOTE: `execute_message()` calls `self.to_instructions_and_accounts()`
+ // which in turn calls `take()` on
+ // `self.message.instructions`, therefore after this point no more
+ // references or usages of `self.message` should be made to avoid
+ // faulty behavior.
+ executable_message.execute(smart_account_signer_seeds)?;
+
+ // Create the event
+ let event = SynchronousTransactionEventV2 {
+ consensus_account: settings_key,
+ consensus_account_type: ConsensusAccountType::Settings,
+ payload: SynchronousTransactionEventPayload::TransactionPayload {
+ account_index: args.account_index,
+ instructions: executable_message.instructions.to_vec(),
+ },
+ signers: ctx.remaining_accounts[..args.num_signers as usize]
+ .iter()
+ .map(|acc| acc.key.clone())
+ .collect(),
+ instruction_accounts: executable_message
+ .accounts
+ .iter()
+ .map(|a| a.key.clone())
+ .collect(),
+ };
+ event
+ }
+ ConsensusAccountType::Policy => {
+ let payload = args.payload.to_policy_payload()?;
+ let policy = consensus_account.policy()?;
+
+ // Determine account offset based on policy expiration type
+ let account_offset = policy
+ .expiration
+ .as_ref()
+ .map(|exp| match exp {
+ // The settings is the first extra remaining account
+ PolicyExpiration::SettingsState(_) => 1,
+ _ => 0,
+ })
+ .unwrap_or(0);
+
+ // Potentially remove the settings account for expiration from
+ // the remaining accounts
+ let remaining_accounts = &remaining_accounts[account_offset..];
+
+ // Execute the policy
+ policy.execute(None, None, payload, &remaining_accounts)?;
+
+ // Policy may updated during execution, log the event
+ let policy_update_event = PolicyEvent {
+ event_type: PolicyEventType::UpdateDuringExecution,
+ settings_pubkey: policy.settings,
+ policy_pubkey: consensus_account_key,
+ policy: Some(policy.clone()),
+ };
+
+ SmartAccountEvent::PolicyEvent(policy_update_event).log(&log_authority_info)?;
+
+ // Create the event
+ let event = SynchronousTransactionEventV2 {
+ consensus_account: consensus_account_key,
+ consensus_account_type: ConsensusAccountType::Policy,
+ payload: SynchronousTransactionEventPayload::PolicyPayload {
+ policy_payload: payload.clone(),
+ },
+ signers: ctx.remaining_accounts[..args.num_signers as usize]
+ .iter()
+ .map(|acc| acc.key.clone())
+ .collect(),
+ instruction_accounts: remaining_accounts
+ .iter()
+ .map(|acc| acc.key.clone())
+ .collect(),
+ };
+ event
+ }
+ };
+
+ // Check the policy invariant
+ consensus_account.invariant()?;
+
+ SmartAccountEvent::SynchronousTransactionEventV2(event).log(&log_authority_info)?;
+
Ok(())
}
}
diff --git a/programs/squads_smart_account_program/src/instructions/transaction_execute_sync_legacy.rs b/programs/squads_smart_account_program/src/instructions/transaction_execute_sync_legacy.rs
new file mode 100644
index 0000000..7a573de
--- /dev/null
+++ b/programs/squads_smart_account_program/src/instructions/transaction_execute_sync_legacy.rs
@@ -0,0 +1,128 @@
+use account_events::SynchronousTransactionEvent;
+use anchor_lang::prelude::*;
+
+use crate::{
+ consensus::ConsensusAccount,
+ consensus_trait::{Consensus, ConsensusAccountType},
+ errors::*,
+ events::*,
+ program::SquadsSmartAccountProgram,
+ state::*,
+ utils::{validate_synchronous_consensus, SynchronousTransactionMessage},
+ SmallVec,
+};
+
+use super::CompiledInstruction;
+
+#[derive(AnchorSerialize, AnchorDeserialize)]
+pub struct LegacySyncTransactionArgs {
+ /// The index of the smart account this transaction is for
+ pub account_index: u8,
+ /// The number of signers to reach threshold and adequate permissions
+ pub num_signers: u8,
+ /// Expected to be serialized as a SmallVec
+ pub instructions: Vec,
+}
+
+#[derive(Accounts)]
+pub struct LegacySyncTransaction<'info> {
+ #[account(
+ constraint = consensus_account.check_derivation(consensus_account.key()).is_ok(),
+ // Legacy sync transactions only support settings
+ constraint = consensus_account.account_type() == ConsensusAccountType::Settings
+ )]
+ pub consensus_account: Box>,
+ pub program: Program<'info, SquadsSmartAccountProgram>,
+ // `remaining_accounts` must include the following accounts in the exact order:
+ // 1. The exact amount of signers required to reach the threshold
+ // 2. Any remaining accounts associated with the instructions
+}
+
+impl LegacySyncTransaction<'_> {
+ fn validate(
+ &self,
+ args: &LegacySyncTransactionArgs,
+ remaining_accounts: &[AccountInfo],
+ ) -> Result<()> {
+ let Self { consensus_account, .. } = self;
+ validate_synchronous_consensus(&consensus_account, args.num_signers, remaining_accounts)
+ }
+ #[access_control(ctx.accounts.validate(&args, &ctx.remaining_accounts))]
+ pub fn sync_transaction(ctx: Context, args: LegacySyncTransactionArgs) -> Result<()> {
+ // Wrapper consensus account
+ let consensus_account = &ctx.accounts.consensus_account;
+ let settings = consensus_account.read_only_settings()?;
+ let settings_key = consensus_account.key();
+ let settings_account_info = consensus_account.to_account_info();
+
+ // Deserialize the instructions
+ let compiled_instructions =
+ SmallVec::::try_from_slice(&args.instructions)
+ .map_err(|_| SmartAccountError::InvalidInstructionArgs)?;
+ // Convert to SmartAccountCompiledInstruction
+ let settings_compiled_instructions: Vec =
+ Vec::from(compiled_instructions)
+ .into_iter()
+ .map(SmartAccountCompiledInstruction::from)
+ .collect();
+
+ let smart_account_seeds = &[
+ SEED_PREFIX,
+ settings_key.as_ref(),
+ SEED_SMART_ACCOUNT,
+ &args.account_index.to_le_bytes(),
+ ];
+
+ let (smart_account_pubkey, smart_account_bump) =
+ Pubkey::find_program_address(smart_account_seeds, ctx.program_id);
+
+ // Get the signer seeds for the smart account
+ let smart_account_signer_seeds = &[
+ smart_account_seeds[0],
+ smart_account_seeds[1],
+ smart_account_seeds[2],
+ smart_account_seeds[3],
+ &[smart_account_bump],
+ ];
+
+ let executable_message = SynchronousTransactionMessage::new_validated(
+ &settings_key,
+ &smart_account_pubkey,
+ &settings.signers,
+ &settings_compiled_instructions,
+ &ctx.remaining_accounts,
+ )?;
+
+ // Execute the transaction message instructions one-by-one.
+ // NOTE: `execute_message()` calls `self.to_instructions_and_accounts()`
+ // which in turn calls `take()` on
+ // `self.message.instructions`, therefore after this point no more
+ // references or usages of `self.message` should be made to avoid
+ // faulty behavior.
+ executable_message.execute(smart_account_signer_seeds)?;
+
+ // Log the event
+ let event = SynchronousTransactionEvent {
+ settings_pubkey: settings_key,
+ signers: ctx.remaining_accounts[..args.num_signers as usize]
+ .iter()
+ .map(|acc| acc.key.clone())
+ .collect(),
+ account_index: args.account_index,
+ instructions: executable_message.instructions.to_vec(),
+ instruction_accounts: executable_message
+ .accounts
+ .iter()
+ .map(|a| a.key.clone())
+ .collect(),
+ };
+ let log_authority_info = LogAuthorityInfo {
+ authority: settings_account_info,
+ authority_seeds: get_settings_signer_seeds(settings.seed),
+ bump: settings.bump,
+ program: ctx.accounts.program.to_account_info(),
+ };
+ SmartAccountEvent::SynchronousTransactionEvent(event).log(&log_authority_info)?;
+ Ok(())
+ }
+}
diff --git a/programs/squads_smart_account_program/src/interface/consensus.rs b/programs/squads_smart_account_program/src/interface/consensus.rs
new file mode 100644
index 0000000..54c43f8
--- /dev/null
+++ b/programs/squads_smart_account_program/src/interface/consensus.rs
@@ -0,0 +1,216 @@
+use anchor_lang::{
+ prelude::{AccountInfo, Pubkey},
+ AccountDeserialize, AccountSerialize, Discriminator, Owners, Result,
+};
+
+use crate::{
+ errors::SmartAccountError, get_policy_signer_seeds, get_settings_signer_seeds, state::{Policy, Settings}, SmartAccountSigner
+};
+
+use super::consensus_trait::{Consensus, ConsensusAccountType};
+
+#[derive(Clone)]
+pub enum ConsensusAccount {
+ Settings(Settings),
+ Policy(Policy),
+}
+
+static OWNERS: [Pubkey; 1] = [crate::ID];
+
+// Implemented for InterfaceAccount
+impl Owners for ConsensusAccount {
+ // Just our own program ID, since the interface account is just to wrap the trait
+ fn owners() -> &'static [Pubkey] {
+ &OWNERS
+ }
+}
+
+// Implemented for InterfaceAccount
+impl AccountSerialize for ConsensusAccount {
+ fn try_serialize(&self, writer: &mut W) -> anchor_lang::Result<()> {
+ match self {
+ ConsensusAccount::Settings(settings) => settings.try_serialize(writer),
+ ConsensusAccount::Policy(policy) => policy.try_serialize(writer),
+ }
+ }
+}
+
+// Implemented for InterfaceAccount
+impl AccountDeserialize for ConsensusAccount {
+ fn try_deserialize(reader: &mut &[u8]) -> anchor_lang::Result {
+ let discriminator: [u8; 8] = reader[..8].try_into().unwrap();
+ match discriminator {
+ Settings::DISCRIMINATOR => Ok(ConsensusAccount::Settings(Settings::try_deserialize(
+ reader,
+ )?)),
+ Policy::DISCRIMINATOR => Ok(ConsensusAccount::Policy(Policy::try_deserialize(reader)?)),
+ _ => Err(anchor_lang::error::Error::from(
+ anchor_lang::error::ErrorCode::AccountDiscriminatorMismatch,
+ )),
+ }
+ }
+
+ fn try_deserialize_unchecked(buf: &mut &[u8]) -> anchor_lang::Result {
+ let discriminator: [u8; 8] = buf[..8].try_into().unwrap();
+ match discriminator {
+ Settings::DISCRIMINATOR => Ok(ConsensusAccount::Settings(
+ Settings::try_deserialize_unchecked(buf)?,
+ )),
+ Policy::DISCRIMINATOR => Ok(ConsensusAccount::Policy(
+ Policy::try_deserialize_unchecked(buf)?,
+ )),
+ _ => Err(anchor_lang::error::Error::from(
+ anchor_lang::error::ErrorCode::AccountDiscriminatorMismatch,
+ )),
+ }
+ }
+}
+
+impl ConsensusAccount {
+ /// Returns the number of signers in the consensus account.
+ pub fn signers_len(&self) -> usize {
+ self.signers().len()
+ }
+
+ /// Returns the bump for the consensus account.
+ pub fn bump(&self) -> u8 {
+ match self {
+ ConsensusAccount::Settings(settings) => settings.bump,
+ ConsensusAccount::Policy(policy) => policy.bump,
+ }
+ }
+ /// Returns the signer seeds for the consensus account.
+ pub fn get_signer_seeds(&self) -> Vec> {
+ match self {
+ ConsensusAccount::Settings(settings) => get_settings_signer_seeds(settings.seed),
+ ConsensusAccount::Policy(policy) => get_policy_signer_seeds(&policy.settings, policy.seed),
+ }
+ }
+ /// Returns the settings if the consensus account is a settings.
+ pub fn settings(&mut self) -> Result<&mut Settings> {
+ match self {
+ ConsensusAccount::Settings(settings) => Ok(settings),
+ ConsensusAccount::Policy(_) => {
+ Err(SmartAccountError::ConsensusAccountNotSettings.into())
+ }
+ }
+ }
+
+ /// Returns the settings if the consensus account is a settings.
+ pub fn read_only_settings(&self) -> Result<&Settings> {
+ match self {
+ ConsensusAccount::Settings(settings) => Ok(settings),
+ ConsensusAccount::Policy(_) => {
+ Err(SmartAccountError::ConsensusAccountNotSettings.into())
+ }
+ }
+ }
+
+ /// Returns the policy if the consensus account is a policy.
+ pub fn policy(&mut self) -> Result<&mut Policy> {
+ match self {
+ ConsensusAccount::Settings(_) => {
+ return Err(SmartAccountError::ConsensusAccountNotPolicy.into())
+ }
+ ConsensusAccount::Policy(policy) => Ok(policy),
+ }
+ }
+
+ /// Returns the policy if the consensus account is a policy.
+ pub fn read_only_policy(&self) -> Result<&Policy> {
+ match self {
+ ConsensusAccount::Settings(_) => {
+ return Err(SmartAccountError::ConsensusAccountNotPolicy.into())
+ }
+ ConsensusAccount::Policy(policy) => Ok(policy),
+ }
+ }
+
+ /// Helper method to delegate to the underlying consensus implementation
+ fn as_consensus(&self) -> &dyn Consensus {
+ match self {
+ ConsensusAccount::Settings(settings) => settings,
+ ConsensusAccount::Policy(policy) => policy,
+ }
+ }
+
+ /// Helper method to delegate to the underlying consensus implementation (mutable)
+ fn as_consensus_mut(&mut self) -> &mut dyn Consensus {
+ match self {
+ ConsensusAccount::Settings(settings) => settings,
+ ConsensusAccount::Policy(policy) => policy,
+ }
+ }
+}
+
+impl Consensus for ConsensusAccount {
+ fn is_active(&self, accounts: &[AccountInfo]) -> Result<()> {
+ self.as_consensus().is_active(accounts)
+ }
+
+ fn check_derivation(&self, key: Pubkey) -> Result<()> {
+ self.as_consensus().check_derivation(key)
+ }
+
+ fn account_type(&self) -> ConsensusAccountType {
+ self.as_consensus().account_type()
+ }
+
+ fn signers(&self) -> &[SmartAccountSigner] {
+ self.as_consensus().signers()
+ }
+
+ fn threshold(&self) -> u16 {
+ self.as_consensus().threshold()
+ }
+
+ fn time_lock(&self) -> u32 {
+ self.as_consensus().time_lock()
+ }
+
+ fn transaction_index(&self) -> u64 {
+ self.as_consensus().transaction_index()
+ }
+
+ fn set_transaction_index(&mut self, transaction_index: u64) -> Result<()> {
+ self.as_consensus_mut()
+ .set_transaction_index(transaction_index)
+ }
+
+ fn stale_transaction_index(&self) -> u64 {
+ self.as_consensus().stale_transaction_index()
+ }
+
+ fn invalidate_prior_transactions(&mut self) {
+ self.as_consensus_mut().invalidate_prior_transactions()
+ }
+
+ fn is_signer(&self, signer_pubkey: Pubkey) -> Option {
+ self.as_consensus().is_signer(signer_pubkey)
+ }
+
+ fn signer_has_permission(&self, signer_pubkey: Pubkey, permission: crate::Permission) -> bool {
+ self.as_consensus()
+ .signer_has_permission(signer_pubkey, permission)
+ }
+
+ fn num_voters(&self) -> usize {
+ self.as_consensus().num_voters()
+ }
+
+ fn num_proposers(&self) -> usize {
+ self.as_consensus().num_proposers()
+ }
+
+ fn num_executors(&self) -> usize {
+ self.as_consensus().num_executors()
+ }
+
+ fn cutoff(&self) -> usize {
+ self.as_consensus().cutoff()
+ }
+
+ fn invariant(&self) -> Result<()> {
+ self.as_consensus().invariant()
+ }
+}
diff --git a/programs/squads_smart_account_program/src/interface/consensus_trait.rs b/programs/squads_smart_account_program/src/interface/consensus_trait.rs
new file mode 100644
index 0000000..08ce7eb
--- /dev/null
+++ b/programs/squads_smart_account_program/src/interface/consensus_trait.rs
@@ -0,0 +1,78 @@
+use anchor_lang::prelude::*;
+use borsh::{BorshDeserialize, BorshSerialize};
+
+use crate::{Permission, SmartAccountSigner};
+
+#[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Debug)]
+pub enum ConsensusAccountType {
+ Settings,
+ Policy,
+}
+
+pub trait Consensus {
+ fn account_type(&self) -> ConsensusAccountType;
+ fn check_derivation(&self, key: Pubkey) -> Result<()>;
+ fn is_active(&self, accounts: &[AccountInfo]) -> Result<()>;
+
+ // Core consensus fields
+ fn signers(&self) -> &[SmartAccountSigner];
+ fn threshold(&self) -> u16;
+ fn time_lock(&self) -> u32;
+ fn transaction_index(&self) -> u64;
+ fn set_transaction_index(&mut self, transaction_index: u64) -> Result<()>;
+ fn stale_transaction_index(&self) -> u64;
+
+ // Returns `Some(index)` if `signer_pubkey` is a signer, with `index` into the `signers` vec.
+ /// `None` otherwise.
+ fn is_signer(&self, signer_pubkey: Pubkey) -> Option {
+ self.signers()
+ .binary_search_by_key(&signer_pubkey, |s| s.key)
+ .ok()
+ }
+
+ fn signer_has_permission(&self, signer_pubkey: Pubkey, permission: Permission) -> bool {
+ match self.is_signer(signer_pubkey) {
+ Some(index) => self.signers()[index].permissions.has(permission),
+ _ => false,
+ }
+ }
+
+ // Permission counting methods
+ fn num_voters(&self) -> usize {
+ self.signers()
+ .iter()
+ .filter(|s| s.permissions.has(Permission::Vote))
+ .count()
+ }
+
+ fn num_proposers(&self) -> usize {
+ self.signers()
+ .iter()
+ .filter(|s| s.permissions.has(Permission::Initiate))
+ .count()
+ }
+
+ fn num_executors(&self) -> usize {
+ self.signers()
+ .iter()
+ .filter(|s| s.permissions.has(Permission::Execute))
+ .count()
+ }
+
+ /// How many "reject" votes are enough to make the transaction "Rejected".
+ /// The cutoff must be such that it is impossible for the remaining voters to reach the approval threshold.
+ /// For example: total voters = 7, threshold = 3, cutoff = 5.
+ fn cutoff(&self) -> usize {
+ self.num_voters()
+ .checked_sub(usize::from(self.threshold()))
+ .unwrap()
+ .checked_add(1)
+ .unwrap()
+ }
+
+ // Stale transaction protection (ported from Settings)
+ fn invalidate_prior_transactions(&mut self);
+
+ // Consensus validation (ported from Settings invariant)
+ fn invariant(&self) -> Result<()>;
+}
diff --git a/programs/squads_smart_account_program/src/interface/mod.rs b/programs/squads_smart_account_program/src/interface/mod.rs
new file mode 100644
index 0000000..55ed2aa
--- /dev/null
+++ b/programs/squads_smart_account_program/src/interface/mod.rs
@@ -0,0 +1,2 @@
+pub mod consensus;
+pub mod consensus_trait;
\ No newline at end of file
diff --git a/programs/squads_smart_account_program/src/lib.rs b/programs/squads_smart_account_program/src/lib.rs
index cf05546..5c36d5d 100644
--- a/programs/squads_smart_account_program/src/lib.rs
+++ b/programs/squads_smart_account_program/src/lib.rs
@@ -10,18 +10,20 @@ use anchor_lang::prelude::*;
#[cfg(not(feature = "no-entrypoint"))]
use solana_security_txt::security_txt;
+pub use events::*;
pub use instructions::ProgramConfig;
pub use instructions::*;
+pub use interface::*;
pub use state::*;
pub use utils::SmallVec;
-pub use events::*;
pub mod allocator;
pub mod errors;
+pub mod events;
pub mod instructions;
+pub mod interface;
pub mod state;
mod utils;
-pub mod events;
#[cfg(not(feature = "no-entrypoint"))]
security_txt! {
@@ -204,7 +206,9 @@ pub mod squads_smart_account_program {
/// Execute a smart account transaction.
/// The transaction must be `Approved`.
- pub fn execute_transaction(ctx: Context) -> Result<()> {
+ pub fn execute_transaction<'info>(
+ ctx: Context<'_, '_, 'info, 'info, ExecuteTransaction<'info>>,
+ ) -> Result<()> {
ExecuteTransaction::execute_transaction(ctx)
}
@@ -255,10 +259,12 @@ pub mod squads_smart_account_program {
}
/// Use a spending limit to transfer tokens from a smart account vault to a destination account.
+ #[deprecated(note = "Use the spending limit policy instead")]
pub fn use_spending_limit(
ctx: Context,
args: UseSpendingLimitArgs,
) -> Result<()> {
+ msg!("Method is being deprecated. Use the new spending limit policy instead");
UseSpendingLimit::use_spending_limit(ctx, args)
}
@@ -278,6 +284,12 @@ pub mod squads_smart_account_program {
CloseTransaction::close_transaction(ctx)
}
+ /// Closes a `Transaction` and the corresponding `Proposal` for
+ /// empty/deleted policies.
+ pub fn close_empty_policy_transaction(ctx: Context) -> Result<()> {
+ CloseEmptyPolicyTransaction::close_empty_policy_transaction(ctx)
+ }
+
/// Closes a `BatchTransaction` belonging to the `batch` and `proposal`.
/// `transaction` can be closed if either:
/// - it's marked as executed within the `batch`;
@@ -297,8 +309,18 @@ pub mod squads_smart_account_program {
}
/// Synchronously execute a transaction
+ #[deprecated(note = "Use `execute_transaction_sync_v2` instead")]
pub fn execute_transaction_sync(
- ctx: Context,
+ ctx: Context,
+ args: LegacySyncTransactionArgs,
+ ) -> Result<()> {
+ msg!("Method is being deprecated. Use `execute_transaction_sync_v2` instead");
+ LegacySyncTransaction::sync_transaction(ctx, args)
+ }
+
+ /// Synchronously execute a policy transaction
+ pub fn execute_transaction_sync_v2<'info>(
+ ctx: Context<'_, '_, 'info, 'info, SyncTransaction<'info>>,
args: SyncTransactionArgs,
) -> Result<()> {
SyncTransaction::sync_transaction(ctx, args)
@@ -312,7 +334,10 @@ pub mod squads_smart_account_program {
SyncSettingsTransaction::sync_settings_transaction(ctx, args)
}
/// Log an event
- pub fn log_event<'info>(ctx: Context<'_, '_, 'info, 'info, LogEvent<'info>>, args: LogEventArgs) -> Result<()> {
+ pub fn log_event<'info>(
+ ctx: Context<'_, '_, 'info, 'info, LogEvent<'info>>,
+ args: LogEventArgsV2,
+ ) -> Result<()> {
LogEvent::log_event(ctx, args)
}
}
diff --git a/programs/squads_smart_account_program/src/state/batch.rs b/programs/squads_smart_account_program/src/state/batch.rs
index ecd9815..a52012e 100644
--- a/programs/squads_smart_account_program/src/state/batch.rs
+++ b/programs/squads_smart_account_program/src/state/batch.rs
@@ -10,7 +10,7 @@ use crate::{TransactionMessage, SmartAccountTransactionMessage};
#[account]
#[derive(InitSpace)]
pub struct Batch {
- /// The settings this belongs to.
+ /// The consensus account (settings or policy) this belongs to.
pub settings: Pubkey,
/// Signer of the smart account who submitted the batch.
pub creator: Pubkey,
diff --git a/programs/squads_smart_account_program/src/state/legacy_transaction.rs b/programs/squads_smart_account_program/src/state/legacy_transaction.rs
new file mode 100644
index 0000000..6fb2331
--- /dev/null
+++ b/programs/squads_smart_account_program/src/state/legacy_transaction.rs
@@ -0,0 +1,244 @@
+use anchor_lang::prelude::*;
+use anchor_lang::solana_program::borsh0_10::get_instance_packed_len;
+
+use crate::errors::*;
+use crate::instructions::{CompiledInstruction, MessageAddressTableLookup, TransactionMessage};
+
+/// Stores data required for tracking the voting and execution status of a smart
+///account transaction.
+/// Smart Account transaction is a transaction that's executed on behalf of the
+/// smart account PDA
+/// and wraps arbitrary Solana instructions, typically calling into other Solana programs.
+#[account]
+#[derive(Default)]
+pub struct LegacyTransaction {
+ /// The consensus account this belongs to.
+ pub smart_account_settings: Pubkey,
+ /// Signer of the Smart Account who submitted the transaction.
+ pub creator: Pubkey,
+ /// The rent collector for the transaction account.
+ pub rent_collector: Pubkey,
+ /// Index of this transaction within the smart account.
+ pub index: u64,
+ /// bump for the transaction seeds.
+ pub bump: u8,
+ /// The account index of the smart account this transaction belongs to.
+ pub account_index: u8,
+ /// Derivation bump of the smart account PDA this transaction belongs to.
+ pub account_bump: u8,
+ /// Derivation bumps for additional signers.
+ /// Some transactions require multiple signers. Often these additional signers are "ephemeral" keypairs
+ /// that are generated on the client with a sole purpose of signing the transaction and be discarded immediately after.
+ /// When wrapping such transactions into smart account ones, we replace these "ephemeral" signing keypairs
+ /// with PDAs derived from the SmartAccountTransaction's `transaction_index`
+ /// and controlled by the Smart Account Program;
+ /// during execution the program includes the seeds of these PDAs into the `invoke_signed` calls,
+ /// thus "signing" on behalf of these PDAs.
+ pub ephemeral_signer_bumps: Vec,
+ /// data required for executing the transaction.
+ pub message: SmartAccountTransactionMessage,
+}
+
+impl LegacyTransaction {
+ pub fn size(ephemeral_signers_length: u8, transaction_message: &[u8]) -> Result {
+ let transaction_message: SmartAccountTransactionMessage =
+ TransactionMessage::deserialize(&mut &transaction_message[..])?.try_into()?;
+ let message_size = get_instance_packed_len(&transaction_message).unwrap_or_default();
+
+ Ok(
+ 8 + // anchor account discriminator
+ 32 + // settings
+ 32 + // creator
+ 32 + // rent_collector
+ 8 + // index
+ 1 + // bump
+ 1 + // account_index
+ 1 + // account_bump
+ (4 + usize::from(ephemeral_signers_length)) + // ephemeral_signers_bumps vec
+ message_size, // message
+ )
+ }
+ /// Reduces the Transaction to its default empty value and moves
+ /// ownership of the data to the caller/return value.
+ pub fn take(&mut self) -> LegacyTransaction {
+ core::mem::take(self)
+ }
+}
+
+#[derive(AnchorSerialize, AnchorDeserialize, Clone, Default, Eq, PartialEq)]
+pub struct SmartAccountTransactionMessage {
+ /// The number of signer pubkeys in the account_keys vec.
+ pub num_signers: u8,
+ /// The number of writable signer pubkeys in the account_keys vec.
+ pub num_writable_signers: u8,
+ /// The number of writable non-signer pubkeys in the account_keys vec.
+ pub num_writable_non_signers: u8,
+ /// Unique account pubkeys (including program IDs) required for execution of the tx.
+ /// The signer pubkeys appear at the beginning of the vec, with writable pubkeys first, and read-only pubkeys following.
+ /// The non-signer pubkeys follow with writable pubkeys first and read-only ones following.
+ /// Program IDs are also stored at the end of the vec along with other non-signer non-writable pubkeys:
+ ///
+ /// ```plaintext
+ /// [pubkey1, pubkey2, pubkey3, pubkey4, pubkey5, pubkey6, pubkey7, pubkey8]
+ /// |---writable---| |---readonly---| |---writable---| |---readonly---|
+ /// |------------signers-------------| |----------non-singers-----------|
+ /// ```
+ pub account_keys: Vec,
+ /// List of instructions making up the tx.
+ pub instructions: Vec,
+ /// List of address table lookups used to load additional accounts
+ /// for this transaction.
+ pub address_table_lookups: Vec,
+}
+
+impl SmartAccountTransactionMessage {
+ /// Returns the number of all the account keys (static + dynamic) in the message.
+ pub fn num_all_account_keys(&self) -> usize {
+ let num_account_keys_from_lookups = self
+ .address_table_lookups
+ .iter()
+ .map(|lookup| lookup.writable_indexes.len() + lookup.readonly_indexes.len())
+ .sum::();
+
+ self.account_keys.len() + num_account_keys_from_lookups
+ }
+
+ /// Returns true if the account at the specified index is a part of static `account_keys` and was requested to be writable.
+ pub fn is_static_writable_index(&self, key_index: usize) -> bool {
+ let num_account_keys = self.account_keys.len();
+ let num_signers = usize::from(self.num_signers);
+ let num_writable_signers = usize::from(self.num_writable_signers);
+ let num_writable_non_signers = usize::from(self.num_writable_non_signers);
+
+ if key_index >= num_account_keys {
+ // `index` is not a part of static `account_keys`.
+ return false;
+ }
+
+ if key_index < num_writable_signers {
+ // `index` is within the range of writable signer keys.
+ return true;
+ }
+
+ if key_index >= num_signers {
+ // `index` is within the range of non-signer keys.
+ let index_into_non_signers = key_index.saturating_sub(num_signers);
+ // Whether `index` is within the range of writable non-signer keys.
+ return index_into_non_signers < num_writable_non_signers;
+ }
+
+ false
+ }
+
+ /// Returns true if the account at the specified index was requested to be a signer.
+ pub fn is_signer_index(&self, key_index: usize) -> bool {
+ key_index < usize::from(self.num_signers)
+ }
+}
+
+impl TryFrom for SmartAccountTransactionMessage {
+ type Error = Error;
+
+ fn try_from(message: TransactionMessage) -> Result {
+ let account_keys: Vec = message.account_keys.into();
+ let instructions: Vec = message.instructions.into();
+ let instructions: Vec = instructions
+ .into_iter()
+ .map(SmartAccountCompiledInstruction::from)
+ .collect();
+ let address_table_lookups: Vec =
+ message.address_table_lookups.into();
+
+ let num_all_account_keys = account_keys.len()
+ + address_table_lookups
+ .iter()
+ .map(|lookup| lookup.writable_indexes.len() + lookup.readonly_indexes.len())
+ .sum::();
+
+ require!(
+ usize::from(message.num_signers) <= account_keys.len(),
+ SmartAccountError::InvalidTransactionMessage
+ );
+ require!(
+ message.num_writable_signers <= message.num_signers,
+ SmartAccountError::InvalidTransactionMessage
+ );
+ require!(
+ usize::from(message.num_writable_non_signers)
+ <= account_keys
+ .len()
+ .saturating_sub(usize::from(message.num_signers)),
+ SmartAccountError::InvalidTransactionMessage
+ );
+
+ // Validate that all program ID indices and account indices are within the bounds of the account keys.
+ for instruction in &instructions {
+ require!(
+ usize::from(instruction.program_id_index) < num_all_account_keys,
+ SmartAccountError::InvalidTransactionMessage
+ );
+
+ for account_index in &instruction.account_indexes {
+ require!(
+ usize::from(*account_index) < num_all_account_keys,
+ SmartAccountError::InvalidTransactionMessage
+ );
+ }
+ }
+
+ Ok(Self {
+ num_signers: message.num_signers,
+ num_writable_signers: message.num_writable_signers,
+ num_writable_non_signers: message.num_writable_non_signers,
+ account_keys,
+ instructions,
+ address_table_lookups: address_table_lookups
+ .into_iter()
+ .map(SmartAccountMessageAddressTableLookup::from)
+ .collect(),
+ })
+ }
+}
+
+/// Concise serialization schema for instructions that make up a transaction.
+/// Closely mimics the Solana transaction wire format.
+#[derive(AnchorSerialize, AnchorDeserialize, Clone, Eq, PartialEq)]
+pub struct SmartAccountCompiledInstruction {
+ pub program_id_index: u8,
+ /// Indices into the tx's `account_keys` list indicating which accounts to pass to the instruction.
+ pub account_indexes: Vec,
+ /// Instruction data.
+ pub data: Vec,
+}
+
+impl From for SmartAccountCompiledInstruction {
+ fn from(compiled_instruction: CompiledInstruction) -> Self {
+ Self {
+ program_id_index: compiled_instruction.program_id_index,
+ account_indexes: compiled_instruction.account_indexes.into(),
+ data: compiled_instruction.data.into(),
+ }
+ }
+}
+
+/// Address table lookups describe an on-chain address lookup table to use
+/// for loading more readonly and writable accounts into a transaction.
+#[derive(AnchorSerialize, AnchorDeserialize, Clone, Eq, PartialEq)]
+pub struct SmartAccountMessageAddressTableLookup {
+ /// Address lookup table account key.
+ pub account_key: Pubkey,
+ /// List of indexes used to load writable accounts.
+ pub writable_indexes: Vec,
+ /// List of indexes used to load readonly accounts.
+ pub readonly_indexes: Vec,
+}
+
+impl From for SmartAccountMessageAddressTableLookup {
+ fn from(m: MessageAddressTableLookup) -> Self {
+ Self {
+ account_key: m.account_key,
+ writable_indexes: m.writable_indexes.into(),
+ readonly_indexes: m.readonly_indexes.into(),
+ }
+ }
+}
diff --git a/programs/squads_smart_account_program/src/state/mod.rs b/programs/squads_smart_account_program/src/state/mod.rs
index bf9d501..9180509 100644
--- a/programs/squads_smart_account_program/src/state/mod.rs
+++ b/programs/squads_smart_account_program/src/state/mod.rs
@@ -1,19 +1,23 @@
pub use self::settings::*;
pub use batch::*;
-pub use settings_transaction::*;
+pub use policies::*;
pub use program_config::*;
pub use proposal::*;
pub use seeds::*;
+pub use settings_transaction::*;
pub use spending_limit::*;
+pub use legacy_transaction::*;
pub use transaction_buffer::*;
pub use transaction::*;
mod batch;
-mod settings_transaction;
-mod settings;
+mod policies;
mod program_config;
mod proposal;
mod seeds;
+mod settings;
+mod settings_transaction;
mod spending_limit;
+mod legacy_transaction;
mod transaction_buffer;
mod transaction;
diff --git a/programs/squads_smart_account_program/src/state/policies/implementations/internal_fund_transfer.rs b/programs/squads_smart_account_program/src/state/policies/implementations/internal_fund_transfer.rs
new file mode 100644
index 0000000..557b730
--- /dev/null
+++ b/programs/squads_smart_account_program/src/state/policies/implementations/internal_fund_transfer.rs
@@ -0,0 +1,447 @@
+use crate::{
+ errors::SmartAccountError, get_smart_account_seeds, state::policies::policy_core::PolicyTrait,
+ SEED_PREFIX, SEED_SMART_ACCOUNT,
+};
+use crate::{PolicyExecutionContext, PolicyPayloadConversionTrait, PolicySizeTrait};
+use anchor_lang::prelude::InterfaceAccount;
+use anchor_lang::{prelude::*, system_program, Ids};
+use anchor_spl::token_interface::{self, TokenAccount, TokenInterface, TransferChecked};
+
+/// == InternalFundTransferPolicy ==
+/// This policy allows for the transfer of SOL and SPL tokens between
+/// a set of source and destination accounts.
+///
+/// The policy is defined by a set of source and destination account indices
+/// and a set of allowed mints.
+///===============================================
+///
+#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, PartialEq, Eq)]
+pub struct InternalFundTransferPolicy {
+ // Using bitmasks here not only saves space, but it also prevents us from
+ // having to deduplicate or sort the account indices.
+ // Bitmask of allowed source account indices
+ pub source_account_mask: [u8; 32],
+ // Bitmask of allowed destination account indices
+ pub destination_account_mask: [u8; 32],
+ pub allowed_mints: Vec,
+}
+
+impl InternalFundTransferPolicy {
+ /// Convert a bitmask to a list of indices
+ pub fn mask_to_indices(mask: &[u8; 32]) -> Vec {
+ let mut indices = Vec::new();
+ for i in 0..32 {
+ for j in 0..8 {
+ if mask[i] & (1 << j) != 0 {
+ indices.push((i * 8 + j) as u8);
+ }
+ }
+ }
+ indices
+ }
+
+ /// Convert a list of indices to a bitmask
+ pub fn indices_to_mask(indices: &[u8]) -> [u8; 32] {
+ let mut mask = [0u8; 32];
+ for index in indices {
+ mask[*index as usize / 8] |= 1 << (*index as usize % 8);
+ }
+ mask
+ }
+ /// Checks if the given index is in the given mask
+ pub fn has_account_index(index: u8, mask: &[u8; 32]) -> bool {
+ let byte_idx = (index / 8) as usize;
+ let bit_idx = index % 8;
+ (mask[byte_idx] & (1 << bit_idx)) != 0
+ }
+
+ /// Checks if the given index is in the source account mask
+ pub fn has_source_account_index(&self, index: u8) -> bool {
+ Self::has_account_index(index, &self.source_account_mask)
+ }
+
+ /// Checks if the given index is in the destination account mask
+ pub fn has_destination_account_index(&self, index: u8) -> bool {
+ Self::has_account_index(index, &self.destination_account_mask)
+ }
+}
+
+#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, Eq)]
+pub struct InternalFundTransferPayload {
+ pub source_index: u8,
+ pub destination_index: u8,
+ pub mint: Pubkey,
+ pub decimals: u8,
+ pub amount: u64,
+}
+
+#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, Eq)]
+pub struct InternalFundTransferPolicyCreationPayload {
+ pub source_account_indices: Vec,
+ pub destination_account_indices: Vec,
+ pub allowed_mints: Vec,
+}
+
+impl PolicySizeTrait for InternalFundTransferPolicyCreationPayload {
+ fn creation_payload_size(&self) -> usize {
+ 4 + self.source_account_indices.len() + // source_account_indices vec
+ 4 + self.destination_account_indices.len() + // destination_account_indices vec
+ 4 + self.allowed_mints.len() * 32 // allowed_mints vec
+ }
+
+ fn policy_state_size(&self) -> usize {
+ 32 + // source_account_mask
+ 32 + // destination_account_mask
+ 4 + self.allowed_mints.len() * 32 // allowed_mints vec
+ }
+}
+
+impl PolicyPayloadConversionTrait for InternalFundTransferPolicyCreationPayload {
+ type PolicyState = InternalFundTransferPolicy;
+
+ fn to_policy_state(self) -> Result {
+ // Sort the allowed mints to ensure the invariant function can apply.
+ let mut sorted_allowed_mints = self.allowed_mints.clone();
+ sorted_allowed_mints.sort_by_key(|mint| mint.clone());
+
+ // Create the policy state
+ Ok(InternalFundTransferPolicy {
+ source_account_mask: InternalFundTransferPolicy::indices_to_mask(
+ &self.source_account_indices,
+ ),
+ destination_account_mask: InternalFundTransferPolicy::indices_to_mask(
+ &self.destination_account_indices,
+ ),
+ allowed_mints: sorted_allowed_mints,
+ })
+ }
+}
+
+pub struct InternalFundTransferExecutionArgs {
+ pub settings_key: Pubkey,
+}
+
+impl PolicyTrait for InternalFundTransferPolicy {
+ type PolicyState = Self;
+ type CreationPayload = InternalFundTransferPolicyCreationPayload;
+ type UsagePayload = InternalFundTransferPayload;
+ type ExecutionArgs = InternalFundTransferExecutionArgs;
+
+ fn invariant(&self) -> Result<()> {
+ // There can't be duplicate mints. Requires the mints are sorted.
+ let has_duplicates = self.allowed_mints.windows(2).any(|win| win[0] == win[1]);
+ require!(
+ !has_duplicates,
+ SmartAccountError::InternalFundTransferPolicyInvariantDuplicateMints
+ );
+ Ok(())
+ }
+
+ /// Validates a given usage payload.
+ fn validate_payload(
+ &self,
+ // No difference between synchronous and asynchronous execution
+ _context: PolicyExecutionContext,
+ payload: &Self::UsagePayload,
+ ) -> Result<()> {
+ // Validate source account index is allowed
+ require!(
+ self.has_source_account_index(payload.source_index),
+ SmartAccountError::InternalFundTransferPolicyInvariantSourceAccountIndexNotAllowed
+ );
+
+ // Validate destination account index is allowed
+ require!(
+ self.has_destination_account_index(payload.destination_index),
+ SmartAccountError::InternalFundTransferPolicyInvariantDestinationAccountIndexNotAllowed
+ );
+
+ // Validate mint is allowed (empty allowed_mints means all mints are allowed)
+ if !self.allowed_mints.is_empty() {
+ require!(
+ self.allowed_mints.contains(&payload.mint),
+ SmartAccountError::InternalFundTransferPolicyInvariantMintNotAllowed
+ );
+ }
+
+ // Validate amount is non-zero
+ require!(
+ payload.amount > 0,
+ SmartAccountError::InternalFundTransferPolicyInvariantAmountZero
+ );
+
+ // Validate source and destination are different
+ require!(
+ payload.source_index != payload.destination_index,
+ SmartAccountError::InternalFundTransferPolicyInvariantSourceAndDestinationCannotBeTheSame
+ );
+
+ Ok(())
+ }
+
+ /// Execute the internal fund transfer policy
+ /// Expects the following accounts:
+ /// - Source account
+ /// - Source account token account
+ /// - Destination account token account
+ fn execute_payload<'info>(
+ &mut self,
+ args: Self::ExecutionArgs,
+ payload: &Self::UsagePayload,
+ accounts: &'info [AccountInfo<'info>],
+ ) -> Result<()> {
+ let validated_accounts = Self::validate_accounts(&args.settings_key, &payload, accounts)?;
+
+ match validated_accounts {
+ ValidatedAccounts::NativeTransfer {
+ source_account_info,
+ source_account_bump,
+ destination_account_info,
+ system_program,
+ } => {
+ // Transfer SOL
+ anchor_lang::system_program::transfer(
+ CpiContext::new_with_signer(
+ system_program.to_account_info(),
+ anchor_lang::system_program::Transfer {
+ from: source_account_info.clone(),
+ to: destination_account_info.clone(),
+ },
+ &[&[
+ SEED_PREFIX,
+ args.settings_key.as_ref(),
+ SEED_SMART_ACCOUNT,
+ &payload.source_index.to_le_bytes(),
+ &[source_account_bump],
+ ]],
+ ),
+ payload.amount,
+ )?
+ }
+ ValidatedAccounts::TokenTransfer {
+ source_account_info,
+ source_account_bump,
+ source_token_account_info,
+ destination_token_account_info,
+ mint,
+ token_program,
+ } => {
+ // Transfer SPL token
+ token_interface::transfer_checked(
+ CpiContext::new_with_signer(
+ token_program.to_account_info(),
+ TransferChecked {
+ from: source_token_account_info.to_account_info(),
+ mint: mint.to_account_info(),
+ to: destination_token_account_info.to_account_info(),
+ authority: source_account_info.clone(),
+ },
+ &[&[
+ SEED_PREFIX,
+ args.settings_key.as_ref(),
+ SEED_SMART_ACCOUNT,
+ &payload.source_index.to_le_bytes(),
+ &[source_account_bump],
+ ]],
+ ),
+ payload.amount,
+ payload.decimals,
+ )?;
+ }
+ }
+
+ Ok(())
+ }
+}
+
+enum ValidatedAccounts<'info> {
+ NativeTransfer {
+ source_account_info: &'info AccountInfo<'info>,
+ source_account_bump: u8,
+ destination_account_info: &'info AccountInfo<'info>,
+ system_program: &'info AccountInfo<'info>,
+ },
+ TokenTransfer {
+ source_account_info: &'info AccountInfo<'info>,
+ source_account_bump: u8,
+ source_token_account_info: &'info AccountInfo<'info>,
+ destination_token_account_info: &'info AccountInfo<'info>,
+ mint: &'info AccountInfo<'info>,
+ token_program: &'info AccountInfo<'info>,
+ },
+}
+impl InternalFundTransferPolicy {
+ /// Validates the accounts passed in and returns a struct with the accounts
+ fn validate_accounts<'info>(
+ settings_key: &Pubkey,
+ args: &InternalFundTransferPayload,
+ accounts: &'info [AccountInfo<'info>],
+ ) -> Result> {
+ // Derive source and destination account keys
+ let source_account_index_bytes = args.source_index.to_le_bytes();
+ let destination_account_index_bytes = args.destination_index.to_le_bytes();
+ let source_account_seeds =
+ get_smart_account_seeds(settings_key, &source_account_index_bytes);
+ let destination_account_seeds =
+ get_smart_account_seeds(settings_key, &destination_account_index_bytes);
+
+ // Derive source and destination account keys
+ let (source_account_key, source_account_bump) =
+ Pubkey::find_program_address(source_account_seeds.as_slice(), &crate::ID);
+ // Derive the destination account from the destination index
+ let (destination_account_key, _) =
+ Pubkey::find_program_address(destination_account_seeds.as_slice(), &crate::ID);
+
+ // Mint specific logic
+ match args.mint {
+ // Native SOL transfer
+ mint if mint == Pubkey::default() => {
+ // Parse out the accounts
+ let (source_account_info, destination_account_info, system_program) = if let [source_account_info, destination_account_info, system_program, _remaining @ ..] =
+ accounts
+ {
+ (
+ source_account_info,
+ destination_account_info,
+ system_program,
+ )
+ } else {
+ return err!(SmartAccountError::InvalidNumberOfAccounts);
+ };
+ // Check that the source account is the same as the source account info
+ require!(
+ source_account_key == source_account_info.key(),
+ SmartAccountError::InvalidAccount
+ );
+ // Check that the destination account is the same as the destination account info
+ require!(
+ destination_account_key == destination_account_info.key(),
+ SmartAccountError::InvalidAccount
+ );
+ // Check the system program
+ require!(
+ system_program.key() == system_program::ID,
+ SmartAccountError::InvalidAccount
+ );
+
+ // Sanity check for the decimals. Similar to the one in token_interface::transfer_checked.
+ require!(args.decimals == 9, SmartAccountError::DecimalsMismatch);
+
+ Ok(ValidatedAccounts::NativeTransfer {
+ source_account_info,
+ source_account_bump,
+ destination_account_info,
+ system_program,
+ })
+ }
+ // Token transfer
+ _ => {
+ // Parse out the accounts
+ let (
+ source_account_info,
+ source_token_account_info,
+ destination_token_account_info,
+ mint,
+ token_program,
+ ) = if let [source_account_info, source_token_account_info, destination_token_account_info, mint, token_program, _remaining @ ..] =
+ accounts
+ {
+ (
+ source_account_info,
+ source_token_account_info,
+ destination_token_account_info,
+ mint,
+ token_program,
+ )
+ } else {
+ return err!(SmartAccountError::InvalidNumberOfAccounts);
+ };
+
+ // Check the source account key
+ require!(
+ source_account_key == source_account_info.key(),
+ SmartAccountError::InvalidAccount
+ );
+ // Deserialize the source and destination token accounts. Either
+ // T22 or TokenKeg accounts
+ let source_token_account =
+ InterfaceAccount::<'info, TokenAccount>::try_from(source_token_account_info)?;
+ let destination_token_account =
+ InterfaceAccount::::try_from(destination_token_account_info)?;
+ // Check the mint against the payload
+ require_eq!(args.mint, mint.key());
+
+ // Assert the ownership and mint of the token accounts
+ require!(
+ source_token_account.owner == source_account_key
+ && source_token_account.mint == args.mint,
+ SmartAccountError::InvalidAccount
+ );
+ require!(
+ destination_token_account.owner == destination_account_key
+ && destination_token_account.mint == args.mint,
+ SmartAccountError::InvalidAccount
+ );
+ // Check the token program
+ require_eq!(TokenInterface::ids().contains(&token_program.key()), true);
+
+ Ok(ValidatedAccounts::TokenTransfer {
+ source_account_info,
+ source_account_bump,
+ source_token_account_info,
+ destination_token_account_info,
+ mint,
+ token_program,
+ })
+ }
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_indices_to_mask_and_back() {
+ let indices = vec![0, 1, 8, 15, 31, 63, 127, 255];
+ let mask = InternalFundTransferPolicy::indices_to_mask(&indices);
+ let result_indices = InternalFundTransferPolicy::mask_to_indices(&mask);
+ assert_eq!(indices, result_indices);
+ }
+
+ #[test]
+ fn test_has_account_index() {
+ let indices = vec![2, 5, 10, 20];
+ let mask = InternalFundTransferPolicy::indices_to_mask(&indices);
+ let policy = InternalFundTransferPolicy {
+ source_account_mask: mask,
+ destination_account_mask: [0u8; 32],
+ allowed_mints: vec![],
+ };
+ for &idx in &indices {
+ assert!(InternalFundTransferPolicy::has_account_index(idx, &policy.source_account_mask));
+ }
+ assert!(!InternalFundTransferPolicy::has_account_index(3, &policy.source_account_mask));
+ assert!(!InternalFundTransferPolicy::has_account_index(0, &policy.destination_account_mask));
+ }
+
+ #[test]
+ fn test_has_source_and_destination_account_index() {
+ let source_indices = vec![1, 3, 5];
+ let dest_indices = vec![2, 4, 6];
+ let policy = InternalFundTransferPolicy {
+ source_account_mask: InternalFundTransferPolicy::indices_to_mask(&source_indices),
+ destination_account_mask: InternalFundTransferPolicy::indices_to_mask(&dest_indices),
+ allowed_mints: vec![],
+ };
+ for &idx in &source_indices {
+ assert!(policy.has_source_account_index(idx));
+ }
+ for &idx in &dest_indices {
+ assert!(policy.has_destination_account_index(idx));
+ }
+ assert!(!policy.has_source_account_index(2));
+ assert!(!policy.has_destination_account_index(1));
+ }
+}
diff --git a/programs/squads_smart_account_program/src/state/policies/implementations/mod.rs b/programs/squads_smart_account_program/src/state/policies/implementations/mod.rs
new file mode 100644
index 0000000..c10985a
--- /dev/null
+++ b/programs/squads_smart_account_program/src/state/policies/implementations/mod.rs
@@ -0,0 +1,14 @@
+//! Policy implementations
+//!
+//! This module contains specific policy implementations that use the core policy framework.
+//! Each policy type implements the PolicyExecutor trait for type-safe execution.
+
+pub mod internal_fund_transfer;
+pub mod spending_limit_policy;
+pub mod program_interaction;
+pub mod settings_change;
+
+pub use internal_fund_transfer::*;
+pub use spending_limit_policy::*;
+pub use program_interaction::*;
+pub use settings_change::*;
\ No newline at end of file
diff --git a/programs/squads_smart_account_program/src/state/policies/implementations/program_interaction.rs b/programs/squads_smart_account_program/src/state/policies/implementations/program_interaction.rs
new file mode 100644
index 0000000..e9a4ef0
--- /dev/null
+++ b/programs/squads_smart_account_program/src/state/policies/implementations/program_interaction.rs
@@ -0,0 +1,2166 @@
+use crate::{
+ errors::*,
+ state::policies::utils::{
+ check_pre_balances, PeriodV2, QuantityConstraints, SpendingLimitV2, TimeConstraints,
+ UsageState,
+ },
+ utils::{
+ derive_ephemeral_signers, ExecutableTransactionMessage, SynchronousTransactionMessage,
+ },
+ CompiledInstruction, PolicyExecutionContext, PolicyPayloadConversionTrait, PolicySizeTrait,
+ PolicyTrait, SmallVec, SmartAccountCompiledInstruction, SmartAccountSigner, TransactionMessage,
+ TransactionPayload, TransactionPayloadDetails, HOOK_AUTHORITY_PUBKEY, SEED_EPHEMERAL_SIGNER,
+ SEED_HOOK_AUTHORITY, SEED_PREFIX, SEED_SMART_ACCOUNT,
+};
+use anchor_lang::prelude::*;
+use solana_program::{instruction::Instruction, pubkey};
+
+// =============================================================================
+// BUILTIN PUBKEY CONSTANTS
+// =============================================================================
+
+/// Wrapped SOL mint address
+const WRAPPED_SOL: Pubkey = pubkey!("So11111111111111111111111111111111111111112");
+
+/// Starting index for builtin programs in the pubkey lookup table.
+/// Indices 0-239 are for custom pubkeys stored in pubkey_table.
+/// Indices 240-255 are reserved for commonly-used builtin programs.
+const BUILTIN_INDEX_START: u8 = 240;
+
+/// Resolve a builtin program pubkey by index (240-243).
+/// Returns a reference to the static builtin pubkey constant.
+///
+/// Index mapping:
+/// - 240: System Program
+/// - 241: Token Program (SPL Token)
+/// - 242: Associated Token Account Program
+/// - 243: Token-2022 Program
+/// - 244-255: Reserved for future use (currently invalid)
+#[inline(always)]
+fn resolve_builtin_pubkey(index: u8) -> Result<&'static Pubkey> {
+ match index {
+ 240 => Ok(&anchor_lang::system_program::ID),
+ 241 => Ok(&anchor_spl::token::ID),
+ 242 => Ok(&anchor_spl::associated_token::ID),
+ 243 => Ok(&anchor_spl::token_2022::ID),
+ 244 => Ok(&anchor_spl::mint::USDC),
+ 245 => Ok(&WRAPPED_SOL),
+ _ => Err(SmartAccountError::ProgramInteractionInvalidPubkeyTableIndex.into()),
+ }
+}
+
+// =============================================================================
+// CORE POLICY STRUCTURES
+// =============================================================================
+
+#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
+pub struct ProgramInteractionPolicy {
+ /// The account index of the account that will be used to execute the policy
+ pub account_index: u8,
+ /// Constraints evaluated as a logical OR
+ pub instructions_constraints: Vec,
+ /// Hook invoked before inner instruction execution
+ pub pre_hook: Option,
+ /// Hook invoked after inner instruction execution
+ pub post_hook: Option,
+ /// Spending limits applied during policy execution
+ pub spending_limits: Vec,
+}
+
+#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, Eq, Debug)]
+pub struct InstructionConstraint {
+ /// The program that this constraint applies to
+ pub program_id: Pubkey,
+ /// Account constraints (evaluated as logical AND)
+ pub account_constraints: Vec,
+ /// Data constraints (evaluated as logical AND)
+ pub data_constraints: Vec,
+}
+
+/// Compiled version of InstructionConstraint for use with pubkey_table
+#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, Eq, Debug)]
+pub struct CompiledInstructionConstraint {
+ /// Index into pubkey_table for the program_id
+ pub program_id_index: u8,
+ /// Account constraints (evaluated as logical AND)
+ pub account_constraints: SmallVec,
+ /// Data constraints (evaluated as logical AND)
+ pub data_constraints: SmallVec,
+}
+
+#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, PartialEq, Eq)]
+pub struct Hook {
+ // Dictates how many extra accounts are required for the hook, beyond the program ID
+ pub num_extra_accounts: u8,
+ // Dictates constraints for the hook accounts
+ pub account_constraints: Vec,
+ // Dictates which instruction data will be invoked
+ pub instruction_data: Vec,
+ // The program that will be invoked
+ pub program_id: Pubkey,
+ // Dictates if inner instruction data & account will be passed to the
+ // instruction on top of the instruction data
+ pub pass_inner_instructions: bool,
+}
+
+/// Compiled version of Hook for use with pubkey_table
+#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
+pub struct CompiledHook {
+ // Dictates how many extra accounts are required for the hook, beyond the program ID
+ pub num_extra_accounts: u8,
+ // Dictates constraints for the hook accounts
+ pub account_constraints: SmallVec,
+ // Dictates which instruction data will be invoked
+ pub instruction_data: SmallVec,
+ // Index into pubkey_table for the program_id
+ pub program_id_index: u8,
+ // Dictates if inner instruction data & account will be passed to the
+ // instruction on top of the instruction data
+ pub pass_inner_instructions: bool,
+}
+
+impl Hook {
+ pub fn size(&self) -> usize {
+ 1 + // num_accounts
+ 4 + self.account_constraints.iter().map(|c| c.size()).sum::() + // account_constraints vec
+ 4 + self.instruction_data.len() + // instruction_data
+ 32 + // program_id
+ 1 // pass_inner_instructions
+ }
+
+ // Get the total number of accounts required for the hook
+ pub fn num_accounts(&self) -> usize {
+ // Program ID also needs to get passed in, therefore add 1
+ self.num_extra_accounts
+ .checked_add(1)
+ .map(|v| v as usize)
+ .unwrap()
+ }
+}
+
+impl CompiledHook {
+ pub fn size(&self) -> usize {
+ 1 + // num_accounts
+ 1 + self.account_constraints.iter().map(|c| c.size()).sum::() + // account_constraints
+ 2 + self.instruction_data.len() + // instruction_data
+ 1 + // program_id_index
+ 1 // pass_inner_instructions
+ }
+
+ // Get the total number of accounts required for the hook
+ pub fn num_accounts(&self) -> usize {
+ // Program ID also needs to get passed in, therefore add 1
+ self.num_extra_accounts
+ .checked_add(1)
+ .map(|v| v as usize)
+ .unwrap()
+ }
+}
+
+// =============================================================================
+// CONSTRAINT TYPES AND OPERATORS
+// =============================================================================
+
+impl InstructionConstraint {
+ pub fn size(&self) -> usize {
+ 32 + // program_id
+ 4 + self.account_constraints.iter().map(|c| c.size()).sum::() + // account_constraints vec
+ 4 + self.data_constraints.iter().map(|c| c.size()).sum::() // data_constraints vec
+ }
+}
+
+impl CompiledInstructionConstraint {
+ pub fn size(&self) -> usize {
+ 1 + // program_id_index
+ 1 + self.account_constraints.iter().map(|c| c.size()).sum::() + // account_constraints small_vec
+ 1 + self.data_constraints.iter().map(|c| c.size()).sum::() // data_constraints small_vec
+ }
+}
+
+#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, Eq, Debug)]
+pub enum DataOperator {
+ Equals,
+ NotEquals,
+ GreaterThan,
+ GreaterThanOrEqualTo,
+ LessThan,
+ LessThanOrEqualTo,
+}
+
+#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, Eq, Debug)]
+pub enum DataValue {
+ U8(u8),
+ /// Little-endian u16
+ U16Le(u16),
+ /// Little-endian u32
+ U32Le(u32),
+ /// Little-endian u64
+ U64Le(u64),
+ /// Little-endian u128
+ U128Le(u128),
+ /// Byte slice for discriminators etc. Only supports Equals/NotEquals
+ U8Slice(Vec),
+}
+
+impl DataValue {
+ pub fn size(&self) -> usize {
+ 1 + // enum discriminator
+ match self {
+ DataValue::U8(_) => 1,
+ DataValue::U16Le(_) => 2,
+ DataValue::U32Le(_) => 4,
+ DataValue::U64Le(_) => 8,
+ DataValue::U128Le(_) => 16,
+ DataValue::U8Slice(bytes) => 4 + bytes.len(), // vec length + bytes
+ }
+ }
+}
+
+#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, Eq, Debug)]
+pub struct DataConstraint {
+ pub data_offset: u64,
+ pub data_value: DataValue,
+ pub operator: DataOperator,
+}
+
+#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, Eq, Debug)]
+pub enum AccountConstraintType {
+ Pubkey(Vec),
+ AccountData(Vec),
+}
+
+impl AccountConstraintType {
+ pub fn size(&self) -> usize {
+ match self {
+ AccountConstraintType::Pubkey(keys) => 1 + 4 + keys.len() * 32,
+ AccountConstraintType::AccountData(constraints) => {
+ 1 + 4 + constraints.iter().map(|c| c.size()).sum::()
+ }
+ }
+ }
+}
+
+/// Compiled version of AccountConstraintType for use with pubkey_table
+#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, Eq, Debug)]
+pub enum CompiledAccountConstraintType {
+ Pubkey(SmallVec), // Indices into pubkey_table
+ AccountData(SmallVec),
+}
+
+impl CompiledAccountConstraintType {
+ pub fn size(&self) -> usize {
+ match self {
+ CompiledAccountConstraintType::Pubkey(indices) => 1 + 1 + indices.len(),
+ CompiledAccountConstraintType::AccountData(constraints) => {
+ 1 + 1 + constraints.iter().map(|c| c.size()).sum::()
+ }
+ }
+ }
+}
+
+#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, Eq, Debug)]
+pub struct AccountConstraint {
+ pub account_index: u8,
+ pub account_constraint: AccountConstraintType,
+ pub owner: Option,
+}
+
+/// Compiled version of AccountConstraint for use with pubkey_table
+#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, Eq, Debug)]
+pub struct CompiledAccountConstraint {
+ pub account_index: u8,
+ pub account_constraint: CompiledAccountConstraintType,
+ pub owner_index: Option, // index into pubkey_table for owner
+}
+
+// =============================================================================
+// SIZE CALCULATIONS
+// =============================================================================
+
+impl DataConstraint {
+ pub fn size(&self) -> usize {
+ 8 + // data_offset
+ self.data_value.size() + // data_value
+ 1 // operator
+ }
+}
+
+impl AccountConstraint {
+ pub fn size(&self) -> usize {
+ 1 + // account_index
+ self.account_constraint.size() + // account_constraint (enum, no vec prefix needed)
+ 1 + // option discriminator
+ if self.owner.is_some() { 32 } else { 0 } // owner value (conditional)
+ }
+}
+
+impl CompiledAccountConstraint {
+ pub fn size(&self) -> usize {
+ 1 + // account_index
+ self.account_constraint.size() + // account_constraint (includes enum discriminator + small_vec length + data)
+ 1 + // option discriminator
+ if self.owner_index.is_some() { 1 } else { 0 } // owner_index value
+ }
+}
+
+// =============================================================================
+// CONSTRAINT EVALUATION LOGIC
+// =============================================================================
+
+impl DataConstraint {
+ /// Evaluate constraint against instruction data
+ pub fn evaluate(&self, data: &[u8]) -> Result<()> {
+ let offset = self.data_offset as usize;
+
+ let constraint_passed = match &self.data_value {
+ DataValue::U8(expected) => {
+ // Check bounds
+ if offset >= data.len() {
+ return Err(SmartAccountError::ProgramInteractionDataTooShort.into());
+ }
+ let actual = data[offset];
+ self.compare(actual, *expected)?
+ }
+ DataValue::U16Le(expected) => {
+ // Check bounds for 2 bytes
+ if offset + 2 > data.len() {
+ return Err(SmartAccountError::ProgramInteractionDataTooShort.into());
+ }
+ let bytes = &data[offset..offset + 2];
+ let actual = u16::from_le_bytes([bytes[0], bytes[1]]);
+ self.compare(actual, *expected)?
+ }
+ DataValue::U32Le(expected) => {
+ // Check bounds for 4 bytes
+ if offset + 4 > data.len() {
+ return Err(SmartAccountError::ProgramInteractionDataTooShort.into());
+ }
+ let bytes = &data[offset..offset + 4];
+ let actual = u32::from_le_bytes([bytes[0], bytes[1], bytes[2], bytes[3]]);
+ self.compare(actual, *expected)?
+ }
+ DataValue::U64Le(expected) => {
+ // Check bounds for 8 bytes
+ if offset + 8 > data.len() {
+ return Err(SmartAccountError::ProgramInteractionDataTooShort.into());
+ }
+ let actual = u64::from_le_bytes(
+ data[offset..offset + 8]
+ .try_into()
+ .map_err(|_| SmartAccountError::ProgramInteractionDataParsingError)?,
+ );
+ self.compare(actual, *expected)?
+ }
+ DataValue::U128Le(expected) => {
+ // Check bounds for 16 bytes
+ if offset + 16 > data.len() {
+ return Err(SmartAccountError::ProgramInteractionDataTooShort.into());
+ }
+ let actual = u128::from_le_bytes(
+ data[offset..offset + 16]
+ .try_into()
+ .map_err(|_| SmartAccountError::ProgramInteractionDataParsingError)?,
+ );
+ self.compare(actual, *expected)?
+ }
+ DataValue::U8Slice(expected) => {
+ // Check bounds for slice length
+ if offset + expected.len() > data.len() {
+ return Err(SmartAccountError::ProgramInteractionDataTooShort.into());
+ }
+ let actual = &data[offset..offset + expected.len()];
+ match self.operator {
+ DataOperator::Equals => actual == expected.as_slice(),
+ DataOperator::NotEquals => actual != expected.as_slice(),
+ _ => {
+ return Err(
+ SmartAccountError::ProgramInteractionUnsupportedSliceOperator.into(),
+ );
+ }
+ }
+ }
+ };
+
+ if constraint_passed {
+ Ok(())
+ } else {
+ Err(SmartAccountError::ProgramInteractionInvalidNumericValue.into())
+ }
+ }
+
+ /// Compare two values using the specified operator
+ fn compare(&self, actual: T, expected: T) -> Result {
+ Ok(match self.operator {
+ DataOperator::Equals => actual == expected,
+ DataOperator::NotEquals => actual != expected,
+ DataOperator::GreaterThan => actual > expected,
+ DataOperator::GreaterThanOrEqualTo => actual >= expected,
+ DataOperator::LessThan => actual < expected,
+ DataOperator::LessThanOrEqualTo => actual <= expected,
+ })
+ }
+}
+
+impl AccountConstraint {
+ /// Evaluate the account constraint for a given set of instruction_account_indices and accounts
+ pub fn evaluate_against_instruction_indices_and_accounts(
+ &self,
+ instruction_account_indices: &[u8],
+ accounts: &[AccountInfo],
+ ) -> Result<()> {
+ // Get the account at the given constraint index
+ let mapped_account_index = instruction_account_indices[self.account_index as usize];
+ let account = &accounts[mapped_account_index as usize];
+
+ self.evaluate_against_account_info(account)?;
+
+ Ok(())
+ }
+
+ /// Simply evaluate the account constraint against a single AccountInfo
+ pub fn evaluate_against_account_info(&self, account: &AccountInfo) -> Result<()> {
+ // Evaluate the owner constraint
+ if let Some(owner) = self.owner {
+ require_eq!(
+ account.owner,
+ &owner,
+ SmartAccountError::IllegalAccountOwner
+ );
+ };
+ // Evaluate the account constraint
+ match &self.account_constraint {
+ AccountConstraintType::Pubkey(keys) => {
+ if !keys.contains(&account.key) {
+ return Err(
+ SmartAccountError::ProgramInteractionAccountConstraintViolated.into(),
+ );
+ }
+ }
+ AccountConstraintType::AccountData(constraints) => {
+ let data = account.try_borrow_data()?;
+ for constraint in constraints {
+ constraint.evaluate(&data)?;
+ }
+ }
+ }
+ Ok(())
+ }
+
+ /// Evaluate the account constraint against a set of AccountInfos
+ pub fn evaluate_against_account_infos<'info>(
+ &self,
+ account_infos: &'info [AccountInfo<'info>],
+ ) -> Result<()> {
+ let account_info_to_evalute = account_infos
+ .get(self.account_index as usize)
+ .ok_or(SmartAccountError::ProgramInteractionAccountConstraintViolated)
+ .unwrap();
+
+ self.evaluate_against_account_info(account_info_to_evalute)?;
+
+ Ok(())
+ }
+}
+
+// =============================================================================
+// CREATION PAYLOAD TYPES
+// =============================================================================
+
+/// Limited subset of TimeConstraints
+#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, Eq, Debug)]
+pub struct LimitedTimeConstraints {
+ pub start: i64,
+ pub expiration: Option,
+ pub period: PeriodV2,
+}
+
+/// Limited subset of QuantityConstraints
+#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, Eq, Debug)]
+pub struct LimitedQuantityConstraints {
+ pub max_per_period: u64,
+}
+
+/// Limited subset of BalanceConstraint used to create a policy
+#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, Eq, Debug)]
+pub struct LimitedSpendingLimit {
+ pub mint: Pubkey,
+ pub time_constraints: LimitedTimeConstraints,
+ pub quantity_constraints: LimitedQuantityConstraints,
+}
+
+/// Compiled version of LimitedSpendingLimit for use with pubkey_table
+#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, Eq)]
+pub struct CompiledLimitedSpendingLimit {
+ pub mint_index: u8,
+ pub time_constraints: LimitedTimeConstraints,
+ pub quantity_constraints: LimitedQuantityConstraints,
+}
+
+/// Legacy payload used to create a program interaction policy (V1 format with embedded Pubkeys)
+#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
+pub struct ProgramInteractionPolicyCreationPayloadLegacy {
+ pub account_index: u8,
+ pub instructions_constraints: Vec,
+ pub pre_hook: Option,
+ pub post_hook: Option,
+ pub spending_limits: Vec,
+}
+
+/// Payload used to create a program interaction policy (V2 format with pubkey table and indices)
+#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
+pub struct ProgramInteractionPolicyCreationPayload {
+ pub account_index: u8,
+ pub pubkey_table: SmallVec,
+ pub instructions_constraints: SmallVec,
+ pub pre_hook: Option,
+ pub post_hook: Option,
+ pub spending_limits: SmallVec,
+}
+
+// =============================================================================
+// TRANSACTION PAYLOAD TYPES
+// =============================================================================
+#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
+pub struct ProgramInteractionPayload {
+ pub instruction_constraint_indices: Option>,
+ pub transaction_payload: ProgramInteractionTransactionPayload,
+}
+
+#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
+pub enum ProgramInteractionTransactionPayload {
+ AsyncTransaction(TransactionPayload),
+ SyncTransaction(SyncTransactionPayloadDetails),
+}
+
+#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
+pub struct SyncTransactionPayloadDetails {
+ pub account_index: u8,
+ pub instructions: Vec,
+}
+
+pub struct ProgramInteractionExecutionArgs {
+ pub settings_key: Pubkey,
+ pub transaction_key: Pubkey,
+ pub proposal_key: Pubkey,
+ pub policy_signers: Vec,
+}
+
+// =============================================================================
+// CORE POLICY IMPLEMENTATION
+// =============================================================================
+
+impl Hook {
+ pub fn execute<'info>(
+ &self,
+ hook_accounts: &'info [AccountInfo<'info>],
+ instructions: &[SmartAccountCompiledInstruction],
+ instruction_accounts: &[AccountInfo<'info>],
+ ) -> Result<()> {
+ use borsh::BorshSerialize;
+
+ // Evaluate the hook accounts
+ for account_constraint in self.account_constraints.iter() {
+ account_constraint.evaluate_against_account_infos(hook_accounts)?;
+ }
+
+ // Build the necessary account metas
+ let mut account_metas =
+ Vec::with_capacity(1 + hook_accounts.len() + instruction_accounts.len());
+
+ // Add the hook accounts to the account metas
+ for account in hook_accounts.iter() {
+ let meta = if account.key == &HOOK_AUTHORITY_PUBKEY {
+ AccountMeta::new_readonly(*account.key, true)
+ } else if account.is_writable {
+ AccountMeta::new(*account.key, account.is_signer)
+ } else {
+ AccountMeta::new_readonly(*account.key, account.is_signer)
+ };
+ account_metas.push(meta);
+ }
+
+ // Build the instruction data
+ let mut instruction_data = self.instruction_data.clone();
+
+ if self.pass_inner_instructions {
+ // Serialized Vec represenation of the instructions length
+ instruction_data.extend_from_slice(&(instructions.len() as u32).to_le_bytes());
+
+ // Serialize the instructions
+ for ix in instructions {
+ ix.serialize(&mut instruction_data)
+ .map_err(|_| SmartAccountError::ProgramInteractionTemplateHookError)?;
+ }
+
+ // Add the instruction accounts
+ for account in instruction_accounts.iter() {
+ // Allow the hook authority, as long as it is readonly
+ let meta = if account.key == &HOOK_AUTHORITY_PUBKEY {
+ AccountMeta::new_readonly(*account.key, true)
+ } else if account.is_writable {
+ AccountMeta::new(*account.key, account.is_signer)
+ } else {
+ AccountMeta::new_readonly(*account.key, account.is_signer)
+ };
+ account_metas.push(meta);
+ }
+ }
+
+ // Build the instruction
+ let instruction = Instruction {
+ program_id: self.program_id,
+ accounts: account_metas,
+ data: instruction_data,
+ };
+
+ // Invoke the instruction
+ anchor_lang::solana_program::program::invoke_signed(
+ &instruction,
+ // Concatenate the hook accounts and the instruction accounts
+ &[hook_accounts, instruction_accounts].concat(),
+ &[&[SEED_HOOK_AUTHORITY]],
+ )?;
+ Ok(())
+ }
+}
+impl ProgramInteractionPolicy {
+ /// Evaluate the instruction constraints for a given instruction
+ pub fn evaluate_instruction_constraints<'info>(
+ &self,
+ instruction_constraint_indices: &[u8],
+ instructions: &[SmartAccountCompiledInstruction],
+ accounts: &[AccountInfo<'info>],
+ ) -> Result<()> {
+ // Iterate over instruction and their corresponding instruction constraint
+ for (instruction, instruction_constraint_index) in
+ instructions.iter().zip(instruction_constraint_indices)
+ {
+ let instruction_constraint =
+ &self.instructions_constraints[*instruction_constraint_index as usize];
+ // Evaluate the program id constraint
+ require!(
+ accounts[instruction.program_id_index as usize].key
+ == &instruction_constraint.program_id,
+ SmartAccountError::ProgramInteractionProgramIdMismatch
+ );
+
+ // Evaluate the account constraints
+ for account_constraint in &instruction_constraint.account_constraints {
+ account_constraint.evaluate_against_instruction_indices_and_accounts(
+ &instruction.account_indexes,
+ accounts,
+ )?;
+ }
+ // Evaluate the data constraints
+ for data_constraint in &instruction_constraint.data_constraints {
+ data_constraint.evaluate(instruction.data.as_slice())?;
+ }
+ }
+ Ok(())
+ }
+
+ // Parses hook accounts from the accounts slice and returns them
+ pub fn parse_hook_accounts<'info, 'a>(
+ &self,
+ accounts: &mut &'a [AccountInfo<'info>],
+ ) -> (&'a [AccountInfo<'info>], &'a [AccountInfo<'info>]) {
+ // Split all accounts into pre hook accounts, post_hook accounts and
+ // transaction related accounts including lookups
+ let mut pre_hook_accounts_intermediate: &[AccountInfo<'info>] = &[];
+ let mut post_hook_accounts_intermediate: &[AccountInfo<'info>] = &[];
+ let mut transaction_accounts = *accounts;
+
+ if self.pre_hook.is_some() {
+ let (pre_hook_accounts, remaining_accounts) = transaction_accounts
+ .split_at(self.pre_hook.as_ref().unwrap().num_accounts() as usize);
+ pre_hook_accounts_intermediate = pre_hook_accounts;
+ transaction_accounts = remaining_accounts;
+ };
+ if self.post_hook.is_some() {
+ let (post_hook_accounts, remaining_accounts) = transaction_accounts
+ .split_at(self.post_hook.as_ref().unwrap().num_accounts() as usize);
+ post_hook_accounts_intermediate = post_hook_accounts;
+ transaction_accounts = remaining_accounts;
+ }
+
+ // Re-set transaction accounts
+ *accounts = transaction_accounts;
+
+ (
+ pre_hook_accounts_intermediate,
+ post_hook_accounts_intermediate,
+ )
+ }
+}
+
+// =============================================================================
+// SIZE IMPLEMENTATIONS FOR CREATION PAYLOAD TYPES
+// =============================================================================
+
+impl LimitedTimeConstraints {
+ pub fn size(&self) -> usize {
+ 8 + // start
+ 1 + // option discriminator for expiration
+ match self.expiration {
+ Some(_) => 8, // expiration value
+ None => 0,
+ } +
+ 1 // period enum discriminator (PeriodV2 is small enum)
+ }
+}
+
+impl LimitedQuantityConstraints {
+ pub fn size(&self) -> usize {
+ 8 // max_per_period
+ }
+}
+
+impl LimitedSpendingLimit {
+ pub fn size(&self) -> usize {
+ 32 + // mint
+ self.time_constraints.size() + // time_constraints
+ self.quantity_constraints.size() // quantity_constraints
+ }
+}
+
+impl CompiledLimitedSpendingLimit {
+ pub fn size(&self) -> usize {
+ 1 + // mint_index
+ self.time_constraints.size() + // time_constraints
+ self.quantity_constraints.size() // quantity_constraints
+ }
+}
+
+// =============================================================================
+// PAYLOAD CONVERSION IMPLEMENTATIONS
+// =============================================================================
+
+impl PolicyPayloadConversionTrait for ProgramInteractionPolicyCreationPayloadLegacy {
+ type PolicyState = ProgramInteractionPolicy;
+
+ fn to_policy_state(self) -> Result {
+ // For sanity sake, we limit the number of instruction constraints and
+ // spending limits
+ require!(
+ self.instructions_constraints.len() <= 20,
+ SmartAccountError::ProgramInteractionTooManyInstructionConstraints
+ );
+ require!(
+ self.spending_limits.len() <= 10,
+ SmartAccountError::ProgramInteractionTooManySpendingLimits
+ );
+
+ let mut spending_limits = self.spending_limits.clone();
+ spending_limits.sort_by_key(|c| c.mint);
+
+ let current_timestamp = Clock::get()?.unix_timestamp;
+
+ Ok(ProgramInteractionPolicy {
+ account_index: self.account_index,
+ instructions_constraints: self.instructions_constraints,
+ pre_hook: self.pre_hook,
+ post_hook: self.post_hook,
+ spending_limits: spending_limits
+ .iter()
+ .map(|spending_limit| {
+ // Determine the start timestamp
+ let start = if spending_limit.time_constraints.start == 0 {
+ current_timestamp
+ } else {
+ spending_limit.time_constraints.start
+ };
+ SpendingLimitV2 {
+ mint: spending_limit.mint,
+ time_constraints: TimeConstraints {
+ start,
+ period: spending_limit.time_constraints.period,
+ expiration: spending_limit.time_constraints.expiration,
+ accumulate_unused: false,
+ },
+ quantity_constraints: QuantityConstraints {
+ max_per_period: spending_limit.quantity_constraints.max_per_period,
+ max_per_use: 0,
+ enforce_exact_quantity: false,
+ },
+ usage: UsageState {
+ remaining_in_period: spending_limit.quantity_constraints.max_per_period,
+ last_reset: start,
+ },
+ }
+ })
+ .collect(),
+ })
+ }
+}
+
+impl ProgramInteractionPolicyCreationPayload {
+ /// Resolve a pubkey from the table or builtin constants, checking bounds
+ #[inline]
+ fn resolve_pubkey(&self, index: u8) -> Result {
+ if index >= BUILTIN_INDEX_START {
+ // Builtin program (indices 240-255)
+ // resolve_builtin_pubkey handles validation internally
+ Ok(*resolve_builtin_pubkey(index)?)
+ } else {
+ // Custom pubkey from table (indices 0-239)
+ self.pubkey_table
+ .get(index as usize)
+ .copied()
+ .ok_or_else(|| SmartAccountError::ProgramInteractionInvalidPubkeyTableIndex.into())
+ }
+ }
+
+ /// Convert compiled constraint to full constraint
+ fn expand_instruction_constraint(
+ &self,
+ compiled: &CompiledInstructionConstraint,
+ ) -> Result {
+ Ok(InstructionConstraint {
+ program_id: self.resolve_pubkey(compiled.program_id_index)?,
+ account_constraints: compiled.account_constraints
+ .iter()
+ .map(|ac| self.expand_account_constraint(ac))
+ .collect::>>()?,
+ data_constraints: compiled.data_constraints.to_vec(),
+ })
+ }
+
+ fn expand_account_constraint(
+ &self,
+ compiled: &CompiledAccountConstraint,
+ ) -> Result {
+ Ok(AccountConstraint {
+ account_index: compiled.account_index,
+ account_constraint: match &compiled.account_constraint {
+ CompiledAccountConstraintType::Pubkey(indices) => {
+ // Pre-allocate with known capacity
+ let mut pubkeys = Vec::with_capacity(indices.len());
+ for &idx in indices.iter() {
+ pubkeys.push(self.resolve_pubkey(idx)?);
+ }
+ AccountConstraintType::Pubkey(pubkeys)
+ }
+ CompiledAccountConstraintType::AccountData(constraints) => {
+ AccountConstraintType::AccountData(constraints.to_vec())
+ }
+ },
+ owner: compiled.owner_index
+ .map(|idx| self.resolve_pubkey(idx))
+ .transpose()?,
+ })
+ }
+
+ fn expand_hook(&self, compiled: &CompiledHook) -> Result {
+ Ok(Hook {
+ num_extra_accounts: compiled.num_extra_accounts,
+ account_constraints: compiled.account_constraints
+ .iter()
+ .map(|ac| self.expand_account_constraint(ac))
+ .collect::>>()?,
+ instruction_data: compiled.instruction_data.to_vec(),
+ program_id: self.resolve_pubkey(compiled.program_id_index)?,
+ pass_inner_instructions: compiled.pass_inner_instructions,
+ })
+ }
+
+ fn expand_spending_limit(
+ &self,
+ compiled: &CompiledLimitedSpendingLimit,
+ ) -> Result {
+ Ok(LimitedSpendingLimit {
+ mint: self.resolve_pubkey(compiled.mint_index)?,
+ time_constraints: compiled.time_constraints.clone(),
+ quantity_constraints: compiled.quantity_constraints.clone(),
+ })
+ }
+}
+
+impl PolicyPayloadConversionTrait for ProgramInteractionPolicyCreationPayload {
+ type PolicyState = ProgramInteractionPolicy;
+
+ fn to_policy_state(self) -> Result {
+ // Validate limits
+ require!(
+ self.instructions_constraints.len() <= 20,
+ SmartAccountError::ProgramInteractionTooManyInstructionConstraints
+ );
+ require!(
+ self.spending_limits.len() <= 10,
+ SmartAccountError::ProgramInteractionTooManySpendingLimits
+ );
+ require!(
+ self.pubkey_table.len() <= 240,
+ SmartAccountError::ProgramInteractionTooManyUniquePubkeys
+ );
+
+ // Expand indexed constraints to full Pubkey constraints
+ let instructions_constraints = self.instructions_constraints
+ .iter()
+ .map(|ic| self.expand_instruction_constraint(ic))
+ .collect::>>()?;
+
+ let pre_hook = self.pre_hook
+ .as_ref()
+ .map(|h| self.expand_hook(h))
+ .transpose()?;
+
+ let post_hook = self.post_hook
+ .as_ref()
+ .map(|h| self.expand_hook(h))
+ .transpose()?;
+
+ // Expand indexed spending limits to full spending limits
+ let mut spending_limits = self.spending_limits
+ .iter()
+ .map(|sl| self.expand_spending_limit(sl))
+ .collect::>>()?;
+ spending_limits.sort_by_key(|c| c.mint);
+
+ let current_timestamp = Clock::get()?.unix_timestamp;
+
+ Ok(ProgramInteractionPolicy {
+ account_index: self.account_index,
+ instructions_constraints,
+ pre_hook,
+ post_hook,
+ spending_limits: spending_limits
+ .iter()
+ .map(|spending_limit| {
+ let start = if spending_limit.time_constraints.start == 0 {
+ current_timestamp
+ } else {
+ spending_limit.time_constraints.start
+ };
+ SpendingLimitV2 {
+ mint: spending_limit.mint,
+ time_constraints: TimeConstraints {
+ start,
+ period: spending_limit.time_constraints.period,
+ expiration: spending_limit.time_constraints.expiration,
+ accumulate_unused: false,
+ },
+ quantity_constraints: QuantityConstraints {
+ max_per_period: spending_limit.quantity_constraints.max_per_period,
+ max_per_use: 0,
+ enforce_exact_quantity: false,
+ },
+ usage: UsageState {
+ remaining_in_period: spending_limit.quantity_constraints.max_per_period,
+ last_reset: start,
+ },
+ }
+ })
+ .collect(),
+ })
+ }
+}
+
+impl PolicySizeTrait for ProgramInteractionPolicyCreationPayloadLegacy {
+ fn creation_payload_size(&self) -> usize {
+ 1 + // account_scope
+ 4 + self.instructions_constraints.iter().map(|c| c.size()).sum::() + // instructions_constraints vec
+ 1 + self.pre_hook.as_ref().map(|h| h.size()).unwrap_or(0) + // pre_hook
+ 1 + self.post_hook.as_ref().map(|h| h.size()).unwrap_or(0) + // post_hook
+ 4 + self.spending_limits.iter().map(|constraint| constraint.size()).sum::()
+ // spending_limits vec
+ }
+
+ fn policy_state_size(&self) -> usize {
+ 1 + // account_index (account_scope becomes account_index in policy state)
+ 4 + self.instructions_constraints.iter().map(|c| c.size()).sum::() + // instructions_constraints vec
+ 1 + self.pre_hook.as_ref().map(|h| h.size()).unwrap_or(0) + // pre_hook
+ 1 + self.post_hook.as_ref().map(|h| h.size()).unwrap_or(0) + // post_hook
+ 4 + self.spending_limits.iter().map(|_| SpendingLimitV2::INIT_SPACE).sum::()
+ // spending_limits vec
+ }
+}
+
+impl PolicySizeTrait for ProgramInteractionPolicyCreationPayload {
+ fn creation_payload_size(&self) -> usize {
+ 1 + // account_index
+ 1 + (self.pubkey_table.len() * 32) + // SmallVec + pubkey_table
+ 1 + self.instructions_constraints.iter().map(|c| c.size()).sum::() + // SmallVec + instructions_constraints
+ 1 + self.pre_hook.as_ref().map(|h| h.size()).unwrap_or(0) + // option + pre_hook
+ 1 + self.post_hook.as_ref().map(|h| h.size()).unwrap_or(0) + // option + post_hook
+ 1 + self.spending_limits.iter().map(|constraint| constraint.size()).sum::() // SmallVec + spending_limits
+ }
+
+ fn policy_state_size(&self) -> usize {
+ // After expansion, state size is based on expanded Pubkeys, not indices
+ // This is the same calculation as Legacy but we need to calculate the expanded size
+ 1 + // account_index
+ 4 + self.instructions_constraints.iter().map(|ic| {
+ 32 + // program_id (expanded from index)
+ 4 + ic.account_constraints.iter().map(|ac| {
+ 1 + // account_index
+ match &ac.account_constraint {
+ CompiledAccountConstraintType::Pubkey(indices) => 1 + 4 + indices.len() * 32, // expanded
+ CompiledAccountConstraintType::AccountData(constraints) => {
+ 1 + 4 + constraints.iter().map(|c| c.size()).sum::()
+ }
+ } +
+ 1 + // owner Option discriminator
+ if ac.owner_index.is_some() { 32 } else { 0 } // owner value (conditional)
+ }).sum::() + // account_constraints vec
+ 4 + ic.data_constraints.iter().map(|c| c.size()).sum::() // data_constraints vec
+ }).sum::() + // instructions_constraints vec
+ 1 + self.pre_hook.as_ref().map(|h| {
+ 1 + // num_accounts
+ 4 + h.account_constraints.iter().map(|ac| {
+ 1 + // account_index
+ match &ac.account_constraint {
+ CompiledAccountConstraintType::Pubkey(indices) => 1 + 4 + indices.len() * 32,
+ CompiledAccountConstraintType::AccountData(constraints) => {
+ 1 + 4 + constraints.iter().map(|c| c.size()).sum::()
+ }
+ } +
+ 1 + // owner Option discriminator
+ if ac.owner_index.is_some() { 32 } else { 0 } // owner value (conditional)
+ }).sum::() +
+ 4 + h.instruction_data.len() +
+ 32 + // program_id (expanded from index)
+ 1 // pass_inner_instructions
+ }).unwrap_or(0) + // pre_hook
+ 1 + self.post_hook.as_ref().map(|h| {
+ 1 + // num_accounts
+ 4 + h.account_constraints.iter().map(|ac| {
+ 1 + // account_index
+ match &ac.account_constraint {
+ CompiledAccountConstraintType::Pubkey(indices) => 1 + 4 + indices.len() * 32,
+ CompiledAccountConstraintType::AccountData(constraints) => {
+ 1 + 4 + constraints.iter().map(|c| c.size()).sum::()
+ }
+ } +
+ 1 + // owner Option discriminator
+ if ac.owner_index.is_some() { 32 } else { 0 } // owner value (conditional)
+ }).sum::() +
+ 4 + h.instruction_data.len() +
+ 32 + // program_id (expanded from index)
+ 1 // pass_inner_instructions
+ }).unwrap_or(0) + // post_hook
+ 4 + self.spending_limits.iter().map(|_| SpendingLimitV2::INIT_SPACE).sum::() // vec + spending_limits
+ }
+}
+
+// =============================================================================
+// TRANSACTION PAYLOAD IMPLEMENTATIONS
+// =============================================================================
+impl ProgramInteractionPayload {
+ /// Get the async transaction payload details for the policy
+ pub fn get_transaction_payload(
+ &self,
+ transaction_key: Pubkey,
+ ) -> Result {
+ match &self.transaction_payload {
+ ProgramInteractionTransactionPayload::AsyncTransaction(transaction_payload) => {
+ // Deserialize the transaction message
+ let transaction_message = TransactionMessage::deserialize(
+ &mut transaction_payload.transaction_message.as_slice(),
+ )?;
+ // Derive the ephemeral signer bumps
+ let ephemeral_signer_bumps: Vec = (0..transaction_payload.ephemeral_signers)
+ .map(|ephemeral_signer_index| {
+ let ephemeral_signer_seeds = &[
+ SEED_PREFIX,
+ transaction_key.as_ref(),
+ SEED_EPHEMERAL_SIGNER,
+ &ephemeral_signer_index.to_le_bytes(),
+ ];
+
+ let (_, bump) =
+ Pubkey::find_program_address(ephemeral_signer_seeds, &crate::ID);
+ bump
+ })
+ .collect();
+
+ // Create the transaction payload details
+ Ok(TransactionPayloadDetails {
+ account_index: transaction_payload.account_index,
+ ephemeral_signer_bumps,
+ message: transaction_message.try_into()?,
+ })
+ }
+ _ => Err(SmartAccountError::InvalidPayload.into()),
+ }
+ }
+
+ /// Get the sync transaction payload details for the policy
+ pub fn get_sync_transaction_payload(&self) -> Result<&SyncTransactionPayloadDetails> {
+ match &self.transaction_payload {
+ ProgramInteractionTransactionPayload::SyncTransaction(sync_transaction_payload) => {
+ Ok(sync_transaction_payload)
+ }
+ _ => Err(SmartAccountError::InvalidPayload.into()),
+ }
+ }
+}
+
+impl ProgramInteractionTransactionPayload {
+ /// Get the account index for the transaction payload
+ pub fn get_account_index(&self) -> u8 {
+ match self {
+ ProgramInteractionTransactionPayload::AsyncTransaction(transaction_payload) => {
+ transaction_payload.account_index
+ }
+ ProgramInteractionTransactionPayload::SyncTransaction(sync_transaction_payload) => {
+ sync_transaction_payload.account_index
+ }
+ }
+ }
+
+ /// Get the number of instructions for the transaction payload
+ pub fn instructions_len(&self) -> Result {
+ match self {
+ ProgramInteractionTransactionPayload::AsyncTransaction(transaction_payload) => {
+ // TODO: Inefficient to deserialize the transaction message and
+ // not do anything with it.
+ Ok(TransactionMessage::deserialize(
+ &mut transaction_payload.transaction_message.as_slice(),
+ )
+ .map_err(|_| SmartAccountError::InvalidInstructionArgs)?
+ .instructions
+ .len())
+ }
+ ProgramInteractionTransactionPayload::SyncTransaction(sync_transaction_payload) => {
+ // Since its a small vec, we can get the length directly from
+ // the first byte
+ Ok(sync_transaction_payload.instructions[0] as usize)
+ }
+ }
+ }
+}
+
+// =============================================================================
+// POLICY TRAIT IMPLEMENTATION
+// =============================================================================
+
+impl PolicyTrait for ProgramInteractionPolicy {
+ type PolicyState = Self;
+ type CreationPayload = ProgramInteractionPolicyCreationPayload;
+ type UsagePayload = ProgramInteractionPayload;
+ type ExecutionArgs = ProgramInteractionExecutionArgs;
+
+ /// Validate the policy invariant
+ fn invariant(&self) -> Result<()> {
+ // There can't be duplicate balance constraint for the same mint
+ // Assumes that the balance constraints are sorted by mint
+ let has_duplicate = self
+ .spending_limits
+ .windows(2)
+ .any(|window| window[0].mint == window[1].mint);
+ require!(
+ !has_duplicate,
+ SmartAccountError::ProgramInteractionDuplicateSpendingLimit
+ );
+
+ // Each spending limits invariant must be valid
+ for spending_limit in &self.spending_limits {
+ spending_limit.invariant()?;
+ }
+
+ Ok(())
+ }
+
+ /// Validate the payload for the policy
+ fn validate_payload(
+ &self,
+ context: PolicyExecutionContext,
+ payload: &Self::UsagePayload,
+ ) -> Result<()> {
+ // Validate that the payload is valid for the context
+ match (context, &payload.transaction_payload) {
+ (
+ PolicyExecutionContext::Synchronous,
+ ProgramInteractionTransactionPayload::AsyncTransaction(..),
+ ) => {
+ return Err(
+ SmartAccountError::ProgramInteractionAsyncPayloadNotAllowedWithSyncTransaction
+ .into(),
+ );
+ }
+ (
+ PolicyExecutionContext::Asynchronous,
+ ProgramInteractionTransactionPayload::SyncTransaction(..),
+ ) => {
+ return Err(
+ SmartAccountError::ProgramInteractionSyncPayloadNotAllowedWithAsyncTransaction
+ .into(),
+ );
+ }
+ // Both other variants are valid
+ (_, _) => {}
+ }
+
+ // Get the account index and instructions length
+ let payload_account_index = payload.transaction_payload.get_account_index();
+ let instructions_len = match &payload.transaction_payload {
+ ProgramInteractionTransactionPayload::AsyncTransaction(transaction_payload) => {
+ // TODO: Inefficient to deserialize the transaction message and
+ // not do anything with it.
+ TransactionMessage::deserialize(
+ &mut transaction_payload.transaction_message.as_slice(),
+ )?
+ .instructions
+ .len()
+ }
+ ProgramInteractionTransactionPayload::SyncTransaction(sync_transaction_payload) => {
+ let instructions: SmallVec =
+ SmallVec::::try_from_slice(
+ &sync_transaction_payload.instructions,
+ )
+ .map_err(|_| SmartAccountError::InvalidInstructionArgs)?;
+ instructions.len()
+ }
+ };
+ require_eq!(
+ payload_account_index,
+ self.account_index,
+ SmartAccountError::InvalidPayload
+ );
+
+ // If there are instruction constraints, ensure that the submitted instruction constraints are valid
+ if !self.instructions_constraints.is_empty() {
+ if let Some(instruction_constraint_indices) = &payload.instruction_constraint_indices {
+ // Ensure that the instruction indices match the number of
+ // instructions
+ require_eq!(
+ instruction_constraint_indices.len(),
+ instructions_len,
+ SmartAccountError::ProgramInteractionInstructionCountMismatch
+ );
+ // Ensure that the instruction constraint index is within the bounds
+ // of the instructions constraints
+ for instruction_constraint_index in instruction_constraint_indices {
+ require!(
+ *instruction_constraint_index < self.instructions_constraints.len() as u8,
+ SmartAccountError::ProgramInteractionConstraintIndexOutOfBounds
+ );
+ }
+ } else {
+ return Err(SmartAccountError::ProgramInteractionInstructionCountMismatch.into());
+ }
+ }
+ Ok(())
+ }
+
+ // Wrapper method to distinguish between transaction and sync transaction payloads
+ fn execute_payload<'info>(
+ &mut self,
+ args: Self::ExecutionArgs,
+ payload: &Self::UsagePayload,
+ accounts: &'info [AccountInfo<'info>],
+ ) -> Result<()> {
+ match &payload.transaction_payload {
+ ProgramInteractionTransactionPayload::AsyncTransaction(..) => {
+ self.execute_payload_async(args, payload, accounts)
+ }
+ ProgramInteractionTransactionPayload::SyncTransaction(..) => {
+ self.execute_payload_sync(args, payload, accounts)
+ }
+ }
+ }
+}
+
+// =============================================================================
+// ASYNC TRANSACTION EXECUTION
+// =============================================================================
+
+impl ProgramInteractionPolicy {
+ /// Execute an async transaction through the policy
+ fn execute_payload_async<'info>(
+ &mut self,
+ args: ProgramInteractionExecutionArgs,
+ payload: &ProgramInteractionPayload,
+ mut accounts: &'info [AccountInfo<'info>],
+ ) -> Result<()> {
+ // Get the transaction payload
+ let transaction_payload = payload.get_transaction_payload(args.transaction_key)?;
+
+ // Largely copied from `transaction_execute.rs`
+ let smart_account_seeds = &[
+ SEED_PREFIX,
+ args.settings_key.as_ref(),
+ SEED_SMART_ACCOUNT,
+ &transaction_payload.account_index.to_le_bytes(),
+ ];
+ let (smart_account_pubkey, smart_account_bump) =
+ Pubkey::find_program_address(smart_account_seeds, &crate::ID);
+
+ let smart_account_signer_seeds = &[
+ smart_account_seeds[0],
+ smart_account_seeds[1],
+ smart_account_seeds[2],
+ smart_account_seeds[3],
+ &[smart_account_bump],
+ ];
+
+ // Parse out the hook accounts from the accounts slice
+ let (pre_hook_accounts, post_hook_accounts) = self.parse_hook_accounts(&mut accounts);
+
+ // Get the message account infos and address lookup table account infos
+ let num_lookups = transaction_payload.message.address_table_lookups.len();
+ // Execute the transaction
+ let message_account_infos = accounts
+ .get(num_lookups..)
+ .ok_or(SmartAccountError::InvalidNumberOfAccounts)?;
+ let address_lookup_table_account_infos = accounts
+ .get(..num_lookups)
+ .ok_or(SmartAccountError::InvalidNumberOfAccounts)?;
+
+ // Evaluate the instruction constraints
+ if let Some(instruction_constraint_indices) = &payload.instruction_constraint_indices {
+ self.evaluate_instruction_constraints(
+ instruction_constraint_indices,
+ &transaction_payload.message.instructions,
+ message_account_infos,
+ )?;
+ }
+
+ // Execute the pre hook
+ if let Some(pre_hook) = &self.pre_hook {
+ pre_hook.execute(
+ pre_hook_accounts,
+ &transaction_payload.message.instructions,
+ &accounts[num_lookups..],
+ )?;
+ }
+ let (ephemeral_signer_keys, ephemeral_signer_seeds) = derive_ephemeral_signers(
+ args.transaction_key,
+ &transaction_payload.ephemeral_signer_bumps,
+ );
+
+ let executable_message = ExecutableTransactionMessage::new_validated(
+ transaction_payload.message.clone(),
+ message_account_infos,
+ address_lookup_table_account_infos,
+ &smart_account_pubkey,
+ &ephemeral_signer_keys,
+ )?;
+
+ let protected_accounts = &[args.proposal_key];
+
+ // Update the spending limits if present
+ if !self.spending_limits.is_empty() {
+ let current_timestamp = Clock::get()?.unix_timestamp;
+ // Reset the spending limits if needed
+ for spending_limit in &mut self.spending_limits {
+ spending_limit.reset_if_needed(current_timestamp);
+ }
+
+ let tracked_pre_balances = check_pre_balances(smart_account_pubkey, accounts);
+ // Execute the transaction message instructions one-by-one.
+ // NOTE: `execute_message()` calls `self.to_instructions_and_accounts()`
+ // which in turn calls `take()` on
+ // `self.message.instructions`, therefore after this point no more
+ // references or usages of `self.message` should be made to avoid
+ // faulty behavior.
+ executable_message.execute_message(
+ smart_account_signer_seeds,
+ &ephemeral_signer_seeds,
+ protected_accounts,
+ )?;
+ // Evaluate the balance changes post-execution
+ tracked_pre_balances.evaluate_balance_changes(&mut self.spending_limits)?;
+ } else {
+ // Execute the transaction message instructions one-by-one.
+ // NOTE: `execute_message()` calls `self.to_instructions_and_accounts()`
+ // which in turn calls `take()` on
+ // `self.message.instructions`, therefore after this point no more
+ // references or usages of `self.message` should be made to avoid
+ // faulty behavior.
+ executable_message.execute_message(
+ smart_account_signer_seeds,
+ &ephemeral_signer_seeds,
+ protected_accounts,
+ )?;
+ }
+
+ // Execute post hook
+ if let Some(post_hook) = &self.post_hook {
+ post_hook.execute(
+ post_hook_accounts,
+ &transaction_payload.message.instructions,
+ &accounts[num_lookups..],
+ )?;
+ }
+ Ok(())
+ }
+
+ // =============================================================================
+ // SYNC TRANSACTION EXECUTION
+ // =============================================================================
+
+ /// Execute a synchronous transaction through the policy
+ fn execute_payload_sync<'info>(
+ &mut self,
+ args: ProgramInteractionExecutionArgs,
+ payload: &ProgramInteractionPayload,
+ mut accounts: &'info [AccountInfo<'info>],
+ ) -> Result<()> {
+ // Get the sync transaction payload
+ let sync_transaction_payload = payload.get_sync_transaction_payload()?;
+ // Get the settings key
+ let settings_key = args.settings_key;
+ // Validate the instructions
+ let instructions = SmallVec::::try_from_slice(
+ &sync_transaction_payload.instructions,
+ )
+ .map_err(|_| SmartAccountError::InvalidInstructionArgs)?;
+
+ // Convert to SmartAccountCompiledInstruction
+ let settings_compiled_instructions: Vec =
+ Vec::from(instructions)
+ .into_iter()
+ .map(SmartAccountCompiledInstruction::from)
+ .collect();
+ // Get the smart account seeds
+ let smart_account_seeds = &[
+ SEED_PREFIX,
+ settings_key.as_ref(),
+ SEED_SMART_ACCOUNT,
+ &sync_transaction_payload.account_index.to_le_bytes(),
+ ];
+ let (smart_account_pubkey, smart_account_bump) =
+ Pubkey::find_program_address(smart_account_seeds, &crate::ID);
+
+ // Get the signer seeds for the smart account
+ let smart_account_signer_seeds = &[
+ smart_account_seeds[0],
+ smart_account_seeds[1],
+ smart_account_seeds[2],
+ smart_account_seeds[3],
+ &[smart_account_bump],
+ ];
+
+ // Parse out the hook accounts from the accounts slice
+ let (pre_hook_accounts, post_hook_accounts) = self.parse_hook_accounts(&mut accounts);
+
+ // Evaluate the instruction constraints
+ if let Some(instruction_constraint_indices) = &payload.instruction_constraint_indices {
+ self.evaluate_instruction_constraints(
+ instruction_constraint_indices,
+ &settings_compiled_instructions,
+ accounts,
+ )?;
+ }
+
+ // Execute the pre hook
+ if let Some(pre_hook) = &self.pre_hook {
+ pre_hook.execute(
+ pre_hook_accounts,
+ &settings_compiled_instructions,
+ &accounts,
+ )?;
+ }
+
+ let executable_message = SynchronousTransactionMessage::new_validated(
+ &settings_key,
+ &smart_account_pubkey,
+ &args.policy_signers,
+ &settings_compiled_instructions,
+ accounts,
+ )?;
+
+ // Update the spending limits if present
+ if !self.spending_limits.is_empty() {
+ let current_timestamp = Clock::get()?.unix_timestamp;
+ // Reset the spending limits if needed
+ for spending_limit in &mut self.spending_limits {
+ spending_limit.reset_if_needed(current_timestamp);
+ }
+
+ let tracked_pre_balances = check_pre_balances(smart_account_pubkey, accounts);
+ // Execute the transaction message instructions one-by-one.
+ // NOTE: `execute_message()` calls `self.to_instructions_and_accounts()`
+ // which in turn calls `take()` on
+ // `self.message.instructions`, therefore after this point no more
+ // references or usages of `self.message` should be made to avoid
+ // faulty behavior.
+ executable_message.execute(smart_account_signer_seeds)?;
+ // Evaluate the balance changes post-execution
+ tracked_pre_balances.evaluate_balance_changes(&mut self.spending_limits)?;
+ } else {
+ // Execute the transaction message instructions one-by-one.
+ // NOTE: `execute_message()` calls `self.to_instructions_and_accounts()`
+ // which in turn calls `take()` on
+ // `self.message.instructions`, therefore after this point no more
+ // references or usages of `self.message` should be made to avoid
+ // faulty behavior.
+ executable_message.execute(smart_account_signer_seeds)?;
+ }
+ // Execute the post hook
+ if let Some(post_hook) = &self.post_hook {
+ post_hook.execute(
+ post_hook_accounts,
+ &settings_compiled_instructions,
+ &accounts,
+ )?;
+ }
+
+ Ok(())
+ }
+}
+
+// =============================================================================
+// TESTS
+// =============================================================================
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_data_constraint_u8_equals() {
+ let constraint = DataConstraint {
+ data_offset: 0,
+ data_value: DataValue::U8(42),
+ operator: DataOperator::Equals,
+ };
+
+ assert!(constraint.evaluate(&[42]).is_ok());
+
+ assert_eq!(
+ constraint.evaluate(&[41]).err().unwrap(),
+ SmartAccountError::ProgramInteractionInvalidNumericValue.into()
+ );
+ }
+
+ #[test]
+ fn test_data_constraint_u8_greater_than() {
+ let constraint = DataConstraint {
+ data_offset: 0,
+ data_value: DataValue::U8(10),
+ operator: DataOperator::GreaterThan,
+ };
+
+ assert!(constraint.evaluate(&[11]).is_ok());
+
+ assert_eq!(
+ constraint.evaluate(&[10]).err().unwrap(),
+ SmartAccountError::ProgramInteractionInvalidNumericValue.into()
+ );
+ assert_eq!(
+ constraint.evaluate(&[9]).err().unwrap(),
+ SmartAccountError::ProgramInteractionInvalidNumericValue.into()
+ );
+ }
+
+ #[test]
+ fn test_data_constraint_u16_little_endian() {
+ let constraint = DataConstraint {
+ data_offset: 1,
+ data_value: DataValue::U16Le(0x1234),
+ operator: DataOperator::Equals,
+ };
+
+ // Little endian: 0x1234 = [0x34, 0x12]
+ assert!(constraint.evaluate(&[0x00, 0x34, 0x12]).is_ok());
+
+ assert_eq!(
+ constraint.evaluate(&[0x00, 0x12, 0x34]).err().unwrap(),
+ SmartAccountError::ProgramInteractionInvalidNumericValue.into()
+ );
+ }
+
+ #[test]
+ fn test_data_constraint_u32_less_than_or_equal() {
+ let constraint = DataConstraint {
+ data_offset: 0,
+ data_value: DataValue::U32Le(1000),
+ operator: DataOperator::LessThanOrEqualTo,
+ };
+
+ // Little endian: 1000 = 0x03E8 = [0xE8, 0x03, 0x00, 0x00]
+ assert!(constraint.evaluate(&[0xE8, 0x03, 0x00, 0x00]).is_ok()); // 1000
+ assert!(constraint.evaluate(&[0xE7, 0x03, 0x00, 0x00]).is_ok()); // 999
+ assert_eq!(
+ constraint
+ .evaluate(&[0xE9, 0x03, 0x00, 0x00])
+ .err()
+ .unwrap(),
+ SmartAccountError::ProgramInteractionInvalidNumericValue.into()
+ ); // 1001
+ }
+
+ #[test]
+ fn test_data_constraint_u64_not_equals() {
+ let constraint = DataConstraint {
+ data_offset: 0,
+ data_value: DataValue::U64Le(0x123456789ABCDEF0),
+ operator: DataOperator::NotEquals,
+ };
+
+ let target_bytes = 0x123456789ABCDEF0u64.to_le_bytes();
+ let different_bytes = 0x123456789ABCDEF1u64.to_le_bytes();
+
+ assert_eq!(
+ constraint.evaluate(&target_bytes).err().unwrap(),
+ SmartAccountError::ProgramInteractionInvalidNumericValue.into()
+ );
+ assert!(constraint.evaluate(&different_bytes).is_ok());
+ }
+
+ #[test]
+ fn test_data_constraint_u128_greater_than_or_equal() {
+ let constraint = DataConstraint {
+ data_offset: 0,
+ data_value: DataValue::U128Le(1000),
+ operator: DataOperator::GreaterThanOrEqualTo,
+ };
+
+ let equal_bytes = 1000u128.to_le_bytes();
+ let greater_bytes = 1001u128.to_le_bytes();
+ let lesser_bytes = 999u128.to_le_bytes();
+
+ assert!(constraint.evaluate(&equal_bytes).is_ok());
+ assert!(constraint.evaluate(&greater_bytes).is_ok());
+ assert_eq!(
+ constraint.evaluate(&lesser_bytes).err().unwrap(),
+ SmartAccountError::ProgramInteractionInvalidNumericValue.into()
+ );
+ }
+
+ #[test]
+ fn test_data_constraint_u8_slice_equals() {
+ let constraint = DataConstraint {
+ data_offset: 8,
+ data_value: DataValue::U8Slice(vec![0xDE, 0xAD, 0xBE, 0xEF]),
+ operator: DataOperator::Equals,
+ };
+
+ let mut data = vec![0; 12];
+ data[8..12].copy_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF]);
+
+ assert!(constraint.evaluate(&data).is_ok());
+
+ // Different bytes
+ data[8] = 0xFF;
+ assert_eq!(
+ constraint.evaluate(&data).err().unwrap(),
+ SmartAccountError::ProgramInteractionInvalidNumericValue.into()
+ );
+ }
+
+ #[test]
+ fn test_data_constraint_u8_slice_not_equals() {
+ let constraint = DataConstraint {
+ data_offset: 0,
+ data_value: DataValue::U8Slice(vec![0x01, 0x02, 0x03]),
+ operator: DataOperator::NotEquals,
+ };
+
+ assert!(constraint.evaluate(&[0x01, 0x02, 0x04]).is_ok());
+ assert_eq!(
+ constraint.evaluate(&[0x01, 0x02, 0x03]).err().unwrap(),
+ SmartAccountError::ProgramInteractionInvalidNumericValue.into()
+ );
+ }
+
+ #[test]
+ fn test_data_constraint_u8_slice_invalid_operator() {
+ let constraint = DataConstraint {
+ data_offset: 0,
+ data_value: DataValue::U8Slice(vec![0x01]),
+ operator: DataOperator::GreaterThan, // Invalid for U8Slice
+ };
+
+ assert_eq!(
+ constraint.evaluate(&[0x01]).err().unwrap(),
+ SmartAccountError::ProgramInteractionUnsupportedSliceOperator.into()
+ );
+ }
+
+ #[test]
+ fn test_data_constraint_out_of_bounds() {
+ let constraint = DataConstraint {
+ data_offset: 5,
+ data_value: DataValue::U8(42),
+ operator: DataOperator::Equals,
+ };
+
+ // Data too short
+ assert!(
+ constraint.evaluate(&[1, 2, 3]).err().unwrap()
+ == SmartAccountError::ProgramInteractionDataTooShort.into()
+ );
+
+ // Exact boundary
+ assert_eq!(
+ constraint.evaluate(&[1, 2, 3, 4, 5]).err().unwrap(),
+ SmartAccountError::ProgramInteractionDataTooShort.into()
+ );
+
+ // Just enough data
+ assert_eq!(
+ constraint.evaluate(&[1, 2, 3, 4, 5, 41]).err().unwrap(),
+ SmartAccountError::ProgramInteractionInvalidNumericValue.into()
+ );
+ assert!(constraint.evaluate(&[1, 2, 3, 4, 5, 42]).is_ok());
+ }
+
+ #[test]
+ fn test_data_constraint_multi_byte_out_of_bounds() {
+ let constraint = DataConstraint {
+ data_offset: 2,
+ data_value: DataValue::U32Le(1000),
+ operator: DataOperator::Equals,
+ };
+
+ // Need 4 bytes starting at offset 2, so need at least 6 bytes total
+ assert_eq!(
+ constraint.evaluate(&[1, 2, 3, 4, 5]).err().unwrap(),
+ SmartAccountError::ProgramInteractionDataTooShort.into()
+ ); // Only 5 bytes
+
+ let mut data = vec![0; 6];
+ data[2..6].copy_from_slice(&1000u32.to_le_bytes());
+ assert!(constraint.evaluate(&data).is_ok());
+ }
+
+ #[test]
+ fn test_data_constraint_solana_instruction_discriminator() {
+ // Simulate checking for a specific Solana instruction discriminator
+ let swap_discriminator = [0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF];
+
+ let constraint = DataConstraint {
+ data_offset: 0,
+ data_value: DataValue::U8Slice(swap_discriminator.to_vec()),
+ operator: DataOperator::Equals,
+ };
+
+ // Create instruction data with correct discriminator + some payload
+ let mut instruction_data = swap_discriminator.to_vec();
+ instruction_data.extend_from_slice(&[0xFF, 0xEE, 0xDD, 0xCC]); // Additional data
+
+ assert!(constraint.evaluate(&instruction_data).is_ok());
+
+ // Wrong discriminator
+ let wrong_discriminator = [0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEE];
+ let mut wrong_data = wrong_discriminator.to_vec();
+ wrong_data.extend_from_slice(&[0xFF, 0xEE, 0xDD, 0xCC]);
+
+ assert_eq!(
+ constraint.evaluate(&wrong_data).err().unwrap(),
+ SmartAccountError::ProgramInteractionInvalidNumericValue.into()
+ );
+ }
+
+ #[test]
+ fn test_data_constraint_amount_validation() {
+ // Simulate validating a swap amount in instruction data
+ // Amount at offset 12 (after 8-byte discriminator + 4-byte other data)
+ let max_amount = 1000u64;
+
+ let constraint = DataConstraint {
+ data_offset: 12,
+ data_value: DataValue::U64Le(max_amount),
+ operator: DataOperator::LessThanOrEqualTo,
+ };
+
+ // Create instruction with amount = 500 (valid)
+ let mut instruction_data = vec![0; 20]; // Discriminator + other data + amount
+ instruction_data[12..20].copy_from_slice(&500u64.to_le_bytes());
+ assert!(constraint.evaluate(&instruction_data).is_ok());
+
+ // Create instruction with amount = 1500 (invalid)
+ instruction_data[12..20].copy_from_slice(&1500u64.to_le_bytes());
+ assert_eq!(
+ constraint.evaluate(&instruction_data).err().unwrap(),
+ SmartAccountError::ProgramInteractionInvalidNumericValue.into()
+ );
+
+ // Exactly at limit (valid)
+ instruction_data[12..20].copy_from_slice(&1000u64.to_le_bytes());
+ assert!(constraint.evaluate(&instruction_data).is_ok());
+ }
+
+ #[test]
+ #[ignore = "This test doesn't work because `to_policy_state` uses the Clock"]
+ fn test_creation_payload_size_calculation() {
+ let payload = ProgramInteractionPolicyCreationPayloadLegacy {
+ account_index: 1,
+ pre_hook: None,
+ post_hook: None,
+ instructions_constraints: vec![InstructionConstraint {
+ program_id: Pubkey::new_unique(),
+ account_constraints: vec![AccountConstraint {
+ account_index: 0,
+ account_constraint: AccountConstraintType::Pubkey(vec![
+ Pubkey::new_unique(),
+ Pubkey::new_unique(),
+ ]),
+ owner: None,
+ }],
+ data_constraints: vec![
+ DataConstraint {
+ data_offset: 0,
+ data_value: DataValue::U8Slice(vec![
+ 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF,
+ ]),
+ operator: DataOperator::Equals,
+ },
+ DataConstraint {
+ data_offset: 12,
+ data_value: DataValue::U64Le(1000),
+ operator: DataOperator::LessThanOrEqualTo,
+ },
+ ],
+ }],
+ spending_limits: vec![LimitedSpendingLimit {
+ mint: Pubkey::new_unique(),
+ time_constraints: LimitedTimeConstraints {
+ start: 1640995200, // Jan 1, 2022
+ expiration: Some(1672531200), // Jan 1, 2023
+ period: PeriodV2::Daily,
+ },
+ quantity_constraints: LimitedQuantityConstraints {
+ max_per_period: 1000,
+ },
+ }],
+ };
+
+ let calculated_size = payload.creation_payload_size();
+ let actual_serialized = payload.try_to_vec().unwrap();
+ let actual_size = actual_serialized.len();
+
+ // Since InitSpace overestimates size, we only check that the calculated
+ // size is greater than or equal to the actual size to make sure
+ // serialization succeeds
+ assert!(calculated_size >= actual_size);
+ }
+
+ #[test]
+ #[ignore = "This test doesn't work because `to_policy_state` uses the Clock"]
+ fn test_policy_state_size_calculation() {
+ let payload = ProgramInteractionPolicyCreationPayloadLegacy {
+ account_index: 1,
+ pre_hook: None,
+ post_hook: None,
+ instructions_constraints: vec![InstructionConstraint {
+ program_id: Pubkey::new_unique(),
+ account_constraints: vec![AccountConstraint {
+ account_index: 0,
+ account_constraint: AccountConstraintType::Pubkey(vec![
+ Pubkey::new_unique(),
+ Pubkey::new_unique(),
+ ]),
+ owner: None,
+ }],
+ data_constraints: vec![
+ DataConstraint {
+ data_offset: 0,
+ data_value: DataValue::U8Slice(vec![
+ 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF,
+ ]),
+ operator: DataOperator::Equals,
+ },
+ DataConstraint {
+ data_offset: 12,
+ data_value: DataValue::U64Le(1000),
+ operator: DataOperator::LessThanOrEqualTo,
+ },
+ ],
+ }],
+ spending_limits: vec![LimitedSpendingLimit {
+ mint: Pubkey::new_unique(),
+ time_constraints: LimitedTimeConstraints {
+ start: 1640995200,
+ expiration: Some(1672531200),
+ period: PeriodV2::Daily,
+ },
+ quantity_constraints: LimitedQuantityConstraints {
+ max_per_period: 1000,
+ },
+ }],
+ };
+
+ let policy = payload.clone().to_policy_state().unwrap();
+ let calculated_size = payload.policy_state_size();
+ let actual_serialized = policy.try_to_vec().unwrap();
+ let actual_size = actual_serialized.len();
+
+ // Since InitSpace overestimates size, we only check that the calculated
+ // size is greater than or equal to the actual size to make sure
+ // serialization succeeds
+ assert!(calculated_size >= actual_size);
+ }
+
+ #[test]
+ fn test_resolve_pubkey() {
+ let custom1 = Pubkey::new_unique();
+ let custom2 = Pubkey::new_unique();
+ let custom3 = Pubkey::new_unique();
+
+ let payload = ProgramInteractionPolicyCreationPayload {
+ account_index: 0,
+ pubkey_table: SmallVec::from(vec![custom1, custom2, custom3]),
+ instructions_constraints: SmallVec::from(vec![]),
+ pre_hook: None,
+ post_hook: None,
+ spending_limits: SmallVec::from(vec![]),
+ };
+
+ // Test custom pubkey indices
+ assert_eq!(payload.resolve_pubkey(0).unwrap(), custom1);
+ assert_eq!(payload.resolve_pubkey(1).unwrap(), custom2);
+ assert_eq!(payload.resolve_pubkey(2).unwrap(), custom3);
+
+ // Test out of bounds
+ assert!(payload.resolve_pubkey(3).is_err());
+ }
+
+ #[test]
+ fn test_resolve_pubkey_with_builtins() {
+ let payload = ProgramInteractionPolicyCreationPayload {
+ account_index: 0,
+ pubkey_table: SmallVec::from(vec![]),
+ instructions_constraints: SmallVec::from(vec![]),
+ pre_hook: None,
+ post_hook: None,
+ spending_limits: SmallVec::from(vec![]),
+ };
+
+ // Test all builtin program indices
+ assert_eq!(payload.resolve_pubkey(240).unwrap(), anchor_lang::system_program::ID);
+ assert_eq!(payload.resolve_pubkey(241).unwrap(), anchor_spl::token::ID);
+ assert_eq!(payload.resolve_pubkey(242).unwrap(), anchor_spl::associated_token::ID);
+ assert_eq!(payload.resolve_pubkey(243).unwrap(), anchor_spl::token_2022::ID);
+ assert_eq!(payload.resolve_pubkey(244).unwrap(), anchor_spl::mint::USDC);
+ assert_eq!(payload.resolve_pubkey(245).unwrap(), WRAPPED_SOL);
+
+ // Test invalid builtin index
+ assert!(payload.resolve_pubkey(246).is_err());
+ assert!(payload.resolve_pubkey(255).is_err());
+ }
+
+ #[test]
+ fn test_expand_instruction() {
+ let custom1 = Pubkey::new_unique();
+ let custom2 = Pubkey::new_unique();
+
+ let payload = ProgramInteractionPolicyCreationPayload {
+ account_index: 0,
+ pubkey_table: SmallVec::from(vec![custom1, custom2]),
+ instructions_constraints: SmallVec::from(vec![]),
+ pre_hook: None,
+ post_hook: None,
+ spending_limits: SmallVec::from(vec![]),
+ };
+
+ let compiled = CompiledInstructionConstraint {
+ program_id_index: 240, // System Program (builtin)
+ account_constraints: SmallVec::from(vec![
+ CompiledAccountConstraint {
+ account_index: 0,
+ account_constraint: CompiledAccountConstraintType::Pubkey(
+ SmallVec::from(vec![0, 241, 1]) // custom1, Token Program (builtin), custom2
+ ),
+ owner_index: Some(242), // Associated Token Program (builtin)
+ },
+ CompiledAccountConstraint {
+ account_index: 1,
+ account_constraint: CompiledAccountConstraintType::AccountData(
+ SmallVec::from(vec![
+ DataConstraint {
+ data_offset: 0,
+ data_value: DataValue::U8(42),
+ operator: DataOperator::Equals,
+ }
+ ])
+ ),
+ owner_index: None,
+ },
+ ]),
+ data_constraints: SmallVec::from(vec![
+ DataConstraint {
+ data_offset: 8,
+ data_value: DataValue::U64Le(1000),
+ operator: DataOperator::LessThanOrEqualTo,
+ }
+ ]),
+ };
+
+ let expanded = payload.expand_instruction_constraint(&compiled).unwrap();
+
+ let expected = InstructionConstraint {
+ program_id: anchor_lang::system_program::ID,
+ account_constraints: vec![
+ AccountConstraint {
+ account_index: 0,
+ account_constraint: AccountConstraintType::Pubkey(vec![
+ custom1,
+ anchor_spl::token::ID,
+ custom2,
+ ]),
+ owner: Some(anchor_spl::associated_token::ID),
+ },
+ AccountConstraint {
+ account_index: 1,
+ account_constraint: AccountConstraintType::AccountData(vec![
+ DataConstraint {
+ data_offset: 0,
+ data_value: DataValue::U8(42),
+ operator: DataOperator::Equals,
+ }
+ ]),
+ owner: None,
+ },
+ ],
+ data_constraints: vec![
+ DataConstraint {
+ data_offset: 8,
+ data_value: DataValue::U64Le(1000),
+ operator: DataOperator::LessThanOrEqualTo,
+ }
+ ],
+ };
+
+ assert_eq!(expanded, expected);
+ }
+
+ #[test]
+ fn test_expand_hook() {
+ let custom_program = Pubkey::new_unique();
+
+ let payload = ProgramInteractionPolicyCreationPayload {
+ account_index: 0,
+ pubkey_table: SmallVec::from(vec![custom_program]),
+ instructions_constraints: SmallVec::from(vec![]),
+ pre_hook: None,
+ post_hook: None,
+ spending_limits: SmallVec::from(vec![]),
+ };
+
+ let compiled = CompiledHook {
+ num_extra_accounts: 3,
+ account_constraints: SmallVec::from(vec![
+ CompiledAccountConstraint {
+ account_index: 0,
+ account_constraint: CompiledAccountConstraintType::Pubkey(
+ SmallVec::from(vec![240, 0]) // System Program (builtin), custom_program
+ ),
+ owner_index: Some(241), // Token Program (builtin)
+ },
+ ]),
+ instruction_data: SmallVec::from(vec![1, 2, 3, 4, 5]),
+ program_id_index: 243, // Token-2022 Program (builtin)
+ pass_inner_instructions: true,
+ };
+
+ let expanded = payload.expand_hook(&compiled).unwrap();
+
+ let expected = Hook {
+ num_extra_accounts: 3,
+ account_constraints: vec![
+ AccountConstraint {
+ account_index: 0,
+ account_constraint: AccountConstraintType::Pubkey(vec![
+ anchor_lang::system_program::ID,
+ custom_program,
+ ]),
+ owner: Some(anchor_spl::token::ID),
+ },
+ ],
+ instruction_data: vec![1, 2, 3, 4, 5],
+ program_id: anchor_spl::token_2022::ID,
+ pass_inner_instructions: true,
+ };
+
+ assert_eq!(expanded, expected);
+ }
+
+ #[test]
+ fn test_expand_spending_limits() {
+ let custom_mint = Pubkey::new_unique();
+
+ let payload = ProgramInteractionPolicyCreationPayload {
+ account_index: 0,
+ pubkey_table: SmallVec::from(vec![custom_mint]),
+ instructions_constraints: SmallVec::from(vec![]),
+ pre_hook: None,
+ post_hook: None,
+ spending_limits: SmallVec::from(vec![]),
+ };
+
+ // Test with custom mint
+ let compiled_custom = CompiledLimitedSpendingLimit {
+ mint_index: 0, // custom_mint
+ time_constraints: LimitedTimeConstraints {
+ start: 1640995200,
+ expiration: Some(1672531200),
+ period: PeriodV2::Daily,
+ },
+ quantity_constraints: LimitedQuantityConstraints {
+ max_per_period: 1000,
+ },
+ };
+
+ let expanded_custom = payload.expand_spending_limit(&compiled_custom).unwrap();
+ let expected_custom = LimitedSpendingLimit {
+ mint: custom_mint,
+ time_constraints: LimitedTimeConstraints {
+ start: 1640995200,
+ expiration: Some(1672531200),
+ period: PeriodV2::Daily,
+ },
+ quantity_constraints: LimitedQuantityConstraints {
+ max_per_period: 1000,
+ },
+ };
+ assert_eq!(expanded_custom, expected_custom);
+
+ // Test with USDC builtin
+ let compiled_usdc = CompiledLimitedSpendingLimit {
+ mint_index: 244, // USDC
+ time_constraints: LimitedTimeConstraints {
+ start: 1640995200,
+ expiration: None,
+ period: PeriodV2::Weekly,
+ },
+ quantity_constraints: LimitedQuantityConstraints {
+ max_per_period: 5000,
+ },
+ };
+
+ let expanded_usdc = payload.expand_spending_limit(&compiled_usdc).unwrap();
+ let expected_usdc = LimitedSpendingLimit {
+ mint: anchor_spl::mint::USDC,
+ time_constraints: LimitedTimeConstraints {
+ start: 1640995200,
+ expiration: None,
+ period: PeriodV2::Weekly,
+ },
+ quantity_constraints: LimitedQuantityConstraints {
+ max_per_period: 5000,
+ },
+ };
+ assert_eq!(expanded_usdc, expected_usdc);
+
+ // Test with Wrapped SOL builtin
+ let compiled_wsol = CompiledLimitedSpendingLimit {
+ mint_index: 245, // Wrapped SOL
+ time_constraints: LimitedTimeConstraints {
+ start: 1640995200,
+ expiration: Some(1704067200),
+ period: PeriodV2::Monthly,
+ },
+ quantity_constraints: LimitedQuantityConstraints {
+ max_per_period: 10000,
+ },
+ };
+
+ let expanded_wsol = payload.expand_spending_limit(&compiled_wsol).unwrap();
+ let expected_wsol = LimitedSpendingLimit {
+ mint: WRAPPED_SOL,
+ time_constraints: LimitedTimeConstraints {
+ start: 1640995200,
+ expiration: Some(1704067200),
+ period: PeriodV2::Monthly,
+ },
+ quantity_constraints: LimitedQuantityConstraints {
+ max_per_period: 10000,
+ },
+ };
+ assert_eq!(expanded_wsol, expected_wsol);
+ }
+}
diff --git a/programs/squads_smart_account_program/src/state/policies/implementations/settings_change.rs b/programs/squads_smart_account_program/src/state/policies/implementations/settings_change.rs
new file mode 100644
index 0000000..6a50911
--- /dev/null
+++ b/programs/squads_smart_account_program/src/state/policies/implementations/settings_change.rs
@@ -0,0 +1,567 @@
+use anchor_lang::prelude::*;
+
+use crate::{
+ errors::SmartAccountError, get_settings_signer_seeds, program::SquadsSmartAccountProgram,
+ state::Settings, LogAuthorityInfo, Permissions, PolicyExecutionContext,
+ PolicyPayloadConversionTrait, PolicySizeTrait, PolicyTrait, SettingsAction,
+ SettingsChangePolicyEvent, SmartAccountEvent, SmartAccountSigner,
+};
+
+/// == SettingsChangePolicy ==
+/// This policy allows for the modification of the settings of a smart account.
+///
+/// The policy is defined by a set of allowed settings changes.
+///===============================================
+
+// =============================================================================
+// CORE POLICY STRUCTURES
+// =============================================================================
+#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, Eq, Debug)]
+pub struct SettingsChangePolicy {
+ pub actions: Vec,
+}
+/// Defines which settings changes are allowed by the policy
+#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, Eq, Debug, InitSpace)]
+pub enum AllowedSettingsChange {
+ AddSigner {
+ /// Some() - add a specific signer, None - add any signer
+ new_signer: Option,
+ /// Some() - only allow certain permissions, None - allow all permissions
+ new_signer_permissions: Option,
+ },
+ RemoveSigner {
+ /// Some() - remove a specific signer, None - remove any signer
+ old_signer: Option,
+ },
+ ChangeThreshold,
+ ChangeTimeLock {
+ /// Some() - change timelock to a specific value, None - change timelock to any value
+ new_time_lock: Option,
+ },
+}
+
+// =============================================================================
+// CREATION PAYLOAD TYPES
+// =============================================================================
+
+#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, Eq, Debug)]
+pub struct SettingsChangePolicyCreationPayload {
+ pub actions: Vec,
+}
+
+// =============================================================================
+// EXECUTION PAYLOAD TYPES
+// =============================================================================
+
+/// Limited subset of settings change actions for execution
+#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, Eq)]
+pub enum LimitedSettingsAction {
+ AddSigner { new_signer: SmartAccountSigner },
+ RemoveSigner { old_signer: Pubkey },
+ ChangeThreshold { new_threshold: u16 },
+ SetTimeLock { new_time_lock: u32 },
+}
+
+#[derive(AnchorSerialize, AnchorDeserialize, Clone, PartialEq, Eq)]
+pub struct SettingsChangePayload {
+ pub action_index: Vec,
+ pub actions: Vec,
+}
+
+pub struct SettingsChangeExecutionArgs {
+ pub settings_key: Pubkey,
+}
+
+pub struct ValidatedAccounts<'info> {
+ pub settings: Account<'info, Settings>,
+ /// Optional just to comply with later use of Settings::modify_with_action
+ pub rent_payer: Option>,
+ /// Optional just to comply with later use of Settings::modify_with_action
+ pub system_program: Option>,
+ /// Program account used for logging
+ pub program: Program<'info, SquadsSmartAccountProgram>,
+}
+
+// =============================================================================
+// CONVERSION IMPLEMENTATIONS
+// =============================================================================
+
+impl From for SettingsAction {
+ fn from(action: LimitedSettingsAction) -> Self {
+ match action {
+ LimitedSettingsAction::AddSigner { new_signer } => {
+ SettingsAction::AddSigner { new_signer }
+ }
+ LimitedSettingsAction::RemoveSigner { old_signer } => {
+ SettingsAction::RemoveSigner { old_signer }
+ }
+ LimitedSettingsAction::ChangeThreshold { new_threshold } => {
+ SettingsAction::ChangeThreshold { new_threshold }
+ }
+ LimitedSettingsAction::SetTimeLock { new_time_lock } => {
+ SettingsAction::SetTimeLock { new_time_lock }
+ }
+ }
+ }
+}
+
+impl PolicyPayloadConversionTrait for SettingsChangePolicyCreationPayload {
+ type PolicyState = SettingsChangePolicy;
+
+ fn to_policy_state(self) -> Result {
+ let mut sorted_actions = self.actions.clone();
+ // Sort the actions to ensure the invariant function can apply
+ sorted_actions.sort_by_key(|action| match action {
+ AllowedSettingsChange::AddSigner { new_signer, .. } => (0, new_signer.clone()),
+ AllowedSettingsChange::RemoveSigner { old_signer } => (1, old_signer.clone()),
+ AllowedSettingsChange::ChangeThreshold => (2, None),
+ AllowedSettingsChange::ChangeTimeLock { .. } => (3, None),
+ });
+ Ok(SettingsChangePolicy {
+ actions: sorted_actions,
+ })
+ }
+}
+
+impl PolicySizeTrait for SettingsChangePolicyCreationPayload {
+ fn creation_payload_size(&self) -> usize {
+ 4 + self.actions.len() * AllowedSettingsChange::INIT_SPACE // actions vec
+ }
+
+ fn policy_state_size(&self) -> usize {
+ // Same as creation payload size
+ self.creation_payload_size()
+ }
+}
+
+// =============================================================================
+// POLICY TRAIT IMPLEMENTATION
+// =============================================================================
+impl PolicyTrait for SettingsChangePolicy {
+ type PolicyState = Self;
+ type CreationPayload = SettingsChangePolicyCreationPayload;
+ type UsagePayload = SettingsChangePayload;
+ type ExecutionArgs = SettingsChangeExecutionArgs;
+
+ /// Validate policy invariants - no duplicate actions
+ fn invariant(&self) -> Result<()> {
+ // Check for adjacent duplicates (assumes sorted actions by enum and pubkey)
+ // Rules:
+ // - AddSigner and RemoveSigner can only be present once with any given pubkey
+ // - ChangeThreshold and ChangeTimeLock can only each be present once
+ let has_duplicate = self.actions.windows(2).any(|win| match (&win[0], &win[1]) {
+ (
+ AllowedSettingsChange::AddSigner {
+ new_signer: signer1,
+ ..
+ },
+ AllowedSettingsChange::AddSigner {
+ new_signer: signer2,
+ ..
+ },
+ ) => signer1 == signer2,
+ (
+ AllowedSettingsChange::RemoveSigner {
+ old_signer: signer1,
+ },
+ AllowedSettingsChange::RemoveSigner {
+ old_signer: signer2,
+ },
+ ) => signer1 == signer2,
+ (AllowedSettingsChange::ChangeThreshold, AllowedSettingsChange::ChangeThreshold) => {
+ true
+ }
+ (
+ AllowedSettingsChange::ChangeTimeLock { .. },
+ AllowedSettingsChange::ChangeTimeLock { .. },
+ ) => true,
+ _ => false,
+ });
+
+ if has_duplicate {
+ return Err(SmartAccountError::SettingsChangePolicyInvariantDuplicateActions.into());
+ }
+ Ok(())
+ }
+
+ /// Validate that the payload actions match allowed policy actions
+ fn validate_payload(
+ &self,
+ // No difference between synchronous and asynchronous execution
+ _context: PolicyExecutionContext,
+ payload: &Self::UsagePayload,
+ ) -> Result<()> {
+ // Actions need to be non-zero
+ require!(
+ !payload.actions.is_empty(),
+ SmartAccountError::SettingsChangePolicyActionsMustBeNonZero
+ );
+ // Action indices must match actions length
+ require!(
+ payload.action_index.len() == payload.actions.len(),
+ SmartAccountError::SettingsChangePolicyInvariantActionIndicesActionsLengthMismatch
+ );
+
+ // This is safe because we checked that the action indices match the actions length
+ for (action_index, action) in payload.action_index.iter().zip(payload.actions.iter()) {
+ // Get the corresponding action from the policy state
+ let allowed_action = if let Some(action) = self.actions.get(*action_index as usize) {
+ action
+ } else {
+ return Err(
+ SmartAccountError::SettingsChangePolicyInvariantActionIndexOutOfBounds.into(),
+ );
+ };
+ match (allowed_action, action) {
+ (
+ AllowedSettingsChange::AddSigner {
+ new_signer: allowed_signer,
+ new_signer_permissions: allowed_permissions,
+ },
+ LimitedSettingsAction::AddSigner { new_signer },
+ ) => {
+ if let Some(allowed_signer) = allowed_signer {
+ // If None, any signer can be added
+ require!(
+ &new_signer.key == allowed_signer,
+ SmartAccountError::SettingsChangeAddSignerViolation
+ );
+ }
+ // If None, any permissions can be used
+ if let Some(allowed_permissions) = allowed_permissions {
+ require!(
+ &new_signer.permissions == allowed_permissions,
+ SmartAccountError::SettingsChangeAddSignerPermissionsViolation
+ );
+ }
+ }
+ (
+ AllowedSettingsChange::RemoveSigner {
+ old_signer: allowed_removal_signer,
+ },
+ LimitedSettingsAction::RemoveSigner { old_signer },
+ ) => {
+ // If None, any signer can be removed
+ if let Some(allowed_removal_signer) = allowed_removal_signer {
+ require!(
+ old_signer == allowed_removal_signer,
+ SmartAccountError::SettingsChangeRemoveSignerViolation
+ );
+ }
+ }
+ (
+ AllowedSettingsChange::ChangeThreshold,
+ LimitedSettingsAction::ChangeThreshold { new_threshold: _ },
+ ) => {}
+ (
+ AllowedSettingsChange::ChangeTimeLock {
+ new_time_lock: allowed_time_lock,
+ },
+ LimitedSettingsAction::SetTimeLock { new_time_lock },
+ ) => {
+ // If None, any time lock can be used
+ if let Some(allowed_time_lock) = allowed_time_lock {
+ require!(
+ new_time_lock == allowed_time_lock,
+ SmartAccountError::SettingsChangeChangeTimelockViolation
+ );
+ }
+ }
+ _ => {
+ return Err(SmartAccountError::SettingsChangeActionMismatch.into());
+ }
+ }
+ }
+ Ok(())
+ }
+
+ /// Execute the settings change actions
+ fn execute_payload<'info>(
+ &mut self,
+ args: Self::ExecutionArgs,
+ payload: &Self::UsagePayload,
+ accounts: &'info [AccountInfo<'info>],
+ ) -> Result<()> {
+ // Validate and grab the settings account
+ let mut validated_accounts = self.validate_accounts(args.settings_key, accounts)?;
+ for action in payload.actions.iter() {
+ let settings_action = SettingsAction::from(action.clone());
+ validated_accounts.settings.modify_with_action(
+ &args.settings_key,
+ &settings_action,
+ &Rent::get()?,
+ &validated_accounts.rent_payer,
+ &validated_accounts.system_program,
+ // Only policies and spending limits use remaining accounts, and
+ // those actions are excluded from LimitedSettingsAction
+ &[],
+ &crate::ID,
+ None,
+ )?;
+
+ // Run settings invariant
+ validated_accounts.settings.invariant()?;
+
+ let log_authority_info = LogAuthorityInfo {
+ authority: validated_accounts.settings.to_account_info().clone(),
+ authority_seeds: get_settings_signer_seeds(validated_accounts.settings.seed),
+ bump: validated_accounts.settings.bump,
+ program: validated_accounts.program.to_account_info(),
+ };
+
+ // Log the event since we're modifying the settings state
+ let event = SettingsChangePolicyEvent {
+ settings_pubkey: validated_accounts.settings.key(),
+ settings: validated_accounts.settings.clone().into_inner(),
+ changes: payload.actions.clone(),
+ };
+ SmartAccountEvent::SettingsChangePolicyEvent(event).log(&log_authority_info)?;
+ }
+ // Reallocate the settings account if needed
+ Settings::realloc_if_needed(
+ validated_accounts.settings.to_account_info(),
+ validated_accounts.settings.signers.len(),
+ validated_accounts
+ .rent_payer
+ .map(|rent_payer| rent_payer.to_account_info()),
+ validated_accounts
+ .system_program
+ .map(|system_program| system_program.to_account_info()),
+ )?;
+ Ok(())
+ }
+}
+
+// =============================================================================
+// ACCOUNT VALIDATION
+// =============================================================================
+
+impl SettingsChangePolicy {
+ /// Validate the accounts needed for settings change execution
+ pub fn validate_accounts<'info>(
+ &self,
+ settings_key: Pubkey,
+ accounts: &'info [AccountInfo<'info>],
+ ) -> Result> {
+ let (settings_account_info, rent_payer_info, system_program_info, program_info) = if let [settings_account_info, rent_payer_info, system_program_info, program_info, _remaining @ ..] =
+ accounts
+ {
+ (
+ settings_account_info,
+ rent_payer_info,
+ system_program_info,
+ program_info,
+ )
+ } else {
+ return err!(SmartAccountError::InvalidNumberOfAccounts);
+ };
+
+ // Settings account validation
+ require!(
+ settings_account_info.key() == settings_key,
+ SmartAccountError::SettingsChangeInvalidSettingsKey
+ );
+ require!(
+ settings_account_info.is_writable,
+ SmartAccountError::SettingsChangeInvalidSettingsAccount
+ );
+ let settings: Account<'info, Settings> = Account::try_from(settings_account_info)?;
+
+ // Settings authority validation
+ require!(
+ settings.settings_authority == Pubkey::default(),
+ SmartAccountError::NotSupportedForControlled
+ );
+
+ // Rent payer validation
+ let rent_payer = Signer::try_from(rent_payer_info)
+ .map_err(|_| SmartAccountError::SettingsChangeInvalidRentPayer)?;
+ require!(
+ rent_payer.is_writable,
+ SmartAccountError::SettingsChangeInvalidRentPayer
+ );
+
+ // System program validation
+ let system_program: Program<'info, System> = Program::try_from(system_program_info)
+ .map_err(|_| SmartAccountError::SettingsChangeInvalidSystemProgram)?;
+
+ // Program validation
+ let program: Program<'info, SquadsSmartAccountProgram> =
+ Program::try_from(program_info).map_err(|_| SmartAccountError::InvalidAccount)?;
+
+ Ok(ValidatedAccounts {
+ settings,
+ rent_payer: Some(rent_payer),
+ system_program: Some(system_program),
+ program,
+ })
+ }
+}
+
+// =============================================================================
+// TESTS
+// =============================================================================
+#[cfg(test)]
+mod tests {
+ use crate::Permission;
+
+ use super::*;
+
+ #[test]
+ fn test_invariant_valid_configuration() {
+ let payload = SettingsChangePolicyCreationPayload {
+ actions: vec![
+ AllowedSettingsChange::AddSigner {
+ new_signer: Some(Pubkey::new_unique()),
+ new_signer_permissions: None,
+ },
+ AllowedSettingsChange::RemoveSigner {
+ old_signer: Some(Pubkey::new_unique()),
+ },
+ AllowedSettingsChange::ChangeThreshold,
+ AllowedSettingsChange::ChangeTimeLock {
+ new_time_lock: Some(1800),
+ },
+ ],
+ };
+
+ let policy = payload.to_policy_state().unwrap();
+ assert!(policy.invariant().is_ok());
+ }
+ #[test]
+ fn test_invariant_duplicate_add_signer_same_pubkey() {
+ let duplicate_signer = Pubkey::new_unique();
+ let payload = SettingsChangePolicyCreationPayload {
+ actions: vec![
+ AllowedSettingsChange::AddSigner {
+ new_signer: Some(duplicate_signer),
+ new_signer_permissions: None,
+ },
+ AllowedSettingsChange::RemoveSigner {
+ old_signer: Some(duplicate_signer),
+ },
+ AllowedSettingsChange::AddSigner {
+ new_signer: Some(duplicate_signer),
+ new_signer_permissions: Some(Permissions::from_vec(&[Permission::Initiate])),
+ },
+ ],
+ };
+
+ let policy = payload.to_policy_state().unwrap();
+ assert!(policy.invariant().is_err());
+ }
+
+ #[test]
+ fn test_invariant_duplicate_remove_signer_same_pubkey_out_of_order() {
+ let duplicate_signer = Pubkey::new_unique();
+ let payload = SettingsChangePolicyCreationPayload {
+ actions: vec![
+ AllowedSettingsChange::RemoveSigner {
+ old_signer: Some(duplicate_signer),
+ },
+ AllowedSettingsChange::AddSigner {
+ new_signer: Some(duplicate_signer),
+ new_signer_permissions: None,
+ },
+ AllowedSettingsChange::RemoveSigner {
+ old_signer: Some(duplicate_signer),
+ },
+ ],
+ };
+
+ let policy = payload.to_policy_state().unwrap();
+ assert!(policy.invariant().is_err());
+ }
+
+ #[test]
+ fn test_invariant_duplicate_none_values_invalid() {
+ let payload = SettingsChangePolicyCreationPayload {
+ actions: vec![
+ AllowedSettingsChange::AddSigner {
+ new_signer: None,
+ new_signer_permissions: Some(Permissions::from_vec(&[Permission::Initiate])),
+ },
+ AllowedSettingsChange::AddSigner {
+ new_signer: None,
+ new_signer_permissions: Some(Permissions::from_vec(&[Permission::Execute])),
+ },
+ AllowedSettingsChange::RemoveSigner { old_signer: None },
+ AllowedSettingsChange::RemoveSigner { old_signer: None },
+ ],
+ };
+
+ let policy = payload.to_policy_state().unwrap();
+ assert!(policy.invariant().is_err());
+ }
+
+ #[test]
+ fn test_invariant_duplicate_change_time_lock() {
+ let payload = SettingsChangePolicyCreationPayload {
+ actions: vec![
+ AllowedSettingsChange::ChangeTimeLock {
+ new_time_lock: Some(1800),
+ },
+ AllowedSettingsChange::ChangeTimeLock {
+ new_time_lock: Some(3600),
+ },
+ ],
+ };
+
+ let policy = payload.to_policy_state().unwrap();
+ assert!(policy.invariant().is_err());
+ }
+
+ #[test]
+ fn test_creation_payload_size_calculation() {
+ let payload = SettingsChangePolicyCreationPayload {
+ actions: vec![
+ AllowedSettingsChange::AddSigner {
+ new_signer: Some(Pubkey::new_unique()),
+ new_signer_permissions: Some(Permissions::all()),
+ },
+ AllowedSettingsChange::RemoveSigner {
+ old_signer: Some(Pubkey::new_unique()),
+ },
+ AllowedSettingsChange::ChangeThreshold,
+ AllowedSettingsChange::ChangeTimeLock {
+ new_time_lock: Some(3600),
+ },
+ ],
+ };
+
+ let calculated_size = payload.creation_payload_size();
+ let actual_serialized = payload.try_to_vec().unwrap();
+ let actual_size = actual_serialized.len();
+
+ assert!(calculated_size >= actual_size);
+ }
+
+ #[test]
+ fn test_policy_state_size_calculation() {
+ let payload = SettingsChangePolicyCreationPayload {
+ actions: vec![
+ AllowedSettingsChange::AddSigner {
+ new_signer: Some(Pubkey::new_unique()),
+ new_signer_permissions: Some(Permissions::all()),
+ },
+ AllowedSettingsChange::RemoveSigner {
+ old_signer: Some(Pubkey::new_unique()),
+ },
+ AllowedSettingsChange::ChangeThreshold,
+ AllowedSettingsChange::ChangeTimeLock {
+ new_time_lock: Some(3600),
+ },
+ ],
+ };
+
+ let policy = payload.clone().to_policy_state().unwrap();
+ let calculated_size = payload.policy_state_size();
+ let actual_serialized = policy.try_to_vec().unwrap();
+ let actual_size = actual_serialized.len();
+
+ // Since InitSpace overestimates size, we only check that the calculated
+ // size is greater than or equal to the actual size to make sure
+ // serialization succeeds
+ assert!(calculated_size >= actual_size);
+ }
+}
diff --git a/programs/squads_smart_account_program/src/state/policies/implementations/spending_limit_policy.rs b/programs/squads_smart_account_program/src/state/policies/implementations/spending_limit_policy.rs
new file mode 100644
index 0000000..02f84cc
--- /dev/null
+++ b/programs/squads_smart_account_program/src/state/policies/implementations/spending_limit_policy.rs
@@ -0,0 +1,421 @@
+use anchor_lang::{prelude::*, system_program, Ids};
+use anchor_spl::token_interface::{self, TokenAccount, TokenInterface, TransferChecked};
+
+use crate::{
+ errors::*,
+ get_smart_account_seeds,
+ state::policies::utils::{QuantityConstraints, SpendingLimitV2, TimeConstraints, UsageState},
+ PolicyExecutionContext, PolicyPayloadConversionTrait, PolicySizeTrait, PolicyTrait,
+ SEED_PREFIX, SEED_SMART_ACCOUNT,
+};
+
+/// == SpendingLimitPolicy ==
+/// This policy allows for the transfer of SOL and SPL tokens between
+/// a source account and a set of destination accounts.
+///
+/// The policy is defined by a spending limit configuration and a source account index.
+/// The spending limit configuration includes a mint, time constraints, quantity constraints,
+/// and usage state.
+///===============================================
+
+// =============================================================================
+// CORE POLICY STRUCTURES
+// =============================================================================
+
+/// Main spending limit policy structure
+#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, PartialEq, Eq)]
+pub struct SpendingLimitPolicy {
+ /// The source account index
+ pub source_account_index: u8,
+ /// The destination addresses the spending limit is allowed to send funds to
+ /// If empty, funds can be sent to any address
+ pub destinations: Vec,
+ /// Spending limit configuration (timing, constraints, usage, mint)
+ pub spending_limit: SpendingLimitV2,
+}
+
+// =============================================================================
+// CREATION PAYLOAD TYPES
+// =============================================================================
+
+/// Setup parameters for creating a spending limit
+#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, PartialEq, Eq)]
+pub struct SpendingLimitPolicyCreationPayload {
+ pub mint: Pubkey,
+ pub source_account_index: u8,
+ pub time_constraints: TimeConstraints,
+ pub quantity_constraints: QuantityConstraints,
+ /// Optionally this can be submitted to update a spending limit policy
+ /// Cannot be Some() if accumulate_unused is true, to avoid invariant behavior
+ pub usage_state: Option,
+ pub destinations: Vec,
+}
+
+// =============================================================================
+// EXECUTION PAYLOAD TYPES
+// =============================================================================
+
+/// Payload for using a spending limit policy
+#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, PartialEq, Eq)]
+pub struct SpendingLimitPayload {
+ pub amount: u64,
+ pub destination: Pubkey,
+ pub decimals: u8,
+}
+
+pub struct SpendingLimitExecutionArgs {
+ pub settings_key: Pubkey,
+}
+
+/// Validated account information for different transfer types
+enum ValidatedAccounts<'info> {
+ NativeTransfer {
+ source_account_info: &'info AccountInfo<'info>,
+ source_account_bump: u8,
+ destination_account_info: &'info AccountInfo<'info>,
+ system_program: &'info AccountInfo<'info>,
+ },
+ TokenTransfer {
+ source_account_info: &'info AccountInfo<'info>,
+ source_account_bump: u8,
+ source_token_account_info: &'info AccountInfo<'info>,
+ destination_token_account_info: &'info AccountInfo<'info>,
+ mint: &'info AccountInfo<'info>,
+ token_program: &'info AccountInfo<'info>,
+ },
+}
+
+// =============================================================================
+// PAYLOAD CONVERSION IMPLEMENTATIONS
+// =============================================================================
+
+impl PolicyPayloadConversionTrait for SpendingLimitPolicyCreationPayload {
+ type PolicyState = SpendingLimitPolicy;
+
+ /// Convert creation payload to policy state
+ /// Used by Settings.modify_with_action() to instantiate policy state
+ fn to_policy_state(self) -> Result {
+ let now = Clock::get().unwrap().unix_timestamp;
+ // Sort the destinations
+ let mut destinations = self.destinations;
+ destinations.sort_by_key(|d| d.to_bytes());
+
+ // Modify time constraints to start at the current timestamp if set to 0
+ let mut modified_time_constraints = self.time_constraints;
+ if self.time_constraints.start == 0 {
+ modified_time_constraints.start = now;
+ }
+
+ // Determine usage state based on surrounding constraints
+ let usage_state = if let Some(usage_state) = self.usage_state {
+ // This is the only invariant that needs to be checked on the arg level
+ require!(
+ !self.time_constraints.accumulate_unused,
+ SmartAccountError::SpendingLimitPolicyInvariantAccumulateUnused
+ );
+ usage_state
+ } else {
+ UsageState {
+ remaining_in_period: self.quantity_constraints.max_per_period,
+ last_reset: modified_time_constraints.start,
+ }
+ };
+
+ Ok(SpendingLimitPolicy {
+ spending_limit: SpendingLimitV2 {
+ mint: self.mint,
+ time_constraints: modified_time_constraints,
+ quantity_constraints: self.quantity_constraints,
+ usage: usage_state,
+ },
+ source_account_index: self.source_account_index,
+ destinations,
+ })
+ }
+}
+
+impl PolicySizeTrait for SpendingLimitPolicyCreationPayload {
+ fn creation_payload_size(&self) -> usize {
+ 32 + // mint
+ 1 + // source_account_index
+ TimeConstraints::INIT_SPACE + // time_constraints
+ QuantityConstraints::INIT_SPACE + // quantity_constraints
+ 4 + self.destinations.len() * 32 // destinations vec
+ }
+
+ fn policy_state_size(&self) -> usize {
+ 32 + // mint (in SpendingLimitV2)
+ TimeConstraints::INIT_SPACE + // time_constraints (in SpendingLimitV2)
+ QuantityConstraints::INIT_SPACE + // quantity_constraints (in SpendingLimitV2)
+ UsageState::INIT_SPACE + // usage (in SpendingLimitV2)
+ 1 + // source_account_index
+ 4 + self.destinations.len() * 32 // destinations vec
+ }
+}
+
+// =============================================================================
+// POLICY TRAIT IMPLEMENTATION
+// =============================================================================
+
+impl PolicyTrait for SpendingLimitPolicy {
+ type PolicyState = Self;
+ type CreationPayload = SpendingLimitPolicyCreationPayload;
+ type UsagePayload = SpendingLimitPayload;
+ type ExecutionArgs = SpendingLimitExecutionArgs;
+
+ /// Validate policy invariants - no duplicate destinations and valid spending limit
+ fn invariant(&self) -> Result<()> {
+ // Check that the destinations are not duplicated (assumes sorted destinations)
+ let has_duplicates = self.destinations.windows(2).any(|w| w[0] == w[1]);
+ require!(
+ !has_duplicates,
+ SmartAccountError::SpendingLimitPolicyInvariantDuplicateDestinations
+ );
+
+ // Check the spending limit invariant
+ self.spending_limit.invariant()?;
+ Ok(())
+ }
+
+ /// Validate that the destination is allowed
+ fn validate_payload(
+ &self,
+ // No difference between synchronous and asynchronous execution
+ _context: PolicyExecutionContext,
+ payload: &Self::UsagePayload,
+ ) -> Result<()> {
+ // If destinations are set, check that the destination is in the list of allowed destinations
+ if !self.destinations.is_empty() {
+ require!(
+ self.destinations.contains(&payload.destination),
+ SmartAccountError::InvalidDestination
+ );
+ }
+ Ok(())
+ }
+
+ /// Execute the spending limit transfer
+ fn execute_payload<'info>(
+ &mut self,
+ args: Self::ExecutionArgs,
+ payload: &Self::UsagePayload,
+ accounts: &'info [AccountInfo<'info>],
+ ) -> Result<()> {
+ let current_timestamp = Clock::get()?.unix_timestamp;
+
+ // Check that the spending limit is active
+ self.spending_limit.is_active(current_timestamp)?;
+
+ // Reset the period & amount
+ self.spending_limit.reset_if_needed(current_timestamp);
+
+ // Check that the amount complies with the spending limit
+ self.spending_limit.check_amount(payload.amount)?;
+
+ // Validate the accounts
+ let validated_accounts = self.validate_accounts(&args.settings_key, &payload, accounts)?;
+
+ // Execute the payload
+ match validated_accounts {
+ ValidatedAccounts::NativeTransfer {
+ source_account_info,
+ source_account_bump,
+ destination_account_info,
+ system_program,
+ } => {
+ // Transfer SOL
+ anchor_lang::system_program::transfer(
+ CpiContext::new_with_signer(
+ system_program.to_account_info(),
+ anchor_lang::system_program::Transfer {
+ from: source_account_info.clone(),
+ to: destination_account_info.clone(),
+ },
+ &[&[
+ SEED_PREFIX,
+ args.settings_key.as_ref(),
+ SEED_SMART_ACCOUNT,
+ &self.source_account_index.to_le_bytes(),
+ &[source_account_bump],
+ ]],
+ ),
+ payload.amount,
+ )?
+ }
+ ValidatedAccounts::TokenTransfer {
+ source_account_info,
+ source_account_bump,
+ source_token_account_info,
+ destination_token_account_info,
+ mint,
+ token_program,
+ } => {
+ // Transfer SPL token
+ token_interface::transfer_checked(
+ CpiContext::new_with_signer(
+ token_program.to_account_info(),
+ TransferChecked {
+ from: source_token_account_info.to_account_info(),
+ mint: mint.to_account_info(),
+ to: destination_token_account_info.to_account_info(),
+ authority: source_account_info.clone(),
+ },
+ &[&[
+ SEED_PREFIX,
+ args.settings_key.as_ref(),
+ SEED_SMART_ACCOUNT,
+ &self.source_account_index.to_le_bytes(),
+ &[source_account_bump],
+ ]],
+ ),
+ payload.amount,
+ payload.decimals,
+ )?;
+ }
+ }
+
+ // Decrement the amount
+ self.spending_limit.decrement(payload.amount);
+
+ // Invariant check
+ self.invariant()?;
+
+ Ok(())
+ }
+}
+
+// =============================================================================
+// ACCOUNT VALIDATION
+// =============================================================================
+
+impl SpendingLimitPolicy {
+ /// Validate the accounts needed for transfer execution
+ fn validate_accounts<'info>(
+ &self,
+ settings_key: &Pubkey,
+ args: &SpendingLimitPayload,
+ accounts: &'info [AccountInfo<'info>],
+ ) -> Result> {
+ // Derive source account key
+ let source_account_index_bytes = self.source_account_index.to_le_bytes();
+ let source_account_seeds =
+ get_smart_account_seeds(settings_key, &source_account_index_bytes);
+
+ // Derive source and destination account keys
+ let (source_account_key, source_account_bump) =
+ Pubkey::find_program_address(source_account_seeds.as_slice(), &crate::ID);
+
+ // Mint specific logic
+ match self.spending_limit.mint {
+ // Native SOL transfer
+ mint if mint == Pubkey::default() => {
+ // Parse out the accounts
+ let (source_account_info, destination_account_info, system_program) = if let [source_account_info, destination_account_info, system_program, _remaining @ ..] =
+ accounts
+ {
+ (
+ source_account_info,
+ destination_account_info,
+ system_program,
+ )
+ } else {
+ return err!(SmartAccountError::InvalidNumberOfAccounts);
+ };
+ // Check that the source account is the same as the source account info
+ require!(
+ source_account_key == source_account_info.key(),
+ SmartAccountError::InvalidAccount
+ );
+ // Check that the destination account is the same as the destination account info
+ require!(
+ args.destination == destination_account_info.key(),
+ SmartAccountError::InvalidAccount
+ );
+
+ // Check that the source account is not the same as the destination account
+ require!(
+ source_account_info.key() != destination_account_info.key(),
+ SmartAccountError::InvalidAccount
+ );
+
+ // Check the system program
+ require!(
+ system_program.key() == system_program::ID,
+ SmartAccountError::InvalidAccount
+ );
+
+ // Sanity check for the decimals. Similar to the one in token_interface::transfer_checked.
+ require!(args.decimals == 9, SmartAccountError::DecimalsMismatch);
+
+ Ok(ValidatedAccounts::NativeTransfer {
+ source_account_info,
+ source_account_bump,
+ destination_account_info,
+ system_program,
+ })
+ }
+ // Token transfer
+ _ => {
+ // Parse out the accounts
+ let (
+ source_account_info,
+ source_token_account_info,
+ destination_token_account_info,
+ mint,
+ token_program,
+ ) = if let [source_account_info, source_token_account_info, destination_token_account_info, mint, token_program, _remaining @ ..] =
+ accounts
+ {
+ (
+ source_account_info,
+ source_token_account_info,
+ destination_token_account_info,
+ mint,
+ token_program,
+ )
+ } else {
+ return err!(SmartAccountError::InvalidNumberOfAccounts);
+ };
+
+ // Check the source account key
+ require!(
+ source_account_key == source_account_info.key(),
+ SmartAccountError::InvalidAccount
+ );
+
+ // Deserialize the source and destination token accounts. Either
+ // T22 or TokenKeg accounts
+ let source_token_account =
+ InterfaceAccount::<'info, TokenAccount>::try_from(source_token_account_info)?;
+ let destination_token_account =
+ InterfaceAccount::::try_from(destination_token_account_info)?;
+
+ // Check the mint against the policy state
+ require_eq!(self.spending_limit.mint, mint.key());
+
+ // Assert the ownership and mint of the token accounts
+ require!(
+ source_token_account.owner == source_account_key
+ && source_token_account.mint == self.spending_limit.mint,
+ SmartAccountError::InvalidAccount
+ );
+ require!(
+ destination_token_account.owner == args.destination
+ && destination_token_account.mint == self.spending_limit.mint,
+ SmartAccountError::InvalidAccount
+ );
+ // Check the token program
+ require_eq!(TokenInterface::ids().contains(&token_program.key()), true);
+
+ Ok(ValidatedAccounts::TokenTransfer {
+ source_account_info,
+ source_account_bump,
+ source_token_account_info,
+ destination_token_account_info,
+ mint,
+ token_program,
+ })
+ }
+ }
+ }
+}
diff --git a/programs/squads_smart_account_program/src/state/policies/mod.rs b/programs/squads_smart_account_program/src/state/policies/mod.rs
new file mode 100644
index 0000000..d2ef554
--- /dev/null
+++ b/programs/squads_smart_account_program/src/state/policies/mod.rs
@@ -0,0 +1,7 @@
+pub mod implementations;
+pub mod policy_core;
+mod utils;
+
+pub use policy_core::*;
+
+pub use implementations::*;
diff --git a/programs/squads_smart_account_program/src/state/policies/policy_core/mod.rs b/programs/squads_smart_account_program/src/state/policies/policy_core/mod.rs
new file mode 100644
index 0000000..6188cfb
--- /dev/null
+++ b/programs/squads_smart_account_program/src/state/policies/policy_core/mod.rs
@@ -0,0 +1,15 @@
+//! Core policy framework
+//!
+//! This module contains the fundamental policy framework including:
+//! - Policy struct and PolicyType enum
+//! - PolicyExecutor trait for type-safe execution
+//! - PolicyPayload enum for unified payloads
+//! - Core consensus integration
+
+pub mod payloads;
+pub mod policy;
+pub mod traits;
+
+pub use payloads::*;
+pub use policy::*;
+pub use traits::*;
diff --git a/programs/squads_smart_account_program/src/state/policies/policy_core/payloads.rs b/programs/squads_smart_account_program/src/state/policies/policy_core/payloads.rs
new file mode 100644
index 0000000..74289c9
--- /dev/null
+++ b/programs/squads_smart_account_program/src/state/policies/policy_core/payloads.rs
@@ -0,0 +1,45 @@
+use anchor_lang::prelude::*;
+
+use crate::{
+ state::policies::implementations::InternalFundTransferPayload,
+ InternalFundTransferPolicyCreationPayload, ProgramInteractionPayload,
+ ProgramInteractionPolicyCreationPayload, ProgramInteractionPolicyCreationPayloadLegacy,
+ SettingsChangePayload, SettingsChangePolicyCreationPayload, SpendingLimitPayload,
+ SpendingLimitPolicyCreationPayload,
+};
+
+use super::PolicySizeTrait;
+
+/// Unified enum for all policy creation payloads
+/// These are used in SettingsAction::PolicyCreate to specify which type of policy to create
+#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
+pub enum PolicyCreationPayload {
+ InternalFundTransfer(InternalFundTransferPolicyCreationPayload),
+ SpendingLimit(SpendingLimitPolicyCreationPayload),
+ SettingsChange(SettingsChangePolicyCreationPayload),
+ LegacyProgramInteraction(ProgramInteractionPolicyCreationPayloadLegacy),
+ ProgramInteraction(ProgramInteractionPolicyCreationPayload),
+}
+
+impl PolicyCreationPayload {
+ /// Calculate the size of the resulting policy data after creation
+ pub fn policy_state_size(&self) -> usize {
+ // 1 for the Wrapper enum type
+ 1 + match self {
+ PolicyCreationPayload::InternalFundTransfer(payload) => payload.policy_state_size(),
+ PolicyCreationPayload::SpendingLimit(payload) => payload.policy_state_size(),
+ PolicyCreationPayload::SettingsChange(payload) => payload.policy_state_size(),
+ PolicyCreationPayload::LegacyProgramInteraction(payload) => payload.policy_state_size(),
+ PolicyCreationPayload::ProgramInteraction(payload) => payload.policy_state_size(),
+ }
+ }
+}
+
+/// Unified enum for all policy execution payloads
+#[derive(AnchorSerialize, AnchorDeserialize, Clone)]
+pub enum PolicyPayload {
+ InternalFundTransfer(InternalFundTransferPayload),
+ ProgramInteraction(ProgramInteractionPayload),
+ SpendingLimit(SpendingLimitPayload),
+ SettingsChange(SettingsChangePayload),
+}
diff --git a/programs/squads_smart_account_program/src/state/policies/policy_core/policy.rs b/programs/squads_smart_account_program/src/state/policies/policy_core/policy.rs
new file mode 100644
index 0000000..43cc0b8
--- /dev/null
+++ b/programs/squads_smart_account_program/src/state/policies/policy_core/policy.rs
@@ -0,0 +1,431 @@
+use anchor_lang::prelude::*;
+
+use super::{payloads::PolicyPayload, traits::PolicyTrait, PolicyExecutionContext};
+use crate::state::policies::implementations::InternalFundTransferPolicy;
+use crate::MAX_TIME_LOCK;
+use crate::{
+ errors::*,
+ interface::consensus_trait::{Consensus, ConsensusAccountType},
+ InternalFundTransferExecutionArgs, ProgramInteractionExecutionArgs,
+ ProgramInteractionPolicy, Proposal, Settings, SettingsChangeExecutionArgs,
+ SettingsChangePolicy, SmartAccountSigner, SpendingLimitExecutionArgs, SpendingLimitPolicy,
+ Transaction, SEED_POLICY, SEED_PREFIX,
+};
+
+#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, PartialEq, Eq, InitSpace)]
+pub enum PolicyExpiration {
+ /// Policy expires at a specific timestamp
+ Timestamp(i64),
+ /// Policy expires when the core settings hash mismatches the stored hash.
+ SettingsState([u8; 32]),
+}
+
+#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, PartialEq, Eq)]
+pub enum PolicyExpirationArgs {
+ /// Policy expires at a specific timestamp
+ Timestamp(i64),
+ /// Policy expires when the core settings hash mismatches the stored hash.
+ SettingsState,
+}
+
+#[account]
+pub struct Policy {
+ /// The smart account this policy belongs to.
+ pub settings: Pubkey,
+
+ /// The seed of the policy.
+ pub seed: u64,
+
+ /// Bump for the policy.
+ pub bump: u8,
+
+ /// Transaction index for stale transaction protection.
+ pub transaction_index: u64,
+
+ /// Stale transaction index boundary.
+ pub stale_transaction_index: u64,
+
+ /// Signers attached to the policy with their permissions.
+ pub signers: Vec,
+
+ /// Threshold for approvals.
+ pub threshold: u16,
+
+ /// How many seconds must pass between approval and execution.
+ pub time_lock: u32,
+
+ /// The state of the policy.
+ pub policy_state: PolicyState,
+
+ /// Timestamp when the policy becomes active.
+ pub start: i64,
+
+ /// Policy expiration - either time-based or state-based.
+ pub expiration: Option,
+
+ /// Rent Collector for the policy for when it gets closed
+ pub rent_collector: Pubkey
+}
+
+impl Policy {
+ pub fn size(signers_length: usize, policy_data_length: usize) -> usize {
+ 8 + // anchor discriminator
+ 32 + // settings
+ 8 + // seed
+ 1 + // bump
+ 8 + // transaction_index
+ 8 + // stale_transaction_index
+ 4 + // signers vector length
+ signers_length * SmartAccountSigner::INIT_SPACE + // signers
+ 2 + // threshold
+ 4 + // time_lock
+ 1 + policy_data_length + // discriminator + policy_data_length
+ 8 + // start_timestamp
+ 1 + PolicyExpiration::INIT_SPACE + // expiration (discriminator + max data size)
+ 32 // rent_collector
+ }
+
+ /// Check if the policy account space needs to be reallocated.
+ pub fn realloc_if_needed<'a>(
+ policy: AccountInfo<'a>,
+ signers_length: usize,
+ policy_data_length: usize,
+ rent_payer: Option>,
+ system_program: Option>,
+ ) -> Result {
+ let current_account_size = policy.data.borrow().len();
+ let required_size = Policy::size(signers_length, policy_data_length);
+
+ if current_account_size >= required_size {
+ return Ok(false);
+ }
+
+ crate::utils::realloc(&policy, required_size, rent_payer, system_program)?;
+ Ok(true)
+ }
+
+ pub fn invariant(&self) -> Result<()> {
+ // Max number of signers is u16::MAX.
+ require!(
+ self.signers.len() <= usize::from(u16::MAX),
+ SmartAccountError::TooManySigners
+ );
+
+ // There must be no duplicate signers.
+ let has_duplicates = self.signers.windows(2).any(|win| win[0].key == win[1].key);
+ require!(!has_duplicates, SmartAccountError::DuplicateSigner);
+
+ // Signers must not have unknown permissions.
+ require!(
+ self.signers.iter().all(|s| s.permissions.mask < 8),
+ SmartAccountError::UnknownPermission
+ );
+
+ // There must be at least one signer with Initiate permission.
+ require!(self.num_proposers() > 0, SmartAccountError::NoProposers);
+
+ // There must be at least one signer with Execute permission.
+ require!(self.num_executors() > 0, SmartAccountError::NoExecutors);
+
+ // There must be at least one signer with Vote permission.
+ require!(self.num_voters() > 0, SmartAccountError::NoVoters);
+
+ // Threshold must be greater than 0.
+ require!(self.threshold > 0, SmartAccountError::InvalidThreshold);
+
+ // Threshold must not exceed the number of voters.
+ require!(
+ usize::from(self.threshold) <= self.num_voters(),
+ SmartAccountError::InvalidThreshold
+ );
+
+ // Stale transaction index must be <= transaction index.
+ require!(
+ self.stale_transaction_index <= self.transaction_index,
+ SmartAccountError::InvalidStaleTransactionIndex
+ );
+
+ // If policy has expiration, it must be valid
+ if let Some(expiration) = &self.expiration {
+ match expiration {
+ PolicyExpiration::Timestamp(timestamp) => {
+ require!(
+ *timestamp > self.start,
+ SmartAccountError::PolicyInvariantInvalidExpiration
+ );
+ }
+ _ => {}
+ }
+ }
+
+ // Time Lock must not exceed the maximum allowed to prevent bricking the policy.
+ require!(
+ self.time_lock <= MAX_TIME_LOCK,
+ SmartAccountError::TimeLockExceedsMaxAllowed
+ );
+ // Policy state must be valid
+ self.policy_state.invariant()?;
+
+ Ok(())
+ }
+
+ /// Create policy state safely
+ pub fn create_state(
+ settings: Pubkey,
+ seed: u64,
+ bump: u8,
+ signers: &Vec,
+ threshold: u16,
+ time_lock: u32,
+ policy_state: PolicyState,
+ start: i64,
+ expiration: Option,
+ rent_collector: Pubkey,
+ ) -> Result {
+ let mut sorted_signers = signers.clone();
+ sorted_signers.sort_by_key(|s| s.key);
+
+ Ok(Policy {
+ settings,
+ seed,
+ bump,
+ transaction_index: 0,
+ stale_transaction_index: 0,
+ signers: sorted_signers,
+ threshold,
+ time_lock,
+ policy_state,
+ start,
+ expiration,
+ rent_collector,
+ })
+ }
+
+ /// Update policy state safely. Disallows
+ pub fn update_state(
+ &mut self,
+ signers: &Vec,
+ threshold: u16,
+ time_lock: u32,
+ policy_state: PolicyState,
+ expiration: Option,
+ ) -> Result<()> {
+ let mut sorted_signers = signers.clone();
+ sorted_signers.sort_by_key(|s| s.key);
+
+ self.signers = sorted_signers;
+ self.threshold = threshold;
+ self.time_lock = time_lock;
+ self.policy_state = policy_state;
+ self.expiration = expiration;
+ Ok(())
+ }
+}
+
+#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug)]
+pub enum PolicyState {
+ /// Internal fund transfer policy.
+ InternalFundTransfer(InternalFundTransferPolicy),
+ /// Spending limit policy
+ SpendingLimit(SpendingLimitPolicy),
+ /// Settings change policy
+ SettingsChange(SettingsChangePolicy),
+ /// Program interaction policy
+ ProgramInteraction(ProgramInteractionPolicy),
+}
+
+impl PolicyState {
+ pub fn invariant(&self) -> Result<()> {
+ match self {
+ PolicyState::InternalFundTransfer(policy) => policy.invariant(),
+ PolicyState::SpendingLimit(policy) => policy.invariant(),
+ PolicyState::SettingsChange(policy) => policy.invariant(),
+ PolicyState::ProgramInteraction(policy) => policy.invariant(),
+ }
+ }
+}
+
+impl Policy {
+ /// Validate the payload against the policy.
+ pub fn validate_payload(
+ &self,
+ context: PolicyExecutionContext,
+ payload: &PolicyPayload,
+ ) -> Result<()> {
+ match (&self.policy_state, payload) {
+ (
+ PolicyState::InternalFundTransfer(policy),
+ PolicyPayload::InternalFundTransfer(payload),
+ ) => policy.validate_payload(context, payload),
+ (PolicyState::SpendingLimit(policy), PolicyPayload::SpendingLimit(payload)) => {
+ policy.validate_payload(context, payload)
+ }
+ (PolicyState::SettingsChange(policy), PolicyPayload::SettingsChange(payload)) => {
+ policy.validate_payload(context, payload)
+ }
+ (
+ PolicyState::ProgramInteraction(policy),
+ PolicyPayload::ProgramInteraction(payload),
+ ) => policy.validate_payload(context, payload),
+ _ => err!(SmartAccountError::InvalidPolicyPayload),
+ }
+ }
+ /// Dispatch method for policy execution
+ pub fn execute<'info>(
+ &mut self,
+ transaction_account: Option<&Account<'info, Transaction>>,
+ proposal_account: Option<&Account<'info, Proposal>>,
+ payload: &PolicyPayload,
+ accounts: &'info [AccountInfo<'info>],
+ ) -> Result<()> {
+ match (&mut self.policy_state, payload) {
+ (
+ PolicyState::InternalFundTransfer(ref mut policy_state),
+ PolicyPayload::InternalFundTransfer(payload),
+ ) => {
+ let args = InternalFundTransferExecutionArgs {
+ settings_key: self.settings,
+ };
+ policy_state.execute_payload(args, payload, accounts)
+ }
+ (
+ PolicyState::SpendingLimit(ref mut policy_state),
+ PolicyPayload::SpendingLimit(payload),
+ ) => {
+ let args = SpendingLimitExecutionArgs {
+ settings_key: self.settings,
+ };
+ policy_state.execute_payload(args, payload, accounts)
+ }
+ (
+ PolicyState::ProgramInteraction(ref mut policy_state),
+ PolicyPayload::ProgramInteraction(payload),
+ ) => {
+ let args = ProgramInteractionExecutionArgs {
+ settings_key: self.settings,
+ // if the transaction account is not provided, use a default
+ // pubkey (sync transactions)
+ transaction_key: transaction_account
+ .map(|t| t.key())
+ .unwrap_or(Pubkey::default()),
+ // if the proposal account is not provided, use a default
+ // pubkey (sync transactions)
+ proposal_key: proposal_account
+ .map(|p| p.key())
+ .unwrap_or(Pubkey::default()),
+ policy_signers: self.signers.clone(),
+ };
+ policy_state.execute_payload(args, payload, accounts)
+ }
+ (
+ PolicyState::SettingsChange(ref mut policy_state),
+ PolicyPayload::SettingsChange(payload),
+ ) => {
+ let args = SettingsChangeExecutionArgs {
+ settings_key: self.settings,
+ };
+ policy_state.execute_payload(args, payload, accounts)
+ }
+ _ => err!(SmartAccountError::InvalidPolicyPayload),
+ }
+ }
+}
+// Implement Consensus for Policy
+impl Consensus for Policy {
+ /// Checks if a given policy is active based on it's start and expiration
+ fn is_active(&self, accounts: &[AccountInfo]) -> Result<()> {
+ // Get the current timestamp
+ let current_timestamp = Clock::get()?.unix_timestamp;
+ // Check if the policy has started
+ require!(
+ current_timestamp >= self.start,
+ SmartAccountError::PolicyNotActiveYet
+ );
+ // Check if the policy is expired
+ match self.expiration {
+ Some(PolicyExpiration::Timestamp(expiration_timestamp)) => {
+ // Get current timestamp
+ let current_timestamp = Clock::get()?.unix_timestamp;
+ require!(
+ current_timestamp < expiration_timestamp,
+ SmartAccountError::PolicyExpirationViolationTimestampExpired
+ );
+ Ok(())
+ }
+ Some(PolicyExpiration::SettingsState(stored_hash)) => {
+ // Find the settings account in the accounts list
+ let settings_account_info = &accounts
+ .get(0)
+ .ok_or(SmartAccountError::PolicyExpirationViolationSettingsAccountNotPresent)?;
+ require!(
+ settings_account_info.key() == self.settings,
+ SmartAccountError::PolicyExpirationViolationPolicySettingsKeyMismatch
+ );
+ // Deserialize the settings account
+ let account_data = settings_account_info.try_borrow_data()?;
+ let settings = Settings::try_deserialize(&mut &**account_data)?;
+
+ // Generate the current core state hash
+ let current_hash = settings.generate_core_state_hash()?;
+
+ require!(
+ current_hash == stored_hash,
+ SmartAccountError::PolicyExpirationViolationHashExpired
+ );
+ Ok(())
+ }
+ // If the policy has no expiration, it is always active
+ None => Ok(()),
+ }
+ }
+ fn account_type(&self) -> ConsensusAccountType {
+ ConsensusAccountType::Policy
+ }
+
+ fn check_derivation(&self, key: Pubkey) -> Result<()> {
+ // TODO: Since policies can be closed, we need to make the derivation deterministic.
+ let (address, _bump) = Pubkey::find_program_address(
+ &[
+ SEED_PREFIX,
+ SEED_POLICY,
+ self.settings.as_ref(),
+ self.seed.to_le_bytes().as_ref(),
+ ],
+ &crate::ID,
+ );
+ require_keys_eq!(address, key, SmartAccountError::InvalidAccount);
+ Ok(())
+ }
+ fn signers(&self) -> &[SmartAccountSigner] {
+ &self.signers
+ }
+
+ fn threshold(&self) -> u16 {
+ self.threshold
+ }
+
+ fn time_lock(&self) -> u32 {
+ self.time_lock
+ }
+
+ fn transaction_index(&self) -> u64 {
+ self.transaction_index
+ }
+
+ fn set_transaction_index(&mut self, transaction_index: u64) -> Result<()> {
+ self.transaction_index = transaction_index;
+ Ok(())
+ }
+
+ fn stale_transaction_index(&self) -> u64 {
+ self.stale_transaction_index
+ }
+
+ fn invalidate_prior_transactions(&mut self) {
+ self.stale_transaction_index = self.transaction_index;
+ }
+
+ fn invariant(&self) -> Result<()> {
+ self.invariant()
+ }
+}
diff --git a/programs/squads_smart_account_program/src/state/policies/policy_core/traits.rs b/programs/squads_smart_account_program/src/state/policies/policy_core/traits.rs
new file mode 100644
index 0000000..eafdfaa
--- /dev/null
+++ b/programs/squads_smart_account_program/src/state/policies/policy_core/traits.rs
@@ -0,0 +1,59 @@
+use anchor_lang::prelude::*;
+
+/// Trait for policy creation payloads that can be converted to policy state
+pub trait PolicyPayloadConversionTrait {
+ type PolicyState;
+
+ /// Convert the creation payload to the actual policy state
+ fn to_policy_state(self) -> Result;
+}
+
+/// Trait for calculating Borsh serialization sizes of policy-related structs
+pub trait PolicySizeTrait {
+ /// Calculate the size when this payload is Borsh serialized
+ fn creation_payload_size(&self) -> usize;
+
+ /// Calculate the size of the resulting policy state when Borsh serialized
+ fn policy_state_size(&self) -> usize;
+}
+
+/// The context in which the policy is being executed
+pub enum PolicyExecutionContext {
+ /// The policy is being executed synchronously
+ Synchronous,
+ /// The policy is being executed asynchronously
+ Asynchronous,
+}
+/// Core trait for policy execution - implemented by specific policy types
+pub trait PolicyTrait {
+ /// The policy state
+ type PolicyState;
+
+ /// The creation payload
+ type CreationPayload: PolicyPayloadConversionTrait
+ + PolicySizeTrait;
+
+ /// The payload type used when executing this policy
+ type UsagePayload;
+
+ /// Additional arguments needed for policy execution
+ type ExecutionArgs;
+
+ /// Validate the policy state
+ fn invariant(&self) -> Result<()>;
+
+ /// Validate the payload against policy constraints before execution
+ fn validate_payload(
+ &self,
+ context: PolicyExecutionContext,
+ payload: &Self::UsagePayload,
+ ) -> Result<()>;
+
+ /// Execute the policy action with the validated payload
+ fn execute_payload<'info>(
+ &mut self,
+ args: Self::ExecutionArgs,
+ payload: &Self::UsagePayload,
+ accounts: &'info [AccountInfo<'info>],
+ ) -> Result<()>;
+}
diff --git a/programs/squads_smart_account_program/src/state/policies/utils/account_tracking.rs b/programs/squads_smart_account_program/src/state/policies/utils/account_tracking.rs
new file mode 100644
index 0000000..34a717f
--- /dev/null
+++ b/programs/squads_smart_account_program/src/state/policies/utils/account_tracking.rs
@@ -0,0 +1,195 @@
+use anchor_lang::{prelude::*, Ids};
+use anchor_spl::token_interface::{TokenAccount, TokenInterface};
+
+use crate::{
+ errors::SmartAccountError, state::policies::utils::spending_limit_v2::SpendingLimitV2,
+};
+
+pub struct TrackedTokenAccount<'info> {
+ pub account: &'info AccountInfo<'info>,
+ pub balance: u64,
+ pub delegate: Option<(Pubkey, u64)>,
+ pub authority: Pubkey,
+}
+
+pub struct TrackedExecutingAccount<'info> {
+ pub account: &'info AccountInfo<'info>,
+ pub lamports: u64,
+}
+
+pub struct Balances<'info> {
+ pub executing_account: TrackedExecutingAccount<'info>,
+ pub token_accounts: Vec>,
+}
+
+/// Pre-check the balances of the executing account and any token accounts that are owned by it
+pub fn check_pre_balances<'info>(
+ executing_account: Pubkey,
+ accounts: &'info [AccountInfo<'info>],
+) -> Balances<'info> {
+ let mut tracked_token_accounts = Vec::with_capacity(accounts.len());
+
+ // Get the executing account info
+ let executing_account_info = accounts
+ .iter()
+ .find(|account| account.key() == executing_account)
+ .unwrap();
+
+ // Track the executing account
+ let tracked_executing_account = TrackedExecutingAccount {
+ account: executing_account_info,
+ lamports: executing_account_info.lamports(),
+ };
+
+ // Iterate over all accounts and track any given token accounts that are
+ // owned by the executing account
+ let token_program_ids = TokenInterface::ids();
+ for account in accounts {
+ // Only track accounts owned by a token program and that are writable
+ if token_program_ids.contains(&account.owner) && account.is_writable {
+ // This may fail for accounts that are not token accounts, so skip if it does
+ let Ok(token_account) = InterfaceAccount::::try_from(account) else {
+ continue;
+ };
+ // Only track token accounts that are owned by the executing account
+ if token_account.owner == executing_account {
+ let balance = token_account.amount;
+ let delegate = if let Some(delegate_key) = Option::from(token_account.delegate) {
+ Some((delegate_key, token_account.delegated_amount))
+ } else {
+ None
+ };
+ let authority = token_account.owner;
+
+ // Add the token account to the tracked token accounts
+ tracked_token_accounts.push(TrackedTokenAccount {
+ account,
+ balance,
+ delegate,
+ authority,
+ });
+ }
+ }
+ }
+
+ Balances {
+ executing_account: tracked_executing_account,
+ token_accounts: tracked_token_accounts,
+ }
+}
+
+impl<'info> Balances<'info> {
+ /// Evaluate balance changes against the spending limits
+ pub fn evaluate_balance_changes(
+ &self,
+ spending_limits: &mut Vec,
+ ) -> Result<()> {
+ // Get the current timestamp
+ let current_timestamp = Clock::get()?.unix_timestamp;
+
+ // Check the executing accounts lamports
+ let current_lamports = self.executing_account.account.lamports();
+
+ // Check the SOL spending limit
+ // Note: Assumes spending limits have been deduplicated, and no two spending limits can have the same mint
+ if let Some(spending_limit) = spending_limits.iter_mut().find(|spending_limit| {
+ spending_limit.mint() == Pubkey::default()
+ && spending_limit.is_active(current_timestamp).is_ok()
+ }) {
+ // Ensure the executing account doesn't have less lamports than the allowed change
+ let minimum_balance = self
+ .executing_account
+ .lamports
+ .saturating_sub(spending_limit.remaining_in_period());
+ require_gte!(
+ current_lamports,
+ minimum_balance,
+ SmartAccountError::ProgramInteractionInsufficientLamportAllowance
+ );
+ // If the executing account has a lower balance than before, decrement the spending limit
+ if current_lamports < self.executing_account.lamports {
+ spending_limit.decrement(self.executing_account.lamports - current_lamports);
+ }
+ } else {
+ // Ensure the executing account doesn't have less lamports than before
+ require_gte!(
+ current_lamports,
+ self.executing_account.lamports,
+ SmartAccountError::ProgramInteractionModifiedIllegalBalance
+ );
+ }
+
+ // Check all of the token accounts
+ for tracked_token_account in &self.token_accounts {
+ // Ensure that any tracked token account is not closed
+ if tracked_token_account.account.data_is_empty() {
+ return Err(
+ SmartAccountError::ProgramInteractionIllegalTokenAccountModification.into(),
+ );
+ }
+ // Re-deserialize the token account
+ let post_token_account =
+ InterfaceAccount::::try_from(tracked_token_account.account).unwrap();
+
+ // Find the spending limit for the token account if it exists and is active
+ if let Some(spending_limit) = spending_limits.iter_mut().find(|spending_limit| {
+ spending_limit.mint() == post_token_account.mint
+ && spending_limit.is_active(current_timestamp).is_ok()
+ }) {
+ {
+ // Saturating subtraction since remaining_amount could be
+ // higher than the balance
+ let minimum_balance = tracked_token_account
+ .balance
+ .saturating_sub(spending_limit.remaining_in_period());
+
+ // Ensure the token account has no greater difference than the allowed change
+ require_gte!(
+ post_token_account.amount,
+ minimum_balance,
+ SmartAccountError::ProgramInteractionInsufficientTokenAllowance
+ );
+
+ // If the token account has a lower balance than before, decrement the spending limit
+ if post_token_account.amount < tracked_token_account.balance {
+ spending_limit
+ .decrement(tracked_token_account.balance - post_token_account.amount);
+ }
+ }
+ } else {
+ // Ensure the token account has the exact or greater balance
+ // than before
+ require_gte!(
+ post_token_account.amount,
+ tracked_token_account.balance,
+ SmartAccountError::ProgramInteractionModifiedIllegalBalance
+ );
+ }
+
+ // Ensure the delegate, and authority have not changed. Delegated
+ // amount may decrease
+ let post_delegate: Option<(Pubkey, u64)> =
+ if let Some(delegate_key) = Option::from(post_token_account.delegate) {
+ Some((delegate_key, post_token_account.delegated_amount))
+ } else {
+ None
+ };
+ match (post_delegate, tracked_token_account.delegate) {
+ (Some(post_delegate), Some(tracked_delegate)) => {
+ require_eq!(post_delegate.0, tracked_delegate.0);
+ require_gte!(post_delegate.1, tracked_delegate.1);
+ }
+ (None, None) => {}
+ _ => {
+ return Err(SmartAccountError::ProgramInteractionIllegalTokenAccountModification.into());
+ }
+ };
+ require_eq!(
+ post_token_account.owner,
+ tracked_token_account.authority,
+ SmartAccountError::ProgramInteractionIllegalTokenAccountModification
+ );
+ }
+ Ok(())
+ }
+}
diff --git a/programs/squads_smart_account_program/src/state/policies/utils/mod.rs b/programs/squads_smart_account_program/src/state/policies/utils/mod.rs
new file mode 100644
index 0000000..b4988ee
--- /dev/null
+++ b/programs/squads_smart_account_program/src/state/policies/utils/mod.rs
@@ -0,0 +1,5 @@
+pub mod account_tracking;
+pub mod spending_limit_v2;
+
+pub use account_tracking::*;
+pub use spending_limit_v2::*;
\ No newline at end of file
diff --git a/programs/squads_smart_account_program/src/state/policies/utils/spending_limit_v2.rs b/programs/squads_smart_account_program/src/state/policies/utils/spending_limit_v2.rs
new file mode 100644
index 0000000..aeca0bb
--- /dev/null
+++ b/programs/squads_smart_account_program/src/state/policies/utils/spending_limit_v2.rs
@@ -0,0 +1,409 @@
+use anchor_lang::prelude::*;
+
+use crate::errors::SmartAccountError;
+
+#[derive(AnchorSerialize, AnchorDeserialize, Clone, Copy, Debug, PartialEq, Eq, InitSpace)]
+pub enum PeriodV2 {
+ /// The spending limit can only be used once
+ OneTime,
+ /// The spending limit is reset every day
+ Daily,
+ /// The spending limit is reset every week (7 days)
+ Weekly,
+ /// The spending limit is reset every month (30 days)
+ Monthly,
+ /// Custom period in seconds
+ Custom(i64),
+}
+
+impl PeriodV2 {
+ pub fn to_seconds(&self) -> Option {
+ match self {
+ PeriodV2::OneTime => None,
+ PeriodV2::Daily => Some(24 * 60 * 60),
+ PeriodV2::Weekly => Some(7 * 24 * 60 * 60),
+ PeriodV2::Monthly => Some(30 * 24 * 60 * 60),
+ PeriodV2::Custom(seconds) => Some(*seconds),
+ }
+ }
+}
+
+/// Configuration for time-based constraints
+#[derive(AnchorSerialize, AnchorDeserialize, Clone, Copy, Debug, PartialEq, Eq, InitSpace)]
+pub struct TimeConstraints {
+ /// Optional start timestamp (0 means immediate)
+ pub start: i64,
+ /// Optional expiration timestamp
+ pub expiration: Option,
+ /// Reset period for the spending limit
+ pub period: PeriodV2,
+ /// Whether unused allowances accumulate across periods
+ pub accumulate_unused: bool,
+}
+
+/// Quantity constraints for spending limits
+#[derive(AnchorSerialize, AnchorDeserialize, Clone, Copy, Debug, PartialEq, Eq, InitSpace)]
+pub struct QuantityConstraints {
+ /// Maximum quantity per period
+ pub max_per_period: u64,
+ /// Maximum quantity per individual use (0 means no per-use limit)
+ pub max_per_use: u64,
+ /// Whether to enforce exact quantity matching on max per use.
+ pub enforce_exact_quantity: bool,
+}
+
+/// Usage tracking for resource consumption
+#[derive(AnchorSerialize, AnchorDeserialize, Clone, Copy, Debug, PartialEq, Eq, InitSpace)]
+pub struct UsageState {
+ /// Remaining quantity in current period
+ pub remaining_in_period: u64,
+ /// Unix timestamp of last reset
+ pub last_reset: i64,
+}
+
+/// Shared spending limit structure that combines timing, quantity, usage, and mint
+#[derive(AnchorSerialize, AnchorDeserialize, Clone, Debug, PartialEq, Eq, InitSpace)]
+pub struct SpendingLimitV2 {
+ /// The token mint the spending limit is for.
+ /// Pubkey::default() means SOL.
+ /// use NATIVE_MINT for Wrapped SOL.
+ pub mint: Pubkey,
+
+ /// Timing configuration
+ pub time_constraints: TimeConstraints,
+
+ /// Amount constraints
+ pub quantity_constraints: QuantityConstraints,
+
+ /// Current usage tracking
+ pub usage: UsageState,
+}
+
+impl SpendingLimitV2 {
+ /// Check if the spending limit is currently active
+ pub fn is_active(&self, current_timestamp: i64) -> Result<()> {
+ // Check start time
+ if current_timestamp < self.time_constraints.start {
+ return err!(SmartAccountError::SpendingLimitNotActive);
+ }
+
+ // Check expiration
+ if let Some(expiration) = self.time_constraints.expiration {
+ if current_timestamp > expiration {
+ return err!(SmartAccountError::SpendingLimitExpired);
+ }
+ }
+
+ Ok(())
+ }
+
+ // Returns the mint of the spending limit
+ pub fn mint(&self) -> Pubkey {
+ self.mint
+ }
+
+ // Returns the remaining amount in the period for the spending limit
+ pub fn remaining_in_period(&self) -> u64 {
+ self.usage.remaining_in_period
+ }
+
+ /// Check that the amount is less than the remaining amount, and if it complies with the quantity constraints
+ pub fn check_amount(&self, amount: u64) -> Result<()> {
+ // Remaining amount constraint
+ if amount > self.usage.remaining_in_period {
+ return err!(SmartAccountError::SpendingLimitInsufficientRemainingAmount);
+ }
+ // Max per use constraint
+ if self.quantity_constraints.max_per_use > 0 {
+ require!(
+ amount <= self.quantity_constraints.max_per_use,
+ SmartAccountError::SpendingLimitViolatesMaxPerUseConstraint
+ );
+ }
+ // Exact amount constraint
+ if self.quantity_constraints.enforce_exact_quantity {
+ // Exact max per use constraint
+ require_eq!(
+ amount,
+ self.quantity_constraints.max_per_use,
+ SmartAccountError::SpendingLimitViolatesExactQuantityConstraint
+ );
+ }
+
+ Ok(())
+ }
+
+ pub fn decrement(&mut self, amount: u64) {
+ self.usage.remaining_in_period =
+ self.usage.remaining_in_period.checked_sub(amount).unwrap();
+ }
+
+ /// Reset amounts if period boundary has been crossed
+ pub fn reset_if_needed(&mut self, current_timestamp: i64) {
+ // Reset logic for spending limits
+ if let Some(reset_period) = self.time_constraints.period.to_seconds() {
+ // Check that the spending limit is active
+ if self.is_active(current_timestamp).is_err() {
+ return;
+ }
+
+ let passed_since_last_reset = current_timestamp
+ .checked_sub(self.usage.last_reset)
+ .unwrap();
+
+ if passed_since_last_reset > reset_period {
+ let periods_passed = passed_since_last_reset.checked_div(reset_period).unwrap();
+
+ // Update last_reset: last_reset = last_reset + periods_passed * reset_period
+ self.usage.last_reset = self
+ .usage
+ .last_reset
+ .checked_add(periods_passed.checked_mul(reset_period).unwrap())
+ .unwrap();
+
+ if self.time_constraints.accumulate_unused {
+ // For overflow: add missed periods to current amount
+ // (overflow is only enabled with expiration, so we know it exists)
+ let additional_amount = self
+ .quantity_constraints
+ .max_per_period
+ .saturating_mul(periods_passed as u64);
+ self.usage.remaining_in_period = self
+ .usage
+ .remaining_in_period
+ .saturating_add(additional_amount);
+ } else {
+ // For non-overflow: reset to full period amount (original behavior)
+ self.usage.remaining_in_period = self.quantity_constraints.max_per_period;
+ }
+ }
+ }
+ }
+
+ pub fn invariant(&self) -> Result<()> {
+ // Amount per period must be non-zero
+ require_neq!(
+ self.quantity_constraints.max_per_period,
+ 0,
+ SmartAccountError::SpendingLimitInvariantMaxPerPeriodZero
+ );
+
+ // If start time is set, it must be positive
+ require!(
+ self.time_constraints.start >= 0,
+ SmartAccountError::SpendingLimitInvariantStartTimePositive
+ );
+
+ // If expiration is set, it must be positive
+ if self.time_constraints.expiration.is_some() {
+ // Since start is positive, expiration must be greater than start,
+ // we can skip the check for expiration being positive.
+ require!(
+ self.time_constraints.expiration.unwrap() > self.time_constraints.start,
+ SmartAccountError::SpendingLimitInvariantExpirationSmallerThanStart
+ );
+ }
+
+ // If overflow is enabled, must have expiration. This is to prevent
+ // footguns
+ if self.time_constraints.accumulate_unused {
+ // OneTime period cannot have overflow enabled
+ require!(
+ self.time_constraints.period != PeriodV2::OneTime,
+ SmartAccountError::SpendingLimitInvariantOneTimePeriodCannotHaveOverflowEnabled
+ );
+ require!(
+ self.time_constraints.expiration.is_some(),
+ SmartAccountError::SpendingLimitInvariantOverflowEnabledMustHaveExpiration
+ );
+ // Remaining amount must always be less than expiration - start /
+ // period + 1 * max per period
+ let total_time =
+ self.time_constraints.expiration.unwrap() - self.time_constraints.start;
+ let total_periods = total_time
+ .checked_div(self.time_constraints.period.to_seconds().unwrap())
+ .unwrap() as u64;
+ // Total amount based on number of periods within start & expiration
+ let max_amount = match total_time % self.time_constraints.period.to_seconds().unwrap() {
+ // Start & Expiration are divisible by period, so we can use the
+ // total number of periods to calculate the max amount.
+ 0 => total_periods
+ .checked_mul(self.quantity_constraints.max_per_period)
+ .unwrap(),
+ // Start & Expiration are divisible by period with a remainder, so we need to
+ // add an extra period to the total number of periods.
+ _ => (total_periods.checked_add(1).unwrap())
+ .checked_mul(self.quantity_constraints.max_per_period)
+ .unwrap(),
+ };
+ // Remaining amount must always be less than max amount
+ require!(
+ self.usage.remaining_in_period <= max_amount,
+ SmartAccountError::SpendingLimitInvariantOverflowRemainingAmountGreaterThanMaxAmount
+ );
+ } else {
+ // If overflow is disabled, remaining in period must be less than or equal to max per period
+ require!(
+ self.usage.remaining_in_period <= self.quantity_constraints.max_per_period,
+ SmartAccountError::SpendingLimitInvariantRemainingAmountGreaterThanMaxPerPeriod
+ );
+ }
+
+ // If exact amount is enforced, per-use amount must be set and non-zero
+ if self.quantity_constraints.enforce_exact_quantity {
+ require!(
+ self.quantity_constraints.max_per_use > 0,
+ SmartAccountError::SpendingLimitInvariantExactQuantityMaxPerUseZero
+ );
+ }
+
+ // If per-use amount is set, it cannot exceed per-period amount
+ if self.quantity_constraints.max_per_use > 0 {
+ require!(
+ self.quantity_constraints.max_per_use <= self.quantity_constraints.max_per_period,
+ SmartAccountError::SpendingLimitInvariantMaxPerUseGreaterThanMaxPerPeriod
+ );
+ }
+
+ // Custom period must have positive duration
+ if let PeriodV2::Custom(seconds) = self.time_constraints.period {
+ require!(
+ seconds > 0,
+ SmartAccountError::SpendingLimitInvariantCustomPeriodNegative
+ );
+ }
+
+ // Last reset must be between start and expiration
+ if let Some(expiration) = self.time_constraints.expiration {
+ require!(
+ self.usage.last_reset >= self.time_constraints.start
+ && self.usage.last_reset <= expiration,
+ SmartAccountError::SpendingLimitInvariantLastResetOutOfBounds
+ );
+ } else {
+ require!(
+ self.usage.last_reset >= self.time_constraints.start,
+ SmartAccountError::SpendingLimitInvariantLastResetSmallerThanStart
+ );
+ }
+
+ Ok(())
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use solana_program::pubkey::Pubkey;
+
+ fn make_time_constraints(
+ period: PeriodV2,
+ accumulate_unused: bool,
+ start: i64,
+ expiration: Option,
+ ) -> TimeConstraints {
+ TimeConstraints {
+ start,
+ expiration,
+ period,
+ accumulate_unused,
+ }
+ }
+
+ fn make_quantity_constraints(
+ max_per_period: u64,
+ max_per_use: u64,
+ enforce_exact_quantity: bool,
+ ) -> QuantityConstraints {
+ QuantityConstraints {
+ max_per_period,
+ max_per_use,
+ enforce_exact_quantity,
+ }
+ }
+
+ fn make_usage_state(remaining: u64, last_reset: i64) -> UsageState {
+ UsageState {
+ remaining_in_period: remaining,
+ last_reset,
+ }
+ }
+
+ #[test]
+ fn test_reset_amount_non_accumulate_unused() {
+ // 2.5 days in seconds
+ let now = 216_000;
+ let one_and_a_half_days_ago = now - 129_600;
+ let mut policy = SpendingLimitV2 {
+ mint: Pubkey::default(),
+ time_constraints: make_time_constraints(PeriodV2::Daily, false, 0, None),
+ quantity_constraints: make_quantity_constraints(100, 0, false),
+ usage: make_usage_state(50, one_and_a_half_days_ago), // last reset was 1 day ago
+ };
+ // Should reset to max_per_period
+ policy.reset_if_needed(now);
+ assert_eq!(policy.usage.remaining_in_period, 100);
+ }
+
+ #[test]
+ fn test_reset_amount_accumulate_unused() {
+ // 2.5 days in seconds
+ let now = 216_000;
+ let one_and_a_half_days_ago = now - 129_600;
+ let mut policy = SpendingLimitV2 {
+ mint: Pubkey::default(),
+ time_constraints: make_time_constraints(PeriodV2::Daily, true, 0, None),
+ quantity_constraints: make_quantity_constraints(100, 0, false),
+ usage: make_usage_state(50, one_and_a_half_days_ago), // last reset was 1.5 days ago
+ };
+ // Should reset to max_per_period
+ policy.reset_if_needed(now);
+ assert_eq!(policy.usage.remaining_in_period, 150);
+ }
+
+ #[test]
+ fn test_reset_amount_accumulate_unused_2() {
+ // 2.5 days in seconds
+ let now = 216_000;
+ let mut policy = SpendingLimitV2 {
+ mint: Pubkey::default(),
+ time_constraints: make_time_constraints(PeriodV2::Daily, true, 0, None),
+ quantity_constraints: make_quantity_constraints(100, 0, false),
+ usage: make_usage_state(50, 0), // last reset was 1.5 days ago
+ };
+ // Should reset to max_per_period
+ policy.reset_if_needed(now);
+ assert_eq!(policy.usage.remaining_in_period, 250);
+ }
+
+ #[test]
+ fn test_decrement_amount() {
+ let mut policy = SpendingLimitV2 {
+ mint: Pubkey::default(),
+ time_constraints: make_time_constraints(PeriodV2::Daily, false, 1_000_000, None),
+ quantity_constraints: make_quantity_constraints(100, 0, false),
+ usage: make_usage_state(100, 1_000_000),
+ };
+ policy.decrement(30);
+ assert_eq!(policy.usage.remaining_in_period, 70);
+ }
+
+ #[test]
+ fn test_is_active() {
+ let now = 1_000_000;
+ let policy = SpendingLimitV2 {
+ mint: Pubkey::default(),
+ time_constraints: make_time_constraints(
+ PeriodV2::Daily,
+ false,
+ now - 10,
+ Some(now + 100),
+ ),
+ quantity_constraints: make_quantity_constraints(100, 0, false),
+ usage: make_usage_state(100, now - 10),
+ };
+ assert!(policy.is_active(now).is_ok());
+ assert!(policy.is_active(now - 100_000).is_err()); // before start
+ assert!(policy.is_active(now + 200_000).is_err()); // after expiration
+ }
+}
diff --git a/programs/squads_smart_account_program/src/state/proposal.rs b/programs/squads_smart_account_program/src/state/proposal.rs
index 8c3cee7..69fbe90 100644
--- a/programs/squads_smart_account_program/src/state/proposal.rs
+++ b/programs/squads_smart_account_program/src/state/proposal.rs
@@ -1,19 +1,22 @@
#![allow(deprecated)]
use anchor_lang::prelude::*;
+use crate::consensus_trait::ConsensusAccountType;
use crate::errors::*;
use crate::id;
use crate::utils;
use crate::utils::realloc;
-
-use anchor_lang::system_program;
+use crate::LogAuthorityInfo;
+use crate::ProposalEvent;
+use crate::ProposalEventType;
+use crate::SmartAccountEvent;
/// Stores the data required for tracking the status of a smart account proposal.
/// Each `Proposal` has a 1:1 association with a transaction account, e.g. a `Transaction` or a `SettingsTransaction`;
/// the latter can be executed only after the `Proposal` has been approved and its time lock is released.
#[account]
pub struct Proposal {
- /// The settings this belongs to.
+ /// The consensus account (settings or policy) this belongs to.
pub settings: Pubkey,
/// Index of the smart account transaction this proposal is associated with.
pub transaction_index: u64,
@@ -165,16 +168,30 @@ impl Proposal {
proposal_account: Option,
proposal_info: AccountInfo<'info>,
proposal_rent_collector: AccountInfo<'info>,
+ log_authority_info: &LogAuthorityInfo<'info>,
+ consensus_account_type: ConsensusAccountType,
) -> Result<()> {
if let Some(proposal) = proposal_account {
require!(
proposal_rent_collector.key() == proposal.rent_collector,
SmartAccountError::InvalidRentCollector
);
+ let proposal_key = proposal_info.key();
utils::close(
proposal_info,
proposal_rent_collector,
)?;
+ let event = ProposalEvent {
+ event_type: ProposalEventType::Close,
+ consensus_account: log_authority_info.authority.key(),
+ consensus_account_type,
+ proposal_pubkey: proposal_key,
+ transaction_index: proposal.transaction_index,
+ signer: None,
+ memo: None,
+ proposal: None,
+ };
+ SmartAccountEvent::ProposalEvent(event).log(&log_authority_info)?;
}
Ok(())
}
diff --git a/programs/squads_smart_account_program/src/state/seeds.rs b/programs/squads_smart_account_program/src/state/seeds.rs
index ba265b7..e53e8f4 100644
--- a/programs/squads_smart_account_program/src/state/seeds.rs
+++ b/programs/squads_smart_account_program/src/state/seeds.rs
@@ -1,6 +1,4 @@
-use anchor_lang::AnchorSerialize;
-
-use super::Settings;
+use anchor_lang::prelude::Pubkey;
pub const SEED_PREFIX: &[u8] = b"smart_account";
pub const SEED_PROGRAM_CONFIG: &[u8] = b"program_config";
@@ -12,6 +10,31 @@ pub const SEED_SMART_ACCOUNT: &[u8] = b"smart_account";
pub const SEED_EPHEMERAL_SIGNER: &[u8] = b"ephemeral_signer";
pub const SEED_SPENDING_LIMIT: &[u8] = b"spending_limit";
pub const SEED_TRANSACTION_BUFFER: &[u8] = b"transaction_buffer";
+pub const SEED_POLICY: &[u8] = b"policy";
+
+#[cfg(not(feature = "testing"))]
+// Seed is slightly different, to allow for off curve key without bump
+pub const SEED_HOOK_AUTHORITY: &[u8] = b"hook_authority_seeds";
+
+#[cfg(feature = "testing")]
+// Seed is slightly different, to allow for off curve key without bump, with the
+// testng program id
+pub const SEED_HOOK_AUTHORITY: &[u8] = b"hook_authority_seeds_test";
+
+// Hook authority 2MTRji19YQupkpha1Rki8xvoMtEQUfMn9FB1m93DaHj8. Off curve
+// without bump
+#[cfg(not(feature = "testing"))]
+pub const HOOK_AUTHORITY_PUBKEY: Pubkey = Pubkey::new_from_array([
+ 20, 25, 47, 2, 155, 124, 59, 36, 196, 168, 29, 160, 133, 182, 125, 32, 178, 251, 180, 88, 79,
+ 213, 209, 149, 172, 177, 71, 224, 215, 197, 110, 243,
+]);
+
+#[cfg(feature = "testing")]
+// Hook authority 3DBe6CrgCNQ3ydaTRX8j3WenQRdQxkhj87Hqe2MAwwHx
+pub const HOOK_AUTHORITY_PUBKEY: Pubkey = Pubkey::new_from_array([
+ 32, 214, 95, 168, 10, 233, 119, 125, 45, 249, 95, 236, 95, 70, 192, 202, 150, 140, 8, 162, 126,
+ 245, 141, 215, 164, 36, 97, 134, 2, 197, 62, 175,
+]);
pub fn get_settings_signer_seeds(settings_seed: u128) -> Vec> {
vec![
@@ -20,3 +43,51 @@ pub fn get_settings_signer_seeds(settings_seed: u128) -> Vec> {
settings_seed.to_le_bytes().to_vec(),
]
}
+
+pub fn get_policy_signer_seeds(settings_key: &Pubkey, policy_seed: u64) -> Vec> {
+ vec![
+ SEED_PREFIX.to_vec(),
+ SEED_POLICY.to_vec(),
+ settings_key.as_ref().to_vec(),
+ policy_seed.to_le_bytes().to_vec(),
+ ]
+}
+
+/// Derives the account seeds for a given smart account based on the settings key and account index
+pub fn get_smart_account_seeds<'a>(
+ settings_key: &'a Pubkey,
+ account_index_bytes: &'a [u8],
+) -> [&'a [u8]; 4] {
+ [
+ SEED_PREFIX,
+ settings_key.as_ref(),
+ SEED_SMART_ACCOUNT,
+ account_index_bytes,
+ ]
+}
+
+#[cfg(test)]
+mod tests {
+ use std::str::FromStr;
+
+ use anchor_lang::AnchorSerialize;
+
+ use super::*;
+
+ #[test]
+ fn test_hook_authority_pubkey() {
+ let address = Pubkey::create_program_address(&[SEED_HOOK_AUTHORITY], &crate::ID).unwrap();
+ assert_eq!(address, HOOK_AUTHORITY_PUBKEY);
+ }
+
+ #[test]
+ fn test_testing_hook_authority_pubkey() {
+ let test_program_id =
+ Pubkey::from_str("GyhGAqjokLwF9UXdQ2dR5Zwiup242j4mX4J1tSMKyAmD").unwrap();
+ let address =
+ Pubkey::create_program_address(&[SEED_HOOK_AUTHORITY], &test_program_id)
+ .unwrap();
+ println!("address: {:?}", address.try_to_vec());
+ assert_eq!(address, HOOK_AUTHORITY_PUBKEY);
+ }
+}
diff --git a/programs/squads_smart_account_program/src/state/settings.rs b/programs/squads_smart_account_program/src/state/settings.rs
index 9b4e761..ed10fd8 100644
--- a/programs/squads_smart_account_program/src/state/settings.rs
+++ b/programs/squads_smart_account_program/src/state/settings.rs
@@ -1,7 +1,21 @@
use anchor_lang::prelude::*;
use anchor_lang::system_program;
-
-use crate::{errors::*, id, state::*, utils::*, SettingsAction};
+use solana_program::hash::hash;
+
+use crate::AddSpendingLimitEvent;
+use crate::LogAuthorityInfo;
+use crate::PolicyEvent;
+use crate::PolicyEventType;
+use crate::RemoveSpendingLimitEvent;
+use crate::SmartAccountEvent;
+use crate::{
+ errors::*,
+ id,
+ interface::consensus_trait::{Consensus, ConsensusAccountType},
+ state::*,
+ utils::*,
+ SettingsAction,
+};
pub const MAX_TIME_LOCK: u32 = 3 * 30 * 24 * 60 * 60; // 3 months
#[account]
@@ -47,12 +61,33 @@ pub struct Settings {
pub signers: Vec,
/// Counter for how many sub accounts are in use (improves off-chain indexing)
pub account_utilization: u8,
+ /// Seed used for deterministic policy creation.
+ pub policy_seed: Option,
// Reserved for future use
- pub _reserved1: u8,
pub _reserved2: u8,
}
impl Settings {
+ /// Generates a hash of the core settings: Signers, threshold, and time_lock
+ pub fn generate_core_state_hash(&self) -> Result<[u8; 32]> {
+ let mut data_to_hash = Vec::new();
+
+ // Signers
+ for signer in &self.signers {
+ data_to_hash.extend_from_slice(signer.key.as_ref());
+ // Add signer permissions (1 byte)
+ data_to_hash.push(signer.permissions.mask);
+ }
+ // Threshold
+ data_to_hash.extend_from_slice(&self.threshold.to_le_bytes());
+
+ // Timelock
+ data_to_hash.extend_from_slice(&self.time_lock.to_le_bytes());
+
+ let hash_result = hash(&data_to_hash);
+
+ Ok(hash_result.to_bytes())
+ }
pub fn find_and_initialize_settings_account<'info>(
&self,
settings_account_key: Pubkey,
@@ -114,31 +149,10 @@ impl Settings {
4 + // signers vector length
signers_length * SmartAccountSigner::INIT_SPACE + // signers
1 + // sub_account_utilization
- 1 + // _reserved_1
+ 1 + 8 + // policy_seed
1 // _reserved_2
}
- pub fn num_voters(signers: &[SmartAccountSigner]) -> usize {
- signers
- .iter()
- .filter(|m| m.permissions.has(Permission::Vote))
- .count()
- }
-
- pub fn num_proposers(signers: &[SmartAccountSigner]) -> usize {
- signers
- .iter()
- .filter(|m| m.permissions.has(Permission::Initiate))
- .count()
- }
-
- pub fn num_executors(signers: &[SmartAccountSigner]) -> usize {
- signers
- .iter()
- .filter(|m| m.permissions.has(Permission::Execute))
- .count()
- }
-
/// Check if the settings account space needs to be reallocated to accommodate `signers_length`.
/// Returns `true` if the account was reallocated.
pub fn realloc_if_needed<'a>(
@@ -197,15 +211,15 @@ impl Settings {
);
// There must be at least one signer with Initiate permission.
- let num_proposers = Self::num_proposers(signers);
+ let num_proposers = Self::num_proposers(&self);
require!(num_proposers > 0, SmartAccountError::NoProposers);
// There must be at least one signer with Execute permission.
- let num_executors = Self::num_executors(signers);
+ let num_executors = Self::num_executors(&self);
require!(num_executors > 0, SmartAccountError::NoExecutors);
// There must be at least one signer with Vote permission.
- let num_voters = Self::num_voters(signers);
+ let num_voters = Self::num_voters(&self);
require!(num_voters > 0, SmartAccountError::NoVoters);
// Threshold must be greater than 0.
@@ -232,38 +246,6 @@ impl Settings {
Ok(())
}
- /// Makes the transactions created up until this moment stale.
- /// Should be called whenever any settings parameter related to the voting consensus is changed.
- pub fn invalidate_prior_transactions(&mut self) {
- self.stale_transaction_index = self.transaction_index;
- }
-
- /// Returns `Some(index)` if `signer_pubkey` is a signer, with `index` into the `signers` vec.
- /// `None` otherwise.
- pub fn is_signer(&self, signer_pubkey: Pubkey) -> Option {
- self.signers
- .binary_search_by_key(&signer_pubkey, |m| m.key)
- .ok()
- }
-
- pub fn signer_has_permission(&self, signer_pubkey: Pubkey, permission: Permission) -> bool {
- match self.is_signer(signer_pubkey) {
- Some(index) => self.signers[index].permissions.has(permission),
- _ => false,
- }
- }
-
- /// How many "reject" votes are enough to make the transaction "Rejected".
- /// The cutoff must be such that it is impossible for the remaining voters to reach the approval threshold.
- /// For example: total voters = 7, threshold = 3, cutoff = 5.
- pub fn cutoff(&self) -> usize {
- Self::num_voters(&self.signers)
- .checked_sub(usize::from(self.threshold))
- .unwrap()
- .checked_add(1)
- .unwrap()
- }
-
/// Add `new_signer` to the settings `signers` vec and sort the vec.
pub fn add_signer(&mut self, new_signer: SmartAccountSigner) {
self.signers.push(new_signer);
@@ -294,6 +276,7 @@ impl Settings {
system_program: &Option>,
remaining_accounts: &'info [AccountInfo<'info>],
program_id: &Pubkey,
+ log_authority_info: Option<&LogAuthorityInfo<'info>>,
) -> Result<()> {
match action {
SettingsAction::AddSigner { new_signer } => {
@@ -385,6 +368,16 @@ impl Settings {
spending_limit.invariant()?;
spending_limit
.try_serialize(&mut &mut spending_limit_info.data.borrow_mut()[..])?;
+
+ // Log the event
+ let event = AddSpendingLimitEvent {
+ settings_pubkey: self_key.to_owned(),
+ spending_limit_pubkey: spending_limit_key,
+ spending_limit: spending_limit.clone(),
+ };
+ if let Some(log_authority_info) = log_authority_info {
+ SmartAccountEvent::AddSpendingLimitEvent(event).log(&log_authority_info)?;
+ }
}
SettingsAction::RemoveSpendingLimit {
@@ -408,14 +401,323 @@ impl Settings {
);
spending_limit.close(rent_payer.to_account_info())?;
+
+ // Log the closing event
+ let event = RemoveSpendingLimitEvent {
+ settings_pubkey: self_key.to_owned(),
+ spending_limit_pubkey: *spending_limit_key,
+ };
+ if let Some(log_authority_info) = log_authority_info {
+ SmartAccountEvent::RemoveSpendingLimitEvent(event).log(&log_authority_info)?;
+ }
}
SettingsAction::SetArchivalAuthority {
- new_archival_authority,
+ new_archival_authority: _,
} => {
// Marked as NotImplemented until archival feature is implemented.
return err!(SmartAccountError::NotImplemented);
}
+
+ SettingsAction::PolicyCreate {
+ seed,
+ policy_creation_payload,
+ signers,
+ threshold,
+ time_lock,
+ start_timestamp,
+ expiration_args,
+ } => {
+ // Increment the policy seed if it exists, otherwise set it to
+ // 1 (First policy is being created)
+ let next_policy_seed = if let Some(policy_seed) = self.policy_seed {
+ let next_policy_seed = policy_seed.checked_add(1).unwrap();
+
+ // Increment the policy seed
+ self.policy_seed = Some(next_policy_seed);
+ next_policy_seed
+ } else {
+ self.policy_seed = Some(1);
+ 1
+ };
+ // Policies get created at a deterministic address based on the
+ // seed in the settings.
+ let (policy_pubkey, policy_bump) = Pubkey::find_program_address(
+ &[
+ crate::SEED_PREFIX,
+ SEED_POLICY,
+ self_key.as_ref(),
+ &next_policy_seed.to_le_bytes(),
+ ],
+ program_id,
+ );
+
+ let policy_info = remaining_accounts
+ .iter()
+ .find(|acc| acc.key == &policy_pubkey)
+ .ok_or(SmartAccountError::MissingAccount)?;
+
+ // Calculate policy data size based on the creation payload
+ let policy_specific_data_size = policy_creation_payload.policy_state_size();
+
+ let policy_size = Policy::size(signers.len(), policy_specific_data_size);
+
+ let rent_payer = rent_payer
+ .as_ref()
+ .ok_or(SmartAccountError::MissingAccount)?;
+ let system_program = system_program
+ .as_ref()
+ .ok_or(SmartAccountError::MissingAccount)?;
+
+ // Create the policy account (following the pattern from create_spending_limit)
+ create_account(
+ &rent_payer.to_account_info(),
+ &policy_info,
+ &system_program.to_account_info(),
+ &id(),
+ rent,
+ policy_size,
+ vec![
+ crate::SEED_PREFIX.to_vec(),
+ SEED_POLICY.to_vec(),
+ self_key.as_ref().to_vec(),
+ next_policy_seed.to_le_bytes().to_vec(),
+ vec![policy_bump],
+ ],
+ )?;
+
+ // Convert creation payload to policy type
+ // TODO: Get rid of this clone
+ let policy_state = match policy_creation_payload.clone() {
+ PolicyCreationPayload::InternalFundTransfer(creation_payload) => {
+ PolicyState::InternalFundTransfer(creation_payload.to_policy_state()?)
+ }
+ PolicyCreationPayload::LegacyProgramInteraction(creation_payload) => {
+ PolicyState::ProgramInteraction(creation_payload.to_policy_state()?)
+ }
+ PolicyCreationPayload::ProgramInteraction(creation_payload) => {
+ PolicyState::ProgramInteraction(creation_payload.to_policy_state()?)
+ }
+ PolicyCreationPayload::SpendingLimit(mut creation_payload) => {
+ // If accumulate unused is true, and the policy has a
+ // start date in the past, set it to the current
+ // timestamp to avoid unintended accumulated usage
+ let current_timestamp = Clock::get()?.unix_timestamp;
+ if creation_payload.time_constraints.accumulate_unused
+ && creation_payload.time_constraints.start < current_timestamp
+ {
+ creation_payload.time_constraints.start = current_timestamp;
+ }
+ PolicyState::SpendingLimit(creation_payload.to_policy_state()?)
+ }
+ PolicyCreationPayload::SettingsChange(creation_payload) => {
+ PolicyState::SettingsChange(creation_payload.to_policy_state()?)
+ }
+ };
+
+ let expiration: Option =
+ if let Some(expiration_args) = expiration_args {
+ match expiration_args {
+ // Use the provided timestamp
+ PolicyExpirationArgs::Timestamp(timestamp) => {
+ Some(PolicyExpiration::Timestamp(*timestamp))
+ }
+ // Generate the core state hash and use it
+ PolicyExpirationArgs::SettingsState => Some(
+ PolicyExpiration::SettingsState(self.generate_core_state_hash()?),
+ ),
+ }
+ } else {
+ None
+ };
+
+ // Create and serialize the policy
+ let policy = Policy::create_state(
+ *self_key,
+ next_policy_seed,
+ policy_bump,
+ &signers,
+ *threshold,
+ *time_lock,
+ policy_state,
+ // If no start was submitted, use the current timestamp
+ start_timestamp.unwrap_or(Clock::get()?.unix_timestamp),
+ expiration.clone(),
+ rent_payer.key(),
+ )?;
+
+ // Check the policy invariant
+ policy.invariant()?;
+ policy.try_serialize(&mut &mut policy_info.data.borrow_mut()[..])?;
+
+ // Log the event
+ let event = PolicyEvent {
+ event_type: PolicyEventType::Create,
+ settings_pubkey: self_key.to_owned(),
+ policy_pubkey: policy_pubkey,
+ policy: Some(policy),
+ };
+ if let Some(log_authority_info) = log_authority_info {
+ SmartAccountEvent::PolicyEvent(event).log(&log_authority_info)?;
+ }
+ }
+
+ SettingsAction::PolicyUpdate {
+ policy: policy_key,
+ signers,
+ threshold,
+ time_lock,
+ policy_update_payload,
+ expiration_args,
+ } => {
+ // Find the policy account
+ let policy_info = remaining_accounts
+ .iter()
+ .find(|acc| acc.key == policy_key)
+ .ok_or(SmartAccountError::MissingAccount)?;
+
+ // Verify the policy account is writable
+ require!(policy_info.is_writable, ErrorCode::AccountNotMutable);
+
+ // Deserialize the policy account and verify it belongs to this
+ // settings account
+ let mut policy = Account::::try_from(policy_info)?;
+
+ require_keys_eq!(
+ policy.settings,
+ self_key.to_owned(),
+ SmartAccountError::InvalidAccount
+ );
+
+ // Calculate policy data size based on the creation payload
+ let policy_specific_data_size = policy_update_payload.policy_state_size();
+ let policy_size = Policy::size(signers.len(), policy_specific_data_size);
+
+ // Get the rent payer and system program
+ let rent_payer = rent_payer
+ .as_ref()
+ .ok_or(SmartAccountError::MissingAccount)?;
+ let system_program = system_program
+ .as_ref()
+ .ok_or(SmartAccountError::MissingAccount)?;
+
+ // Only accept updates to the same policy type
+ let new_policy_state = match (&policy.policy_state, policy_update_payload.clone()) {
+ (
+ PolicyState::InternalFundTransfer(_),
+ PolicyCreationPayload::InternalFundTransfer(creation_payload),
+ ) => PolicyState::InternalFundTransfer(creation_payload.to_policy_state()?),
+ (
+ PolicyState::ProgramInteraction(_),
+ PolicyCreationPayload::ProgramInteraction(creation_payload),
+ ) => PolicyState::ProgramInteraction(creation_payload.to_policy_state()?),
+ (
+ PolicyState::SpendingLimit(_),
+ PolicyCreationPayload::SpendingLimit(creation_payload),
+ ) => PolicyState::SpendingLimit(creation_payload.to_policy_state()?),
+ (
+ PolicyState::SettingsChange(_),
+ PolicyCreationPayload::SettingsChange(creation_payload),
+ ) => PolicyState::SettingsChange(creation_payload.to_policy_state()?),
+ (_, _) => {
+ return err!(SmartAccountError::InvalidPolicyPayload);
+ }
+ };
+
+ // Determine the new expiration
+ let expiration: Option =
+ if let Some(expiration_args) = expiration_args {
+ match expiration_args {
+ // Use the provided timestamp
+ PolicyExpirationArgs::Timestamp(timestamp) => {
+ Some(PolicyExpiration::Timestamp(*timestamp))
+ }
+ // Generate the core state hash and use it
+ PolicyExpirationArgs::SettingsState => Some(
+ PolicyExpiration::SettingsState(self.generate_core_state_hash()?),
+ ),
+ }
+ } else {
+ None
+ };
+
+ // Update the policy
+ policy.update_state(
+ signers,
+ *threshold,
+ *time_lock,
+ new_policy_state,
+ expiration.clone(),
+ )?;
+
+ // Invalidate prior transaction due to the update
+ policy.invalidate_prior_transactions();
+
+ // Check the policy invariant
+ policy.invariant()?;
+
+ // Realloc the policy account if needed
+ Policy::realloc_if_needed(
+ policy_info.clone(),
+ signers.len(),
+ policy_size,
+ Some(rent_payer.to_account_info()),
+ Some(system_program.to_account_info()),
+ )?;
+
+ // Exit the policy account
+ policy.exit(program_id)?;
+
+ // Log the event
+ let event = PolicyEvent {
+ event_type: PolicyEventType::Update,
+ settings_pubkey: self_key.to_owned(),
+ policy_pubkey: *policy_key,
+ policy: Some(policy.clone().into_inner()),
+ };
+ if let Some(log_authority_info) = log_authority_info {
+ SmartAccountEvent::PolicyEvent(event).log(&log_authority_info)?;
+ }
+ }
+
+ SettingsAction::PolicyRemove { policy: policy_key } => {
+ let policy_info = remaining_accounts
+ .iter()
+ .find(|acc| acc.key == policy_key)
+ .ok_or(SmartAccountError::MissingAccount)?;
+
+ let rent_collector = rent_payer
+ .as_ref()
+ .ok_or(SmartAccountError::MissingAccount)?;
+
+ let policy = Account::