@layerzerolabs/pubkey-policy-hook-solana-contracts 0.0.0-stage → 1.2.82

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/Anchor.toml ADDED
@@ -0,0 +1,17 @@
1
+ [toolchain]
2
+ anchor_version = "1.2.1"
3
+ solana_version = "4.3.0"
4
+
5
+ [features]
6
+ resolution = false
7
+ skip-lint = false
8
+
9
+ [workspace]
10
+ members = ["programs/pubkey_policy_hook"]
11
+
12
+ [programs.localnet]
13
+ pubkey_policy_hook = "7HTVK1Jwzjd4yRGCZnLT4QmwhmZidurFxwz2RMAiUyzk"
14
+
15
+ [provider]
16
+ cluster = "Localnet"
17
+ wallet = "/dev/null"
package/Cargo.toml ADDED
@@ -0,0 +1,21 @@
1
+ [workspace]
2
+ resolver = "2"
3
+ members = ["programs/pubkey_policy_hook"]
4
+ exclude = ["dependencies"]
5
+
6
+ [workspace.dependencies]
7
+ anchor-lang = { version = "=1.2.1", default-features = false, features = ["init-if-needed", "event-cpi", "solana-v4"] }
8
+ anchor-spl = { version = "=1.2.1", default-features = false, features = ["token", "token_2022", "associated_token", "solana-v4"] }
9
+ program-id = { path = "dependencies/program-id-solana-contracts/lib" }
10
+ pubkey-policy-interface = { path = "dependencies/pubkey-policy-solana-contracts/interface" }
11
+ spl-discriminator = "0.5.2"
12
+ spl-tlv-account-resolution = "0.11.2"
13
+ spl-token-2022 = { version = "=11.0.0", features = ["no-entrypoint"] }
14
+ spl-token-2022-interface = "=3.1.1"
15
+ spl-transfer-hook-interface = "2.1.0"
16
+
17
+ [profile.release]
18
+ overflow-checks = true
19
+
20
+ [workspace.lints.rust]
21
+ unexpected_cfgs = { level = "allow", check-cfg = ['cfg(target_os, values("solana"))', 'cfg(feature, values("anchor-debug", "custom-heap", "custom-panic"))'] }
package/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ Copyright (c) 2026 - LayerZero Labs Ltd.
2
+
3
+ Permission is hereby granted, free of charge, to any
4
+ person obtaining a copy of this software and associated
5
+ documentation files (the "Software"), to deal in the
6
+ Software without restriction, including without
7
+ limitation the rights to use, copy, modify, merge,
8
+ publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software
10
+ is furnished to do so, subject to the following
11
+ conditions:
12
+ The above copyright notice and this permission notice
13
+ shall be included in all copies or substantial portions
14
+ of the Software.
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF
16
+ ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
17
+ TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
18
+ PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT
19
+ SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
22
+ IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
23
+ DEALINGS IN THE SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,39 @@
1
- # Temporary Holding Version
1
+ # Pubkey policy hook — Solana
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ `pubkey_policy_hook` is an optional Token-2022 transfer hook. On every transfer it checks the
4
+ source owner, the destination owner and the executing authority against the pubkey policy bound
5
+ into the mint's manifest; the Permanent Delegate is exempt. The mint's hook-update authority binds
6
+ or re-binds the policy with `init_transfer_hook`, and only a policy for the mint binds. Handing that
7
+ authority to a token-manager root's `TransferHook` signer puts rebinding under its DefaultAdmin
8
+ (`set_transfer_hook_policy`).
9
+
10
+ `init_transfer_hook`'s `authority` is an unchecked account, so generated clients mark it as a
11
+ non-signer. A plain key or a CPI-signing PDA must be passed as a signer; a native multisig is passed
12
+ as a non-signer, with its member signers appended as signing remaining accounts.
13
+
14
+ The hook screens transfers only. Token-2022's `SetAuthority(AccountOwner)` reassigns a token
15
+ account, balance included, without a transfer, so it never runs the hook: a blocked owner of a
16
+ non-ATA account can hand it to an allowed key. ATAs (`ImmutableOwner`) and frozen accounts cannot be
17
+ reassigned, so blocking a holder means freezing their accounts too.
18
+
19
+ It is used alongside the [token manager](../token-manager/README.md), whose README and
20
+ [usage doc](../token-manager/docs/usage.md) cover setup with Token ACL; the cross-program scenarios
21
+ that exercise the hook live in token-manager's integration suite.
22
+
23
+ ## Build and test
24
+
25
+ ```bash
26
+ pnpm build # anchor build + IDL and TS type generation
27
+ pnpm test:rust # host tests in programs/pubkey_policy_hook/tests
28
+ pnpm lint # rustfmt --check + clippy -D warnings
29
+ ```
30
+
31
+ Every command runs through the repo Docker VM tooling (`lz-tool anchor`).
32
+
33
+ `PUBKEY_POLICY_HOOK_ID` sets the program ID compiled into the binary and IDL; the default is the
34
+ development ID `7HTVK1Jwzjd4yRGCZnLT4QmwhmZidurFxwz2RMAiUyzk`. `lz-tool` does not pass host
35
+ environment variables into its container, so pass it explicitly:
36
+
37
+ ```sh
38
+ pnpm exec lz-tool --env PUBKEY_POLICY_HOOK_ID=<pubkey> --script 'sh scripts/build.sh' anchor
39
+ ```
package/package.json CHANGED
@@ -1,6 +1,54 @@
1
1
  {
2
2
  "name": "@layerzerolabs/pubkey-policy-hook-solana-contracts",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
3
+ "version": "1.2.82",
4
+ "private": false,
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "files": [
8
+ "programs",
9
+ "scripts",
10
+ "Anchor.toml",
11
+ "Cargo.toml",
12
+ "rust-toolchain.toml",
13
+ "rustfmt.toml",
14
+ "turbo.json",
15
+ "turbo-snapshot.lock.json",
16
+ "!**/target/**",
17
+ "!**/*-keypair.json"
18
+ ],
19
+ "dependencies": {
20
+ "@layerzerolabs/program-id-solana-contracts": "1.2.82",
21
+ "@layerzerolabs/pubkey-policy-solana-contracts": "1.2.82"
22
+ },
23
+ "devDependencies": {
24
+ "@layerzerolabs/build-utils-rust": "1.2.82",
25
+ "@layerzerolabs/vm-tooling-solana": "1.2.82"
26
+ },
27
+ "publishConfig": {
28
+ "access": "public",
29
+ "registry": "https://registry.npmjs.org/"
30
+ },
31
+ "externalRepoConfig": {
32
+ "targets": [
33
+ "audit-external",
34
+ "monorepo-external"
35
+ ]
36
+ },
37
+ "implicitDependencies": {
38
+ "@layerzerolabs/program-id-solana-contracts": "workspace:*",
39
+ "@layerzerolabs/pubkey-policy-solana-contracts": "workspace:*"
40
+ },
41
+ "scripts": {
42
+ "allow-read-write": "[ -z \"$CI\" ] || sudo chmod -R a+rwx ./target",
43
+ "build": "pnpm resolve-dependencies && pnpm exec lz-tool --script 'sh scripts/build.sh' anchor && pnpm build:check && pnpm allow-read-write",
44
+ "build:check": "sh scripts/check-build-outputs.sh",
45
+ "format": "pnpm exec lz-tool --script 'cargo +nightly-2025-06-01 fmt --all' anchor",
46
+ "format:check": "pnpm exec lz-tool --script 'cargo +nightly-2025-06-01 fmt --all -- --check' anchor",
47
+ "lint": "pnpm resolve-dependencies && pnpm format:check && pnpm exec lz-tool --script 'cargo clippy --workspace --all-targets --locked -- -D warnings' anchor",
48
+ "resolve-dependencies": "pnpm exec build-utils-rust resolve",
49
+ "test": "pnpm test:rust",
50
+ "test:rust": "pnpm resolve-dependencies && pnpm exec lz-tool --script 'cargo test --workspace --locked' anchor",
51
+ "test:snapshot:check": "pnpm build:check && pnpm exec turbo-snapshot check",
52
+ "test:snapshot:update": "pnpm build:check && pnpm exec turbo-snapshot update"
53
+ }
6
54
  }
