diff --git a/features/policies/examples/solana.mdx b/features/policies/examples/solana.mdx index bc58b281..4077c1e8 100644 --- a/features/policies/examples/solana.mdx +++ b/features/policies/examples/solana.mdx @@ -47,16 +47,20 @@ See [here](../../../concepts/policies/smart-contract-interfaces) for more inform } ``` -#### Allow Solana transactions that only use the Solana System Program +#### Allow Solana transactions whose top-level instructions only use the Solana System Program ```json { "policyName": "Enable transactions that only use the system program", "effect": "EFFECT_ALLOW", - "condition": "solana.tx.program_keys.all(p, p == '11111111111111111111111111111111')" + "condition": "solana.tx.top_level_program_keys.all(p, p == '11111111111111111111111111111111')" } ``` +This condition checks only program IDs serialized as top-level instructions. It does not restrict +programs invoked later through CPI. To bound CPI reachability, use one of the account-reference +allowlist patterns below. + #### Deny all Solana transactions transferring to an undesired address ```json @@ -97,6 +101,33 @@ See [here](../../../concepts/policies/smart-contract-interfaces) for more inform } ``` +#### Allow only vetted static accounts and reject address table lookups + +This pattern is a conservative CPI reachability allowlist. Include every expected signer, program, +and other account in the list. + +```json +{ + "policyName": "Allow only vetted static Solana accounts", + "effect": "EFFECT_ALLOW", + "condition": "solana.tx.address_table_lookups.count() == 0 && solana.tx.static_account_keys.all(k, k in ['', '', ''])" +} +``` + +#### Allow vetted static accounts and exact address table references + +Turnkey does not resolve these references to public keys during policy evaluation. Use this pattern +only after independently verifying each existing table/index mapping. Do not pre-approve an index +whose account mapping is unknown. + +```json +{ + "policyName": "Allow only vetted Solana account references", + "effect": "EFFECT_ALLOW", + "condition": "solana.tx.static_account_keys.all(k, k in ['', '']) && solana.tx.address_table_lookup_references.all(r, r.address_table_key == '' && ((r.index == && r.writable) || (r.index == && !r.writable)))" +} +``` + #### Deny sending to an address from a table lookup ```json diff --git a/features/policies/language.mdx b/features/policies/language.mdx index 38164aed..55d48c86 100644 --- a/features/policies/language.mdx +++ b/features/policies/language.mdx @@ -122,13 +122,17 @@ The language is strongly typed which makes policies easy to author and maintain. | **Eip7702Authorization** | address | string | The address you would like to authorize | | | chain_id | number | The EVM chain ID | | | nonce | number | The nonce of the authority | -| **SolanaTransaction** | account_keys | list\ | The accounts (public keys) involved in the transaction | -| | program_keys | list\ | The programs (public keys) involved in the transaction | -| | instructions | list\ | A list of Instructions (see below). Each instruction exposes a `parsed_instruction_data` field (with `instruction_name`, `named_accounts`, `program_call_args`) when a matching [Smart Contract Interface (IDL)](/features/policies/smart-contract-interfaces) has been uploaded for that program. | +| **SolanaTransaction** | static_account_keys | list\ | Account public keys serialized directly in the transaction message. This does not include addresses loaded from address lookup tables. | +| | account_keys | list\ | Backward-compatible alias for `static_account_keys` | +| | top_level_program_keys | list\ | Program public keys invoked by top-level transaction instructions. This does not include programs invoked through CPI. | +| | program_keys | list\ | Backward-compatible alias for `top_level_program_keys` | +| | top_level_instructions | list\ | Instructions serialized directly in the transaction message. This does not include instructions invoked through CPI. | +| | instructions | list\ | Backward-compatible alias for `top_level_instructions`. Each instruction exposes a `parsed_instruction_data` field (with `instruction_name`, `named_accounts`, `program_call_args`) when a matching [Smart Contract Interface (IDL)](/features/policies/smart-contract-interfaces) has been uploaded for that program. | | | transfers | list\ | A list of Transfers (see below) | | | recent_blockhash | string | The recent blockhash specified in a transaction | | | spl_transfers | list\ | A list of SPLTransfers (see below) | | | address_table_lookups | list\ | A list of AddressTableLookups (see below) | +| | address_table_lookup_references | list\ | Every address lookup table reference in the transaction, flattened in Solana runtime order. Each item identifies a table key, index, and access mode; Turnkey does not resolve it to an account public key. | | **TronTransaction** | ref_block_bytes | string | The height of the transaction reference block | | | ref_block_hash | string | The hash of the transaction reference block | | | expiration | int | Transaction expiration time in milliseconds | @@ -167,10 +171,10 @@ The language is strongly typed which makes policies easy to author and maintain. | | version | string | The version | | | chain_id | uint | The chain ID | | | verifying_contract | string | The address of the verifying contract | -| **Instruction** | program_key | string | The program (public key) involved in the instruction | +| **Instruction** | program_key | string | The program (public key) invoked by this top-level instruction | | | accounts | list\ | A list of Accounts involved in the instruction | | | instruction_data_hex | string | Raw hex bytes corresponding to instruction data | -| | address_table_lookups | list\ | A list of AddressTableLookups used in the instruction. | +| | address_table_lookups | list\ | Address lookup table references used by the instruction. | | | parsed_instruction_data | Option\ | IDL related field specifying all additional information for an instruction calling a program for which an IDL has been uploaded | | **Transfer** | from | string | A Solana account (public key) representing the sender of the transfer | | | to | string | A Solana account (public key) representing the recipient of the transfer | @@ -187,6 +191,9 @@ The language is strongly typed which makes policies easy to author and maintain. | **AddressTableLookup** | address_table_key | string | A Solana address (public key) corresponding to the address table | | | writable_indexes | list\ | Indexes corresponding to accounts that can perform writes | | | readonly_indexes | list\ | Indexes corresponding to accounts that can only perform reads | +| **AddressTableLookupReference** | address_table_key | string | A Solana address (public key) corresponding to the address lookup table | +| | index | int | The address's index in the lookup table | +| | writable | boolean | Whether the transaction requests writable access to the referenced account | | **SolanaParsedInstructionData** | instruction_name | string | IDL related field specifying the name of the instruction being called | | | discriminator | string | IDL related field specifying the byte discriminator denoting which instruction is being called by the instruction call data | | | named_account | map\ | IDL related field specifying a mapping of account names to the account string, with the names as defined by the program IDL | @@ -631,27 +638,44 @@ Here are some approaches you might take to govern transfers: ([example](/features/policies/examples/solana#allow-solana-transactions-that-have-exactly-one-transfer,-to-one-specific-recipient)). This is the most secure approach, and thus most restrictive. +#### Top-level instructions and CPI + +`solana.tx.top_level_instructions` and `solana.tx.top_level_program_keys` expose only instructions +serialized in the transaction message. Their backward-compatible aliases are `instructions` and +`program_keys`. Inner instructions created through cross-program invocation (CPI) do not exist at +signing time and are not exposed. + +Solana requires every account and program used by a CPI to be passed transitively from the +transaction's loaded account set. When a transaction does not use address lookup tables, +`solana.tx.static_account_keys` is therefore a conservative reachability allowlist for CPI as well +as top-level execution. + #### Account address lookups -Solana transactions can reference onchain address lookup tables for account addresses. Turnkey -surfaces any account address pulled from a lookup table as the literal string `ADDRESS_TABLE_LOOKUP` -in Solana address fields (`account_keys`, instruction accounts, `transfers`, and `spl_transfers`). -The `solana.tx.address_table_lookups` array indicates when lookups are present, but the specific -addresses are not resolved. If you rely on allowlists or denylists of addresses, add a guard for -this placeholder (for example, require `solana.tx.address_table_lookups.count == 0` before comparing -addresses, or explicitly deny when `ADDRESS_TABLE_LOOKUP` appears with something like -`solana.tx.transfers.any(t, t.to == 'ADDRESS_TABLE_LOOKUP')`) so that dynamic lookups cannot bypass -or unexpectedly fail your policy. See the +Solana transactions can reference onchain address lookup tables for account addresses. +`solana.tx.static_account_keys` contains only public keys serialized directly in the transaction +message. `solana.tx.address_table_lookup_references` exposes every lookup as a table key, table +index, and writable flag. Together, these fields identify the complete serialized account-reference +set for the transaction. + +Turnkey does not resolve lookup-table references to account public keys during policy evaluation. +For a self-contained public-key allowlist, require +`solana.tx.address_table_lookups.count() == 0` and allowlist `static_account_keys`. If a policy +permits lookups, approve only exact table/index/access-mode tuples whose existing account mapping +you have independently verified. Do not pre-approve an index whose account mapping is unknown. + +When a parsed transfer field refers to an unresolved lookup-table account, the transfer field uses +the literal string `ADDRESS_TABLE_LOOKUP`. See the [address table lookup examples](/features/policies/examples/solana#deny-all-address-table-lookups) -for examples on how to enforce this. +for policy patterns. #### Program address lookups -Turnkey rejects Solana transactions where program addresses are resolved via address lookup tables. -Program IDs must be statically defined in the transaction to enable static analysis of instructions -without requiring onchain data lookups. If your transaction references a program via an address -table lookup, the signing request will fail. Account addresses (non-program) can still be -dynamically resolved via lookup tables as described above. +Turnkey rejects a transaction whose top-level instruction program ID is loaded through an address +lookup table. Top-level program IDs must be static so Turnkey can parse instructions without +onchain state. A program invoked later through CPI is passed as an account to its caller and may be +identified only by an address lookup table reference. Include `address_table_lookup_references` in +the allowlist, or reject lookup tables, when the policy must bound CPI reachability. See the [Solana policy examples](/features/policies/examples/solana) for sample scenarios.