A griefing pre-fund does not block subscription authority initialization
Source: subscriptions/tests/audit_high_3_1_3.rs L70
- When: the user initializes a subscription authority
- Then: the pre-funded PDA does not block creation (the fix holds) ✓
- Then: the subscription authority decodes to a struct ✓
Result: ✓ success — 10077 CU · claims 2/2 ✓
sequenceDiagram
participant p0 as alice
participant p1 as Subscriptions
participant p2 as system
participant p3 as token
p0->>p1: initSubscriptionAuthority
activate p1
p1->>p2: transferSol
activate p2
p2-->>p1: ✓
deactivate p2
p1->>p2: allocate
activate p2
p2-->>p1: ✓
deactivate p2
p1->>p2: assign
activate p2
p2-->>p1: ✓
deactivate p2
p1->>p3: approve
activate p3
p3-->>p1: ✓ 126cu
deactivate p3
p1-->>p0: ✓ 10077cu
deactivate p1
Authority
flowchart LR
Subscriptions["Subscriptions"]:::program
alice(["alice"]):::signer
SAaliceMint(["SA(alice, Mint)"]):::signer
aliceATAMint[("alice/ATA(Mint)")]:::state
system["system"]:::program
token["token"]:::program
alice -->|signs| Subscriptions
Subscriptions -->|writes| SAaliceMint
Subscriptions -->|writes| aliceATAMint
alice -->|signs| system
system -->|writes| SAaliceMint
SAaliceMint -->|signs| system
token -->|writes| aliceATAMint
alice -->|signs| token
classDef program fill:#dae8fc,stroke:#6c8ebf;
classDef signer fill:#d5e8d4,stroke:#82b366;
classDef state fill:#ffe6cc,stroke:#d79b00;
Ownership
flowchart LR
system["system"]:::program
alice[("alice")]:::state
Subscriptions["Subscriptions"]:::program
SAaliceMint[("SA(alice, Mint)")]:::state
token["token"]:::program
aliceATAMint[("alice/ATA(Mint)")]:::state
system -->|owns| alice
Subscriptions -->|owns| SAaliceMint
token -->|owns| aliceATAMint
classDef program fill:#dae8fc,stroke:#6c8ebf;
classDef signer fill:#d5e8d4,stroke:#82b366;
classDef state fill:#ffe6cc,stroke:#d79b00;
Accounts
| Account | Signer | Writable | Owner |
|---|---|---|---|
| alice | ✓ | ✓ | system |
| SA(alice, Mint) | · | ✓ | Subscriptions |
| Mint | · | · | token |
| alice/ATA(Mint) | · | ✓ | token |
| system | · | · | Nati… |
| token | · | · | BPFL… |
Call tree and logs
A griefing pre-fund does not block subscription authority initialization
alice (10077cu)
└─ Subscriptions::initSubscriptionAuthority ✓ 10077cu
├─ system::transferSol ✓
├─ system::allocate ✓
├─ system::assign ✓
└─ token::approve ✓ 126cu
Program De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44 invoke [1]
Program 11111111111111111111111111111111 invoke [2]
Program 11111111111111111111111111111111 success
Program 11111111111111111111111111111111 invoke [2]
Program 11111111111111111111111111111111 success
Program 11111111111111111111111111111111 invoke [2]
Program 11111111111111111111111111111111 success
Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]
Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 126 of 190066 compute units
Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success
Program De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44 consumed 10077 of 200000 compute units
Program De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44 success
Integration
What an integrator writes: the derivations and the instruction, byte-identical to the transaction this page witnessed. Addresses are this scenario’s; supply your own.
#![allow(unused)]
fn main() {
use solana_instruction::{AccountMeta, Instruction};
use solana_pubkey::{pubkey, Pubkey};
// --- initSubscriptionAuthority ---
let program_id: Pubkey = pubkey!("De1egAFMkMWZSN5rYXRj9CAdheBamobVNubTsi9avR44");
let owner: Pubkey = pubkey!("2rj1EEgg32bupe12mMYnPFaKQCPPyYBhKYmDFZ8cXxpT"); // alice: this scenario's value; supply your own
let token_mint: Pubkey = pubkey!("HhbhtPWNt5Rd2gvrjhGCekommdNQwVLY6ZMp3aBv4xtG"); // Mint: this scenario's value; supply your own
let user_ata: Pubkey = pubkey!("3fVRopUs1BsF6RqteSmcyctBCx2YQ6Zkv7prsUz95VHY"); // supply your own
let system_program: Pubkey = pubkey!("11111111111111111111111111111111"); // the system program
let token_program: Pubkey = pubkey!("TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA"); // the token program
let subscription_authority = Pubkey::find_program_address(&[b"SubscriptionAuthority", owner.as_ref(), token_mint.as_ref()], &program_id).0;
let ix = Instruction {
program_id,
accounts: vec![
AccountMeta::new(owner, true), // owner
AccountMeta::new(subscription_authority, false), // subscriptionAuthority
AccountMeta::new_readonly(token_mint, false), // tokenMint
AccountMeta::new(user_ata, false), // userAta
AccountMeta::new_readonly(system_program, false), // systemProgram
AccountMeta::new_readonly(token_program, false), // tokenProgram
],
// discriminator ++ borsh()
data: vec![0x00],
};
}
The same call through the Codama-generated TypeScript client (the async builder derives every PDA the resolver derived here, so only the free inputs appear):
import { address } from "@solana/kit";
import { getInitSubscriptionAuthorityInstructionAsync } from "your-generated-client";
const ix = await getInitSubscriptionAuthorityInstructionAsync({
owner: ownerSigner, // alice (a TransactionSigner)
tokenMint: address("HhbhtPWNt5Rd2gvrjhGCekommdNQwVLY6ZMp3aBv4xtG"), // Mint
userAta: address("3fVRopUs1BsF6RqteSmcyctBCx2YQ6Zkv7prsUz95VHY"),
// systemProgram defaults to the system program
// tokenProgram defaults to the token program
});
How this page verifies itself: the test that ran it
The narrative above is this test; the page and the code cannot drift.
#![allow(unused)]
fn main() {
#[test]
fn finding_3_1_3_prefunded_pda_does_not_block_creation() {
let mut story = Story::load(SO, IDL);
let alice = story.cast("alice");
let _mallory = story.cast("mallory"); // the griefer is off-chain; the pre-fund is the attack
let mint = story.mint(&alice, MINT_DECIMALS);
story.fund(&alice, &[(mint, 1_000_000)]);
let pda = story.subscription_authority_pda(&alice.pubkey(), &mint);
// The attack: pre-fund Alice's authority PDA address with lamports, no data,
// owned by the System program (Pubkey::default() is the all-zero System id).
story.set_account(&pda, Pubkey::default(), 1_000_000, vec![]);
// On an unconditional CreateAccount this is rejected by the System program;
// the fixed program tops up the rent and Allocate/Assign-s in place.
let outcome = story.init_authority(&alice, mint);
story.then_holds(
"the pre-funded PDA does not block creation (the fix holds)",
outcome.success,
);
// The authority account now exists and decodes against its named type.
story.then("the subscription authority decodes to a struct", move |s| {
matches!(
s.account_as("subscriptionAuthority", &pda),
Value::Struct(_)
)
});
emit_scenario(
&story,
&outcome,
"A griefing pre-fund does not block subscription authority initialization",
);
}
}