@@ -0,0 +1,36 @@
1
+ [package]
2
+ name = "pubkey-policy-hook"
3
+ version = "0.1.0"
4
+ authors = ["LayerZero Labs", "Shankar (@shankars99)"]
5
+ edition = "2021"
6
+ license = "MIT"
7
+ description = "Optional Token-2022 hook enforcing a bound pubkey policy"
8
+
9
+ [lib]
10
+ crate-type = ["cdylib", "lib"]
11
+ name = "pubkey_policy_hook"
12
+
13
+ [features]
14
+ default = []
15
+ no-entrypoint = []
16
+ no-idl = []
17
+ no-log-ix-name = []
18
+ cpi = ["no-entrypoint"]
19
+ custom-heap = []
20
+ custom-panic = []
21
+ anchor-debug = []
22
+ idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build", "pubkey-policy-interface/idl-build"]
23
+
24
+ [dependencies]
25
+ anchor-lang.workspace = true
26
+ anchor-spl.workspace = true
27
+ pubkey-policy-interface.workspace = true
28
+ program-id.workspace = true
29
+ spl-discriminator.workspace = true
30
+ spl-tlv-account-resolution.workspace = true
31
+ spl-token-2022.workspace = true
32
+ spl-token-2022-interface.workspace = true
33
+ spl-transfer-hook-interface.workspace = true
34
+
35
+ [lints]
36
+ workspace = true
@@ -0,0 +1,10 @@
1
+ pubkey_policy_interface::declare_policy_errors! {
2
+ pub enum HookError {
3
+ #[msg("Only the mint transfer-hook authority may configure its pubkey policy")]
4
+ Unauthorized = 9000,
5
+ #[msg("The bound pubkey policy is for another mint")]
6
+ WrongPolicyMint,
7
+ #[msg("The hook only runs inside a Token-2022 transfer")]
8
+ NotTransferring,
9
+ }
10
+ }
@@ -0,0 +1,8 @@
1
+ use anchor_lang::prelude::*;
2
+
3
+ #[event]
4
+ pub struct TransferHookInitialized {
5
+ pub mint: Pubkey,
6
+ pub pubkey_policy: Pubkey,
7
+ pub authority: Pubkey,
8
+ }
@@ -0,0 +1,99 @@
1
+ use crate::{
2
+ account_metas, errors::HookError, TransferHookInitialized, EXTRA_COUNT, EXTRA_METAS_SEED,
3
+ };
4
+ use anchor_lang::prelude::*;
5
+ use anchor_spl::token_interface::Mint;
6
+ use pubkey_policy_interface::PubkeyPolicy;
7
+ use spl_tlv_account_resolution::state::ExtraAccountMetaList;
8
+ use spl_token_2022::processor::Processor;
9
+ use spl_token_2022_interface::{
10
+ extension::{transfer_hook::TransferHook, BaseStateWithExtensions, StateWithExtensions},
11
+ state::Mint as MintState,
12
+ };
13
+ use spl_transfer_hook_interface::instruction::ExecuteInstruction;
14
+
15
+ /// Only the mint's hook-update authority may bind or re-bind the policy, proven exactly as
16
+ /// Token-2022 would: a plain signer, a CPI-signing PDA, or a native multisig whose member
17
+ /// signatures are the remaining accounts (a multisig's own key never counts). The mint need not
18
+ /// name this hook yet: the manifest is inert until it does, so it can be staged before a switch.
19
+ /// The policy must be for this mint: the hook refuses any other on every transfer, which would
20
+ /// stop the mint's transfers until the binding is fixed, and for good once the hook authority is
21
+ /// renounced.
22
+ #[event_cpi]
23
+ #[derive(Accounts)]
24
+ pub struct InitTransferHook<'info> {
25
+ #[account(mut)]
26
+ pub payer: Signer<'info>,
27
+
28
+ /// CHECK: The mint's hook-update authority, authenticated in the handler. Pass it as a signer
29
+ /// for a plain key or CPI-signing PDA, and as a non-signer for a native multisig, whose member
30
+ /// signers are the remaining accounts.
31
+ pub authority: UncheckedAccount<'info>,
32
+
33
+ #[account(owner = spl_token_2022_interface::ID)]
34
+ pub mint: InterfaceAccount<'info, Mint>,
35
+
36
+ /// CHECK: The pubkey policy the hook authority binds, under whichever program owns it; both
37
+ /// are written into the manifest. It must already exist (checked in `apply`), since its owner
38
+ /// is recorded as the program its records live under.
39
+ pub pubkey_policy: UncheckedAccount<'info>,
40
+
41
+ /// CHECK: The mint's hook manifest; written only after `authority` is authenticated.
42
+ #[account(
43
+ init_if_needed,
44
+ payer = payer,
45
+ space = ExtraAccountMetaList::size_of(EXTRA_COUNT).unwrap(),
46
+ seeds = [EXTRA_METAS_SEED, mint.key().as_ref()],
47
+ bump,
48
+ )]
49
+ pub extra_account_metas: UncheckedAccount<'info>,
50
+
51
+ pub system_program: Program<'info, System>,
52
+ }
53
+
54
+ impl InitTransferHook<'_> {
55
+ pub fn apply(ctx: &mut Context<Self>) -> Result<()> {
56
+ {
57
+ let mint_info = ctx.accounts.mint.to_account_info();
58
+ let mint_data = mint_info.try_borrow_data()?;
59
+ let mint = StateWithExtensions::<MintState>::unpack(&mint_data)?;
60
+ let hook = mint.get_extension::<TransferHook>()?;
61
+ let expected =
62
+ Option::<Pubkey>::from(hook.authority).ok_or(error!(HookError::Unauthorized))?;
63
+ // Pass Token-2022 as program_id, never this program: validate_owner treats any account
64
+ // owned by program_id with multisig length as a multisig.
65
+ let authority = ctx.accounts.authority.to_account_info();
66
+ Processor::validate_owner(
67
+ &spl_token_2022_interface::ID,
68
+ &expected,
69
+ &authority,
70
+ authority.data_len(),
71
+ ctx.remaining_accounts,
72
+ )
73
+ .map_err(|_| error!(HookError::Unauthorized))?;
74
+ }
75
+ // A policy not created yet is owned by the System Program, which would be recorded as the
76
+ // policy program.
77
+ let policy =
78
+ PubkeyPolicy::try_deserialize(&mut &ctx.accounts.pubkey_policy.try_borrow_data()?[..])?;
79
+ require_keys_eq!(policy.mint, ctx.accounts.mint.key(), HookError::WrongPolicyMint);
80
+ {
81
+ let mut manifest = ctx.accounts.extra_account_metas.try_borrow_mut_data()?;
82
+ manifest.fill(0);
83
+ ExtraAccountMetaList::init::<ExecuteInstruction>(
84
+ &mut manifest,
85
+ &account_metas(
86
+ &ctx.accounts.pubkey_policy.key(),
87
+ ctx.accounts.pubkey_policy.owner,
88
+ )?,
89
+ )?;
90
+ }
91
+
92
+ emit_cpi!(TransferHookInitialized {
93
+ mint: ctx.accounts.mint.key(),
94
+ pubkey_policy: ctx.accounts.pubkey_policy.key(),
95
+ authority: ctx.accounts.authority.key(),
96
+ });
97
+ Ok(())
98
+ }
99
+ }
@@ -0,0 +1,5 @@
1
+ pub mod init_transfer_hook;
2
+ pub mod transfer_hook;
3
+
4
+ pub use init_transfer_hook::*;
5
+ pub use transfer_hook::*;
@@ -0,0 +1,103 @@
1
+ use crate::errors::HookError;
2
+ use anchor_lang::prelude::*;
3
+ use anchor_spl::token_interface::{Mint, TokenAccount};
4
+ use pubkey_policy_interface::{PolicyError, ValidatedPubkeyPolicy};
5
+ use spl_token_2022_interface::{
6
+ extension::{
7
+ permanent_delegate::get_permanent_delegate, transfer_hook::TransferHookAccount,
8
+ BaseStateWithExtensions, StateWithExtensions,
9
+ },
10
+ state::{Account as AccountState, Mint as MintState},
11
+ };
12
+
13
+ #[derive(Clone, AnchorSerialize, AnchorDeserialize)]
14
+ pub struct TransferHookParams {
15
+ /// SPL `Execute` payload; unused, since the policy does not depend on the amount.
16
+ pub amount: u64,
17
+ }
18
+
19
+ /// Token-2022 invokes this during a transfer with the accounts it resolved from this mint's
20
+ /// manifest, which only `init_transfer_hook` writes (first extra: the bound pubkey policy). The
21
+ /// hook writes nothing and makes no CPI. It requires Token-2022's `transferring` flag on both token
22
+ /// accounts, so a direct `Execute` call fails instead of looking like a transfer; it does not
23
+ /// re-check the manifest.
24
+ #[derive(Accounts)]
25
+ pub struct TransferHook<'info> {
26
+ pub source: InterfaceAccount<'info, TokenAccount>,
27
+
28
+ pub mint: InterfaceAccount<'info, Mint>,
29
+
30
+ pub destination: InterfaceAccount<'info, TokenAccount>,
31
+
32
+ /// CHECK: Token-2022 validated the owner, delegate or permanent delegate.
33
+ pub authority: UncheckedAccount<'info>,
34
+
35
+ /// CHECK: The canonical manifest Token-2022 resolved the extras from.
36
+ pub extra_account_metas: UncheckedAccount<'info>,
37
+
38
+ /// CHECK: The policy the manifest binds; decoded in the handler.
39
+ pub pubkey_policy: UncheckedAccount<'info>,
40
+
41
+ /// CHECK: The program that owns the policy and its records, named by the manifest for the
42
+ /// records' derivation; never invoked. The reader takes the program from the policy's owner,
43
+ /// so a manifest naming another program fails the record check.
44
+ pub policy_program: UncheckedAccount<'info>,
45
+
46
+ /// CHECK: Owner's subject record; `ValidatedPubkeyPolicy::read` checks its address and owner.
47
+ pub source_record: UncheckedAccount<'info>,
48
+
49
+ /// CHECK: Owner's subject record; `ValidatedPubkeyPolicy::read` checks its address and owner.
50
+ pub destination_record: UncheckedAccount<'info>,
51
+
52
+ /// CHECK: Authority's subject record; `ValidatedPubkeyPolicy::read` checks its address and
53
+ /// owner.
54
+ pub authority_record: UncheckedAccount<'info>,
55
+ }
56
+
57
+ impl TransferHook<'_> {
58
+ pub fn apply(ctx: &Context<Self>, _params: &TransferHookParams) -> Result<()> {
59
+ let accounts = &ctx.accounts;
60
+ require!(
61
+ is_transferring(&accounts.source)? && is_transferring(&accounts.destination)?,
62
+ HookError::NotTransferring
63
+ );
64
+
65
+ // The mint's Permanent Delegate is exempt, matching EVM `recoverFunds`' unchecked
66
+ // `_transfer`. Token-2022 has authenticated it and already rejects frozen accounts.
67
+ if accounts.authority_is_permanent_delegate()? {
68
+ return Ok(());
69
+ }
70
+
71
+ // Check source, destination and the actual executing authority, not source.delegate:
72
+ // Token-2022 clears an exhausted delegate allowance before invoking us.
73
+ for (record, subject) in [
74
+ (&accounts.source_record, accounts.source.owner),
75
+ (&accounts.destination_record, accounts.destination.owner),
76
+ (&accounts.authority_record, accounts.authority.key()),
77
+ ] {
78
+ let validated = ValidatedPubkeyPolicy::read(&accounts.pubkey_policy, record, &subject)?;
79
+ // `init_transfer_hook` already refuses another mint's policy; checked again so the hook
80
+ // stays correct on its own.
81
+ require_keys_eq!(validated.mint(), accounts.mint.key(), HookError::WrongPolicyMint);
82
+ require!(!validated.is_blocked(), PolicyError::Blocked);
83
+ }
84
+ Ok(())
85
+ }
86
+
87
+ fn authority_is_permanent_delegate(&self) -> Result<bool> {
88
+ let mint_info = self.mint.to_account_info();
89
+ let mint_data = mint_info.try_borrow_data()?;
90
+ let mint = StateWithExtensions::<MintState>::unpack(&mint_data)?;
91
+ Ok(get_permanent_delegate(&mint) == Some(self.authority.key()))
92
+ }
93
+ }
94
+
95
+ /// Token-2022 sets `transferring` on both accounts only for the duration of the hook call.
96
+ fn is_transferring(account: &InterfaceAccount<TokenAccount>) -> Result<bool> {
97
+ let info = account.to_account_info();
98
+ let data = info.try_borrow_data()?;
99
+ let state = StateWithExtensions::<AccountState>::unpack(&data)?;
100
+ Ok(state
101
+ .get_extension::<TransferHookAccount>()
102
+ .is_ok_and(|ext| bool::from(ext.transferring)))
103
+ }
@@ -0,0 +1,33 @@
1
+ //! Optional transfer-time enforcement of a bound pubkey policy.
2
+
3
+ use anchor_lang::prelude::*;
4
+ use program_id::declare_program_id;
5
+ use spl_discriminator::SplDiscriminate;
6
+ use spl_transfer_hook_interface::instruction::ExecuteInstruction;
7
+
8
+ pub mod errors;
9
+ pub mod events;
10
+ pub mod instructions;
11
+ pub mod state;
12
+
13
+ pub use events::*;
14
+ pub use instructions::*;
15
+ pub use state::*;
16
+
17
+ declare_program_id!("PUBKEY_POLICY_HOOK_ID", "7HTVK1Jwzjd4yRGCZnLT4QmwhmZidurFxwz2RMAiUyzk");
18
+
19
+ #[program]
20
+ pub mod pubkey_policy_hook {
21
+ use super::*;
22
+
23
+ /// Create or rewrite the mint's hook manifest to bind `pubkey_policy`. Requires the mint's
24
+ /// hook-update authority; changes neither the policy nor the mint.
25
+ pub fn init_transfer_hook(mut ctx: Context<InitTransferHook>) -> Result<()> {
26
+ InitTransferHook::apply(&mut ctx)
27
+ }
28
+
29
+ #[instruction(discriminator = ExecuteInstruction::SPL_DISCRIMINATOR_SLICE)]
30
+ pub fn transfer_hook(ctx: Context<TransferHook>, params: TransferHookParams) -> Result<()> {
31
+ TransferHook::apply(&ctx, &params)
32
+ }
33
+ }
@@ -0,0 +1,51 @@
1
+ //! The canonical SPL manifest is the hook's only persistent state.
2
+
3
+ use anchor_lang::prelude::*;
4
+ use pubkey_policy_interface::SUBJECT_SEED;
5
+ use spl_tlv_account_resolution::{account::ExtraAccountMeta, seeds::Seed};
6
+
7
+ pub const EXTRA_METAS_SEED: &[u8] = b"extra-account-metas";
8
+ pub const EXTRA_COUNT: usize = 5;
9
+ const SOURCE_ACCOUNT_INDEX: u8 = 0;
10
+ const PUBKEY_POLICY_ACCOUNT_INDEX: u8 = 5;
11
+ const DESTINATION_ACCOUNT_INDEX: u8 = 2;
12
+ const AUTHORITY_ACCOUNT_INDEX: u8 = 3;
13
+ const POLICY_PROGRAM_ACCOUNT_INDEX: u8 = 6;
14
+ // SPL account base layout: mint (32 bytes), then owner (32 bytes).
15
+ const TOKEN_ACCOUNT_OWNER_OFFSET: u8 = 32;
16
+ const PUBKEY_LENGTH: u8 = 32;
17
+
18
+ /// Execute accounts: source, mint, destination, authority, manifest, pubkey policy (5), the
19
+ /// program that owns it (6), and the source/destination/authority records under it (7-9).
20
+ pub fn account_metas(
21
+ pubkey_policy: &Pubkey,
22
+ policy_program: &Pubkey,
23
+ ) -> Result<Vec<ExtraAccountMeta>> {
24
+ let record = |subject_seed: Seed| {
25
+ ExtraAccountMeta::new_external_pda_with_seeds(
26
+ POLICY_PROGRAM_ACCOUNT_INDEX,
27
+ &[
28
+ Seed::Literal { bytes: SUBJECT_SEED.to_vec() },
29
+ Seed::AccountKey { index: PUBKEY_POLICY_ACCOUNT_INDEX },
30
+ subject_seed,
31
+ ],
32
+ false,
33
+ false,
34
+ )
35
+ };
36
+ Ok(vec![
37
+ ExtraAccountMeta::new_with_pubkey(pubkey_policy, false, false)?,
38
+ ExtraAccountMeta::new_with_pubkey(policy_program, false, false)?,
39
+ record(Seed::AccountData {
40
+ account_index: SOURCE_ACCOUNT_INDEX,
41
+ data_index: TOKEN_ACCOUNT_OWNER_OFFSET,
42
+ length: PUBKEY_LENGTH,
43
+ })?,
44
+ record(Seed::AccountData {
45
+ account_index: DESTINATION_ACCOUNT_INDEX,
46
+ data_index: TOKEN_ACCOUNT_OWNER_OFFSET,
47
+ length: PUBKEY_LENGTH,
48
+ })?,
49
+ record(Seed::AccountKey { index: AUTHORITY_ACCOUNT_INDEX })?,
50
+ ])
51
+ }
@@ -0,0 +1,276 @@
1
+ //! Handler-level policy tests, including Anchor account validation. These do not
2
+ //! replace runtime tests of Token-2022 authority checks or transaction rollback.
3
+
4
+ use anchor_lang::{prelude::*, solana_program::program_option::COption};
5
+ use pubkey_policy_hook::{
6
+ account_metas,
7
+ errors::HookError,
8
+ instructions::transfer_hook::{TransferHook, TransferHookBumps, TransferHookParams},
9
+ EXTRA_COUNT, EXTRA_METAS_SEED, ID,
10
+ };
11
+ use pubkey_policy_interface::{
12
+ pubkey_policy_address, subject_address, Membership, PolicyError, PubkeyPolicy,
13
+ PubkeyPolicyMode, SubjectRecord,
14
+ };
15
+ use spl_tlv_account_resolution::state::ExtraAccountMetaList;
16
+ use spl_token_2022::{
17
+ extension::{
18
+ permanent_delegate::PermanentDelegate,
19
+ transfer_hook::{TransferHook as HookExtension, TransferHookAccount},
20
+ BaseStateWithExtensionsMut, ExtensionType, StateWithExtensionsMut,
21
+ },
22
+ state::{Account as AccountState, Mint as MintState},
23
+ };
24
+ use spl_transfer_hook_interface::instruction::ExecuteInstruction;
25
+ use std::collections::BTreeSet;
26
+
27
+ // Positions in the Execute account list (see `account_metas`).
28
+ const PUBKEY_POLICY: usize = 5;
29
+ const POLICY_PROGRAM: usize = 6;
30
+ const SOURCE_RECORD: usize = 7;
31
+ const DESTINATION_RECORD: usize = 8;
32
+ const AUTHORITY_RECORD: usize = 9;
33
+
34
+ struct TestAccount {
35
+ key: Pubkey,
36
+ owner: Pubkey,
37
+ data: Vec<u8>,
38
+ lamports: u64,
39
+ executable: bool,
40
+ }
41
+
42
+ impl TestAccount {
43
+ fn new(key: Pubkey, owner: Pubkey, data: Vec<u8>) -> Self {
44
+ Self { key, owner, data, lamports: 1_000_000, executable: false }
45
+ }
46
+
47
+ fn info(&mut self) -> AccountInfo<'_> {
48
+ AccountInfo::new(
49
+ &self.key,
50
+ false,
51
+ false,
52
+ &mut self.lamports,
53
+ &mut self.data,
54
+ &self.owner,
55
+ self.executable,
56
+ )
57
+ }
58
+ }
59
+
60
+ fn record(policy: &Pubkey, subject: &Pubkey, blocked: bool) -> TestAccount {
61
+ let mut data = Vec::new();
62
+ SubjectRecord {
63
+ policy: *policy,
64
+ subject: *subject,
65
+ membership: Membership { blacklisted: blocked, whitelisted: false },
66
+ }
67
+ .try_serialize(&mut data)
68
+ .unwrap();
69
+ TestAccount::new(subject_address(policy, subject).0, pubkey_policy_interface::ID, data)
70
+ }
71
+
72
+ fn token_data(
73
+ mint: Pubkey,
74
+ owner: Pubkey,
75
+ delegate: Option<Pubkey>,
76
+ transferring: bool,
77
+ ) -> Vec<u8> {
78
+ let mut data = vec![
79
+ 0;
80
+ ExtensionType::try_calculate_account_len::<AccountState>(&[
81
+ ExtensionType::TransferHookAccount
82
+ ],)
83
+ .unwrap()
84
+ ];
85
+ let mut state =
86
+ StateWithExtensionsMut::<AccountState>::unpack_uninitialized(&mut data).unwrap();
87
+ state.base = AccountState {
88
+ mint,
89
+ owner,
90
+ amount: 100,
91
+ delegate: delegate.into(),
92
+ state: spl_token_2022::state::AccountState::Initialized,
93
+ is_native: COption::None,
94
+ delegated_amount: if delegate.is_some() { 100 } else { 0 },
95
+ close_authority: COption::None,
96
+ };
97
+ state.init_extension::<TransferHookAccount>(false).unwrap().transferring = transferring.into();
98
+ state.init_account_type().unwrap();
99
+ state.pack_base();
100
+ data
101
+ }
102
+
103
+ /// When Some(true), the authority is the PD but is deliberately neither owner
104
+ /// nor approved delegate. Some(false) configures a different PD. None omits PD.
105
+ fn run_hook(
106
+ source_blocked: bool,
107
+ destination_blocked: bool,
108
+ authority_blocked: bool,
109
+ authority_is_pd: Option<bool>,
110
+ transferring: [bool; 2],
111
+ manifest_binds_other_policy: bool,
112
+ ) -> Result<()> {
113
+ run_hook_with_accounts(
114
+ source_blocked,
115
+ destination_blocked,
116
+ authority_blocked,
117
+ authority_is_pd,
118
+ transferring,
119
+ manifest_binds_other_policy,
120
+ |_| {},
121
+ )
122
+ }
123
+
124
+ fn run_hook_with_accounts(
125
+ source_blocked: bool,
126
+ destination_blocked: bool,
127
+ authority_blocked: bool,
128
+ authority_is_pd: Option<bool>,
129
+ transferring: [bool; 2],
130
+ manifest_binds_other_policy: bool,
131
+ mutate: impl FnOnce(&mut [TestAccount]),
132
+ ) -> Result<()> {
133
+ let mint = Pubkey::new_unique();
134
+ let admin = Pubkey::new_unique();
135
+ let authority = Pubkey::new_unique();
136
+ let source_owner = Pubkey::new_unique();
137
+ let destination_owner = Pubkey::new_unique();
138
+ let policy_key = pubkey_policy_address(&mint, &admin).0;
139
+ let policy = PubkeyPolicy {
140
+ mint,
141
+ current_default_admin: admin,
142
+ pending_default_admin: Pubkey::default(),
143
+ mode: PubkeyPolicyMode::Blacklist,
144
+ };
145
+ let mut policy_data = Vec::new();
146
+ policy.try_serialize(&mut policy_data).unwrap();
147
+
148
+ let mut extensions = vec![ExtensionType::TransferHook];
149
+ if authority_is_pd.is_some() {
150
+ extensions.push(ExtensionType::PermanentDelegate);
151
+ }
152
+ let mut mint_data =
153
+ vec![0; ExtensionType::try_calculate_account_len::<MintState>(&extensions).unwrap()];
154
+ let mut mint_state =
155
+ StateWithExtensionsMut::<MintState>::unpack_uninitialized(&mut mint_data).unwrap();
156
+ mint_state.base.is_initialized = true;
157
+ mint_state.base.decimals = 9;
158
+ mint_state.init_extension::<HookExtension>(false).unwrap().program_id =
159
+ Some(ID).try_into().unwrap();
160
+ if let Some(is_pd) = authority_is_pd {
161
+ let pd = if is_pd { authority } else { Pubkey::new_unique() };
162
+ mint_state.init_extension::<PermanentDelegate>(false).unwrap().delegate =
163
+ Some(pd).try_into().unwrap();
164
+ }
165
+ mint_state.init_account_type().unwrap();
166
+ mint_state.pack_base();
167
+
168
+ let manifest_key = Pubkey::find_program_address(&[EXTRA_METAS_SEED, mint.as_ref()], &ID).0;
169
+ let mut manifest_data = vec![0; ExtraAccountMetaList::size_of(EXTRA_COUNT).unwrap()];
170
+ ExtraAccountMetaList::init::<ExecuteInstruction>(
171
+ &mut manifest_data,
172
+ &account_metas(
173
+ &if manifest_binds_other_policy {
174
+ pubkey_policy_address(&mint, &Pubkey::new_unique()).0
175
+ } else {
176
+ policy_key
177
+ },
178
+ &pubkey_policy_interface::ID,
179
+ )
180
+ .unwrap(),
181
+ )
182
+ .unwrap();
183
+ let source_delegate = (authority_is_pd != Some(true)).then_some(authority);
184
+ let mut accounts = [
185
+ TestAccount::new(
186
+ Pubkey::new_unique(),
187
+ spl_token_2022::ID,
188
+ token_data(mint, source_owner, source_delegate, transferring[0]),
189
+ ),
190
+ TestAccount::new(mint, spl_token_2022::ID, mint_data),
191
+ TestAccount::new(
192
+ Pubkey::new_unique(),
193
+ spl_token_2022::ID,
194
+ token_data(mint, destination_owner, None, transferring[1]),
195
+ ),
196
+ TestAccount::new(authority, System::id(), vec![]),
197
+ TestAccount::new(manifest_key, ID, manifest_data),
198
+ TestAccount::new(policy_key, pubkey_policy_interface::ID, policy_data),
199
+ TestAccount {
200
+ executable: true,
201
+ ..TestAccount::new(
202
+ pubkey_policy_interface::ID,
203
+ anchor_lang::solana_program::bpf_loader_upgradeable::ID,
204
+ vec![],
205
+ )
206
+ },
207
+ record(&policy_key, &source_owner, source_blocked),
208
+ record(&policy_key, &destination_owner, destination_blocked),
209
+ record(&policy_key, &authority, authority_blocked),
210
+ ];
211
+ mutate(&mut accounts);
212
+ let infos: Vec<_> = accounts.iter_mut().map(TestAccount::info).collect();
213
+ let mut remaining = infos.as_slice();
214
+ let mut bumps = TransferHookBumps::default();
215
+ let mut validated =
216
+ TransferHook::try_accounts(&ID, &mut remaining, &[], &mut bumps, &mut BTreeSet::new())?;
217
+ let context = Context::new(&ID, &mut validated, remaining, bumps);
218
+ TransferHook::apply(&context, &TransferHookParams { amount: 1 })
219
+ }
220
+
221
+ fn assert_error(result: Result<()>, code: impl Into<u32>) {
222
+ let anchor_lang::error::Error::AnchorError(error) = result.unwrap_err() else {
223
+ panic!("expected an Anchor error");
224
+ };
225
+ assert_eq!(error.error_code_number, code.into());
226
+ }
227
+
228
+ /// The mint's actual permanent delegate is exempt from every check; anyone else is checked on
229
+ /// source, destination and authority.
230
+ #[test]
231
+ fn actual_pd_is_exempt_and_everyone_else_is_fully_checked() {
232
+ for is_pd in [None, Some(false), Some(true)] {
233
+ for source in [false, true] {
234
+ for destination in [false, true] {
235
+ for authority in [false, true] {
236
+ let result =
237
+ run_hook(source, destination, authority, is_pd, [true, true], false);
238
+ let allowed = is_pd == Some(true) || (!source && !destination && !authority);
239
+ if allowed {
240
+ result.unwrap();
241
+ } else {
242
+ assert_error(result, PolicyError::Blocked);
243
+ }
244
+ }
245
+ }
246
+ }
247
+ }
248
+ }
249
+
250
+ #[test]
251
+ fn typed_policy_and_subject_record_boundaries_remain_enforced() {
252
+ use anchor_lang::error::ErrorCode;
253
+ type Substitute = fn(&mut [TestAccount]);
254
+ let substitutions: [(Substitute, u32); 5] = [
255
+ (|a| a[PUBKEY_POLICY].data[..8].fill(0), ErrorCode::AccountDiscriminatorMismatch.into()),
256
+ // The policy's mint follows its discriminator.
257
+ (|a| a[PUBKEY_POLICY].data[8..40].fill(7), HookError::WrongPolicyMint.into()),
258
+ (|a| a[SOURCE_RECORD].key = Pubkey::new_unique(), PolicyError::WrongRecord.into()),
259
+ (|a| a[DESTINATION_RECORD].key = Pubkey::new_unique(), PolicyError::WrongRecord.into()),
260
+ (|a| a[AUTHORITY_RECORD].key = Pubkey::new_unique(), PolicyError::WrongRecord.into()),
261
+ ];
262
+ for (substitute, code) in substitutions {
263
+ assert_error(
264
+ run_hook_with_accounts(false, false, false, None, [true, true], false, substitute),
265
+ code,
266
+ );
267
+ }
268
+ }
269
+
270
+ #[test]
271
+ fn policy_program_key_is_a_pda_namespace_not_an_invocation_target() {
272
+ run_hook_with_accounts(false, false, false, None, [true, true], false, |accounts| {
273
+ accounts[POLICY_PROGRAM].executable = false;
274
+ })
275
+ .unwrap();
276
+ }
@@ -0,0 +1,3 @@
1
+ [toolchain]
2
+ channel = "1.97.1"
3
+ components = ["rustfmt", "clippy"]
package/rustfmt.toml ADDED
@@ -0,0 +1,19 @@
1
+ # Matches console-oft-app/contracts/solana.
2
+ hard_tabs = false
3
+ max_width = 100
4
+ use_small_heuristics = "Max"
5
+ imports_granularity = "Crate"
6
+ reorder_imports = true
7
+ newline_style = "Unix"
8
+ comment_width = 100
9
+ wrap_comments = true
10
+ chain_width = 80
11
+ spaces_around_ranges = false
12
+ binop_separator = "Front"
13
+ reorder_impl_items = false
14
+ match_arm_leading_pipes = "Preserve"
15
+ match_arm_blocks = true
16
+ match_block_trailing_comma = true
17
+ trailing_comma = "Vertical"
18
+ trailing_semicolon = true
19
+ use_field_init_shorthand = true
@@ -0,0 +1,8 @@
1
+ #!/bin/sh
2
+ # Runs inside the Anchor VM. Macro-derived addresses need repairing in JSON
3
+ # before generating the TypeScript types, otherwise their address literals differ.
4
+ set -eu
5
+
6
+ anchor build --ignore-keys
7
+ add_address_to_idl.py
8
+ anchor idl type target/idl/pubkey_policy_hook.json --out target/types/pubkey_policy_hook.ts
@@ -0,0 +1,11 @@
1
+ #!/bin/sh
2
+ # The generic snapshot tool can succeed with a partially populated output tree.
3
+ # Require every production artifact; ignore unrelated or stale local artifacts.
4
+ set -eu
5
+
6
+ for artifact in target/deploy/pubkey_policy_hook.so target/idl/pubkey_policy_hook.json target/types/pubkey_policy_hook.ts; do
7
+ if [ ! -s "$artifact" ]; then
8
+ echo "Missing or empty build artifact: $artifact (run pnpm build first)" >&2
9
+ exit 1
10
+ fi
11
+ done
@@ -0,0 +1,218 @@
1
+ {
2
+ "dependencies/framework-solana-anchor-trait/Cargo.toml": {
3
+ "hash": "0b7f948a88e45d146c6d2e7398cb8bed5c6f9055efbeb304f4dc0fb6b8886a2e",
4
+ "size": 953
5
+ },
6
+ "dependencies/framework-solana-anchor-trait/src/assembly.rs": {
7
+ "hash": "94d17961365f4ad90ecc7e5f6490032f4bf88887ded949d44a816841ece37884",
8
+ "size": 4451
9
+ },
10
+ "dependencies/framework-solana-anchor-trait/src/generation.rs": {
11
+ "hash": "13cc38f6473f8629f4a2acfdcc64f0d5e328a63ea44ada2b80386f10ea4b35b9",
12
+ "size": 8385
13
+ },
14
+ "dependencies/framework-solana-anchor-trait/src/lib.rs": {
15
+ "hash": "7fd1a01908e0da13281f55106ee97191a3fcca9ba9d56dbd67f351b22bd9de76",
16
+ "size": 5525
17
+ },
18
+ "dependencies/framework-solana-anchor-trait/src/overrides.rs": {
19
+ "hash": "0a2c9dc934266b3d0da955592a5d5cb140a65d31558d8b0a0c474cfd4d9a25b4",
20
+ "size": 10546
21
+ },
22
+ "dependencies/program-id-solana-contracts/Cargo.toml": {
23
+ "hash": "80b65002644e7d4dee9c1817b4e601771389cde3999ad07c4c78b985731eb8cf",
24
+ "size": 89
25
+ },
26
+ "dependencies/program-id-solana-contracts/lib/Cargo.toml": {
27
+ "hash": "72a806d6fd7b3ff3205bb0c187313d5b4e72e9bdd4394713bd8d44b054404d85",
28
+ "size": 306
29
+ },
30
+ "dependencies/program-id-solana-contracts/lib/src/lib.rs": {
31
+ "hash": "418ce6414b5aac485677d046e3a5f73f2f82002f7a7fe161c7c44edae15c2fb4",
32
+ "size": 2226
33
+ },
34
+ "dependencies/pubkey-policy-solana-contracts/Cargo.toml": {
35
+ "hash": "5cb0ffec9e21293faa338723ac505853da1fcbb5ddb6c99f9d8ad40c5fca7828",
36
+ "size": 1186
37
+ },
38
+ "dependencies/pubkey-policy-solana-contracts/interface/Cargo.toml": {
39
+ "hash": "afa7632da897d1e2c3f347ad530795c578f4f3ff050ff1117007f44b19b0a9a0",
40
+ "size": 296
41
+ },
42
+ "dependencies/pubkey-policy-solana-contracts/interface/src/error.rs": {
43
+ "hash": "df8a87d7d773ff2070faba2cb7cc6dcbb4199c22966ea988da8a560d3f0ec693",
44
+ "size": 1342
45
+ },
46
+ "dependencies/pubkey-policy-solana-contracts/interface/src/lib.rs": {
47
+ "hash": "d2f68529deea1483337a8bae664ee07e38ed7ffd0e763189a3e20d882161c6e7",
48
+ "size": 503
49
+ },
50
+ "dependencies/pubkey-policy-solana-contracts/interface/src/pubkey_policy.rs": {
51
+ "hash": "327a0e805d1c87aed14a6368fe8471853d961d5c0ed30eec1e5b31a8da084ee0",
52
+ "size": 10828
53
+ },
54
+ "dependencies/pubkey-policy-solana-contracts/interface/src/seeds.rs": {
55
+ "hash": "5cf8967100a547fbed373f7bc5935c7306fe2b59771ff2f64a9ea2e9213fa8c8",
56
+ "size": 895
57
+ },
58
+ "dependencies/pubkey-policy-solana-contracts/interface/src/state.rs": {
59
+ "hash": "f796e40fbb13f25d66f57663b6fe9804b2cf46722ca99d7ea92a04b06cf889ed",
60
+ "size": 2344
61
+ },
62
+ "dependencies/pubkey-policy-solana-contracts/interface/src/version.rs": {
63
+ "hash": "cc1e1e22dbaaf0e5888990caa3aa361b0e3534bde4a625b701efd3b0381a6d6c",
64
+ "size": 846
65
+ },
66
+ "dependencies/pubkey-policy-solana-contracts/programs/pubkey_policy/Cargo.toml": {
67
+ "hash": "3d1caa746fb84ad5721a4c8013f5d079fc1336acca12d3ad9611f273b7d67dea",
68
+ "size": 727
69
+ },
70
+ "dependencies/pubkey-policy-solana-contracts/programs/pubkey_policy/src/errors.rs": {
71
+ "hash": "19219a369dba7886701ec1c9bb05a9e37845e2a0b363c4616c3cb0602431f74f",
72
+ "size": 392
73
+ },
74
+ "dependencies/pubkey-policy-solana-contracts/programs/pubkey_policy/src/events.rs": {
75
+ "hash": "3e46edfe3bce2ca16954b08438ff97133bae42ab3dd23c05cc79f1da98e6c7d1",
76
+ "size": 583
77
+ },
78
+ "dependencies/pubkey-policy-solana-contracts/programs/pubkey_policy/src/instructions/get_version.rs": {
79
+ "hash": "ae16bc35b0fda5507e2b932b621993ec1368f7cada2273df63821ece25c75405",
80
+ "size": 321
81
+ },
82
+ "dependencies/pubkey-policy-solana-contracts/programs/pubkey_policy/src/instructions/init_pubkey_policy.rs": {
83
+ "hash": "87d6cf6f9ae5578ff4aebe8db7781f3cdc004afedaf0c79be278b65981a35fca",
84
+ "size": 1940
85
+ },
86
+ "dependencies/pubkey-policy-solana-contracts/programs/pubkey_policy/src/instructions/membership.rs": {
87
+ "hash": "96df73837f8b7e5562902ff58f1ea2a365fafa7e4c3cd04c449f5d742e4b1304",
88
+ "size": 1070
89
+ },
90
+ "dependencies/pubkey-policy-solana-contracts/programs/pubkey_policy/src/instructions/mod.rs": {
91
+ "hash": "dd7d6750234b2a69311349f638a76cbb5e3a251ff54293f358c157c3da246f35",
92
+ "size": 266
93
+ },
94
+ "dependencies/pubkey-policy-solana-contracts/programs/pubkey_policy/src/instructions/set_blacklisted.rs": {
95
+ "hash": "276324d4aca0bea3c90cafc09a81e410a675a5a0e1f392054bc7bb602a3be8e6",
96
+ "size": 1652
97
+ },
98
+ "dependencies/pubkey-policy-solana-contracts/programs/pubkey_policy/src/instructions/set_mode.rs": {
99
+ "hash": "adaa9f3a43ae021d8ef7761c4313038faaa770ac018d0841faa34f3d848c75fc",
100
+ "size": 820
101
+ },
102
+ "dependencies/pubkey-policy-solana-contracts/programs/pubkey_policy/src/instructions/set_whitelisted.rs": {
103
+ "hash": "26d64868cad2acb05d6bbcf0bba6b304a15f8b14ea17b6a7ea0181d9d36cb975",
104
+ "size": 1652
105
+ },
106
+ "dependencies/pubkey-policy-solana-contracts/programs/pubkey_policy/src/lib.rs": {
107
+ "hash": "574dec75e6cb37a45b96e02a6dc0cbe60dbb88095e477ed150c9227f7f53af75",
108
+ "size": 1848
109
+ },
110
+ "dependencies/pubkey-policy-solana-contracts/programs/pubkey_policy/src/types.rs": {
111
+ "hash": "22bbf4d42a1b7dad95554565a879a7c12c9cbcd8e8fad869dc7158cc390cc323",
112
+ "size": 650
113
+ },
114
+ "dependencies/rbac-solana-contracts/Cargo.toml": {
115
+ "hash": "5952cecde0045443aeb270137acc3a1c4d082c9bc294521e5d7f62381cb4d98c",
116
+ "size": 739
117
+ },
118
+ "dependencies/rbac-solana-contracts/macros/Cargo.toml": {
119
+ "hash": "f60b1cd9b00fe183904ad5f9b1353ba168afee15f0189392b2cd66d9203e125a",
120
+ "size": 410
121
+ },
122
+ "dependencies/rbac-solana-contracts/macros/src/init_default_admin.rs": {
123
+ "hash": "231520848042bba179fb73aa969ca5b65854941635508ba7b404202f5746f81e",
124
+ "size": 2830
125
+ },
126
+ "dependencies/rbac-solana-contracts/macros/src/instructions/accept_default_admin_transfer.rs": {
127
+ "hash": "b93649e17ad83dd06b7b514d78e69c24326e03b68108ccc014a0d4fe441098f6",
128
+ "size": 4610
129
+ },
130
+ "dependencies/rbac-solana-contracts/macros/src/instructions/begin_default_admin_transfer.rs": {
131
+ "hash": "b23e1b6f3cc315b965ca443484d00e9b15e3fd40dbdb6ce50d7d2e85435a851f",
132
+ "size": 2209
133
+ },
134
+ "dependencies/rbac-solana-contracts/macros/src/instructions/get_default_admin.rs": {
135
+ "hash": "fa6c69183befaa5edf5545174c473a854c1dee7a26a3d77c803f3b876361f9f6",
136
+ "size": 1534
137
+ },
138
+ "dependencies/rbac-solana-contracts/macros/src/instructions/grant_role.rs": {
139
+ "hash": "1ae6fb336a1649f931da42ef3450357e55a6a438fa06a10a428a61be7c034084",
140
+ "size": 4210
141
+ },
142
+ "dependencies/rbac-solana-contracts/macros/src/instructions/mod.rs": {
143
+ "hash": "768f50e7d5750770cc79b81f639f765cf202af28808afbe6e801be448eb16ca0",
144
+ "size": 882
145
+ },
146
+ "dependencies/rbac-solana-contracts/macros/src/instructions/renounce_role.rs": {
147
+ "hash": "3a443beef4bd6eb3cac0ef89c5ef941d9c05fe4d256604aa301d530701d89568",
148
+ "size": 3285
149
+ },
150
+ "dependencies/rbac-solana-contracts/macros/src/instructions/revoke_role.rs": {
151
+ "hash": "9b37dca9843fb49f854666216cca3a4731bed63949fdc5ec4340ffcf2665d1ee",
152
+ "size": 4015
153
+ },
154
+ "dependencies/rbac-solana-contracts/macros/src/lib.rs": {
155
+ "hash": "736aed9a41b41fc494ce83823e72bcce43aac891f2cb8cf4fd30c9e95fd57cbc",
156
+ "size": 6081
157
+ },
158
+ "dependencies/rbac-solana-contracts/macros/src/only_role.rs": {
159
+ "hash": "57e96f7a7642c12442122221dbd70e78b9391f1478785a875ecaa3b3f3962b3a",
160
+ "size": 1933
161
+ },
162
+ "dependencies/rbac-solana-contracts/macros/src/rbac.rs": {
163
+ "hash": "64ef4430c489db3dc1a1448924c501d184e8e71705c814be3546ed1f725c7026",
164
+ "size": 2248
165
+ },
166
+ "dependencies/rbac-solana-contracts/macros/src/role_type.rs": {
167
+ "hash": "d721b4b7c4a7874bde4eb9288405448adf93bef4bcd65ff58a3a6a02555871c0",
168
+ "size": 1946
169
+ },
170
+ "dependencies/rbac-solana-contracts/macros/src/setup_default_admin.rs": {
171
+ "hash": "f57017eec0d662d9eb940b46b9f6447b97b7c2bf9323c05c789c32de04981be1",
172
+ "size": 4204
173
+ },
174
+ "dependencies/rbac-solana-contracts/macros/src/state_types.rs": {
175
+ "hash": "7a8eb86ef0d1551e2b4cf8e6b6d07cfdc90efd85a69a9ef84ffc910a4a6ee4f8",
176
+ "size": 3764
177
+ },
178
+ "dependencies/rbac-solana-contracts/macros/src/utils.rs": {
179
+ "hash": "c0853f52de9ff03c456987700eb19ffd2759361e4930b2fa0d4e43b095df11bb",
180
+ "size": 2093
181
+ },
182
+ "dependencies/rbac-solana-contracts/src/errors.rs": {
183
+ "hash": "4cb21ae9175155c5d8192db1f7d9e5841c0a6a8dbb06e05173794a655ff71c79",
184
+ "size": 1737
185
+ },
186
+ "dependencies/rbac-solana-contracts/src/events.rs": {
187
+ "hash": "748a6421feac3decd612b83b0e4431040b3d4d760c3dfb285fe06528d538177b",
188
+ "size": 1497
189
+ },
190
+ "dependencies/rbac-solana-contracts/src/lib.rs": {
191
+ "hash": "66ddc73dc314a30d5083795653acefc6a4d3ae9277933dd428f573393c024a1d",
192
+ "size": 3122
193
+ },
194
+ "dependencies/rbac-solana-contracts/src/traits.rs": {
195
+ "hash": "5261d024f8eec774c09f673dbc738e3f7f7c3c528c365f7ef40d3b49014b4292",
196
+ "size": 2980
197
+ },
198
+ "dependencies/rbac-solana-contracts/src/types.rs": {
199
+ "hash": "cda8f74904eb546f4d9fc8ded80900e2b33b550ec9cdba800216f92972ab1295",
200
+ "size": 966
201
+ },
202
+ "target/deploy/pubkey_policy_hook.so": {
203
+ "hash": "36066e41bc94ada17546993db302943713f65c0db5d8cfc60ec15fa4e702382a",
204
+ "size": 180848
205
+ },
206
+ "target/idl/pubkey_policy_hook.json": {
207
+ "hash": "d9101eb70c8dcf4a8a9c3cea7040df09c78b8cf76f0e54cce750c2e794650b37",
208
+ "size": 4719
209
+ },
210
+ "target/types/pubkey_policy_hook.ts": {
211
+ "hash": "0449cf898a7be975338e82dca086476e7c4fde71dcce28f7255ee9321e27186e",
212
+ "size": 4951
213
+ },
214
+ "target/types/pubkey_policy_hook_errors.ts": {
215
+ "hash": "6fc6fae620c6ee9f4256593877ba1804d4dda58e35f3a6ac1185a53dd30672a6",
216
+ "size": 300
217
+ }
218
+ }
package/turbo.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "extends": ["//"],
3
+ "tasks": {
4
+ "build": {
5
+ "inputs": [
6
+ "$TURBO_DEFAULT$",
7
+ "!**/*.md",
8
+ "!**/LICENSE",
9
+ "!programs/**/tests/**",
10
+ "!tests/**/*.rs",
11
+ "!turbo-snapshot.lock.json"
12
+ ],
13
+ "outputs": ["dependencies/**", "target/deploy/*.so", "target/idl/**", "target/types/**"]
14
+ },
15
+ "test": {
16
+ "dependsOn": ["build", "lint"]
17
+ }
18
+ }
19
+ }