Status: Implemented
Solana's SPL Token delegate model allows only one delegate per token account. This creates friction for:
- P2P delegations where users want to authorize friends/services to spend on their behalf
- Multiple simultaneous payment authorizations from a single token account
- Need for controlled, recurring payments with enforceable limits
We implement a single-track delegation model that provides:
- SubscriptionAuthority Authority (SA): A programmatic delegate with unlimited token approval authority (
u64::MAX) over user token accounts - Delegation PDAs: Individual constraints governing SA spending behavior
- Delegation Types: Fixed (one-time with expiry) and Recurring (periodic pulls with limits)
- Tech Stack: Pinocchio framework, Codama for IDL generation and TypeScript/Rust client generation
Key Design: SA receives unlimited approval, but can only transfer when Delegation PDA constraints allow. The program validates constraints before executing transfers, making the system as secure as traditional approval-based delegations while enabling subscription-authority capabilities.
At a high level, Alice will first create a SubscriptionAuthorityPDA which she will give the power to transfer tokens on her behalf.
After this, she will be able to create Delegations of different kinds to different users.
Once she creates a Delegation for Bob, he will be able to perform transfers through the program. The subscription_authority program will perform the relevant checks depending on the type of delegation between Alice and Bob.
graph TB
B[Bob] -->|transfer_fixed| FD
A[Alice] -->|initialize_subscription_authority| MD[SubscriptionAuthority PDA]
A -->|create_delegation | FD[Delegation]
FD -->|transfer_fixed| MD
MD -->|transfer| TP[Token Program]
TP -->|Transfers from Alice's ATA to Bob| B
sequenceDiagram
participant U as User (Alice)
participant P as Program
participant S as SVM
participant T as Token Program
U->>P: initialize_subscription_authority(user, mint, user_ata)
Note over P: Validate PDA derived from<br/>["SubscriptionAuthority", user, mint]
P->>S: Create SA account
S->>P: Account created
P->>T: Approve(user_ata, SA, u64::MAX)
T->>P: Delegation approved
P->>U: SA ready for delegations
sequenceDiagram
participant A as Alice (Delegator)
participant P as Program
participant S as Sponsor (Optional Payer)
participant B as Bob
Note over A,S: If sponsor provided, they pay for delegation rent
A->>P: create_fixed_delegation(bob, nonce, amount, expiry_ts[, sponsor])
Note over P: Validate SA exists<br/>Alice is delegator
P->>P: Derive Delegation PDA<br/>["delegation", SA, alice, bob, nonce]
Note over P: Create FixedDelegation PDA<br/>Store payer (Alice or sponsor)
P->>A: Delegation created
Note over A,B: Bob can now spend up to `amount`<br/>until `expiry_ts`
sequenceDiagram
participant A as Alice (Delegator)
participant P as Program
participant S as Sponsor (Optional Payer)
participant B as Bob
Note over A,S: If sponsor provided, they pay for delegation rent
A->>P: create_recurring_delegation(bob, nonce, amount_per_period, period_length_s, start_ts, expiry_ts[, sponsor])
Note over P: Validate SA exists<br/>Alice is delegator
P->>P: Derive Delegation PDA<br/>["delegation", SA, alice, bob, nonce]
Note over P: Create RecurringDelegation PDA<br/>Store payer (Alice or sponsor)
P->>A: Delegation created
Note over A,B: Bob can pull `amount_per_period`<br/>every `period_length_s` seconds until `expiry_ts`
sequenceDiagram
participant B as Bob
participant P as Program
participant SA as SA
participant T as Token Program
B->>P: transfer_fixed(delegation_pda, amount)
Note over P: Load FixedDelegation
alt Not expired and amount OK
P->>SA: Delegate authority check
P->>T: Transfer from Alice's ATA<br/>to Bob's ATA (amount)
T->>B: Tokens transferred
P->>P: Deduct from delegation.amount
else Expired or too much
P->>B: Error (expired or exceeds)
end
sequenceDiagram
participant A as Alice (Delegator)
participant P as Program
participant S as Sponsor (if payer != delegator)
alt Delegator revokes (any time)
A->>P: revoke_delegation(delegation_pda[, sponsor_receiver])
P->>P: Close PDA, rent to original payer
else Sponsor revokes (only after expiry)
S->>P: revoke_delegation(delegation_pda)
P->>P: Close PDA, rent to sponsor
end
Note: Sponsors of delegations with
expiry_ts == 0(no expiry) cannot independently reclaim rent.
Each user creates one SA per token mint with seeds ["SubscriptionAuthority", user, mint]. The SA:
- Receives
u64::MAXdelegated approval from the user's ATA - Acts as the delegate for all transfers from that user's account
- Cannot transfer on its own - requires active Delegation PDA to authorize
Delegates discover their delegations via getProgramAccounts with memcmp filter on the delegatee field at byte offset 35 (DELEGATEE_OFFSET):
// Delegatee discovers Bob's delegations:
getProgramAccounts(PROGRAM_ID, {
filters: [{ memcmp: { offset: DELEGATEE_OFFSET, bytes: bobPubkey } }],
});| Instruction | Actor | Purpose |
|---|---|---|
initialize_subscription_authority |
Delegator | Create SA and approve u64::MAX delegate authority |
close_subscription_authority |
Delegator | Close SA account and return rent |
| Instruction | Actor | Purpose |
|---|---|---|
create_fixed_delegation |
Delegator | Create one-time delegation with nonce, amount, and expiry (payer can be sponsor) |
create_recurring_delegation |
Delegator | Create recurring delegation with period limits (payer can be sponsor) |
revoke_delegation |
Delegator / Sponsor | Close a delegation account and return rent to the original payer. Sponsor can only revoke after expiry. |
| Instruction | Actor | Purpose |
|---|---|---|
transfer_fixed |
Delegatee | Execute token transfer for a fixed delegation, enforcing limits |
transfer_recurring |
Delegatee | Execute token transfer for a recurring delegation, enforcing period limits |
#[repr(u8)]
pub enum AccountDiscriminator {
SubscriptionAuthority = 0,
Plan = 1,
FixedDelegation = 2,
RecurringDelegation = 3,
SubscriptionDelegation = 4,
}The SubscriptionAuthority PDA stores the delegator and mint information:
#[repr(C, packed)]
pub struct SubscriptionAuthority {
pub discriminator: u8, // 1 byte - AccountDiscriminator::SubscriptionAuthority
pub user: Address, // 32 bytes - delegator key
pub token_mint: Address, // 32 bytes - mint this SA controls
pub bump: u8, // 1 byte
pub init_id: i64, // 8 bytes - slot-based generation identifier
}
impl SubscriptionAuthority {
pub const SEED: &[u8] = b"SubscriptionAuthority";
pub const LEN: usize = 74;
pub fn find_pda(user: &Address, token_mint: &Address) -> (Address, u8) {
Address::find_program_address(
&[Self::SEED, user.as_ref(), token_mint.as_ref()],
&crate::ID,
)
}
}PDA seeds: ["SubscriptionAuthority", delegator_key, mint_key]
Note (init_id): The
init_idfield is set fromClock::slotwhen the account is created. Every delegation header stores a copy of this value. On transfer, the program validatesheader.init_id == subscription_authority.init_id. If a user closes and re-initializes their SubscriptionAuthority, the new slot produces a differentinit_id, making all old delegations non-transferable (error:StaleSubscriptionAuthority). This prevents orphaned delegations from being revived and also makes closing an effective emergency kill switch.
Shared header for all delegation types (FixedDelegation, RecurringDelegation, SubscriptionDelegation):
#[repr(C, packed)]
pub struct Header {
pub discriminator: u8, // 1 byte - AccountDiscriminator variant
pub version: u8, // 1 byte - account format version (see ADR-003)
pub bump: u8, // 1 byte - PDA bump seed
pub delegator: Address, // 32 bytes - user granting delegation
pub delegatee: Address, // 32 bytes - beneficiary
pub payer: Address, // 32 bytes - who paid for the delegation account
pub init_id: i64, // 8 bytes - copied from SubscriptionAuthority.init_id at creation
}
impl Header {
pub const LEN: usize = 107;
}Field offsets are defined as standalone constants in state/header.rs:
DISCRIMINATOR_OFFSET = 0VERSION_OFFSET = 1BUMP_OFFSET = 2DELEGATOR_OFFSET = 3DELEGATEE_OFFSET = 35PAYER_OFFSET = 67INIT_ID_OFFSET = 99
One-time delegation with explicit amount and expiry:
#[repr(C, packed)]
pub struct FixedDelegation {
pub header: Header, // 107 bytes
pub amount: u64, // 8 bytes - remaining pullable amount
pub expiry_ts: i64, // 8 bytes - Unix timestamp (0 = no expiry)
}
impl FixedDelegation {
pub const LEN: usize = 123;
}PDA seeds: ["delegation", subscription_authority, delegator, delegatee, nonce]
Use cases: One-time payments, time-limited allowances, gift delegations
Recurring delegation with period tracking:
#[repr(C, packed)]
pub struct RecurringDelegation {
pub header: Header, // 107 bytes
pub current_period_start_ts: i64, // 8 bytes - start of current period
pub period_length_s: u64, // 8 bytes - seconds per period
pub expiry_ts: i64, // 8 bytes - delegation expiry (0 = no expiry)
pub amount_per_period: u64, // 8 bytes - max per period
pub amount_pulled_in_period: u64, // 8 bytes - tracking
}
impl RecurringDelegation {
pub const LEN: usize = 147;
}PDA seeds: Same as FixedDelegation
Use cases: Subscription payments, recurring allowances, salary-style disbursements
Creates the SA and grants it u64::MAX delegated approval over the user's ATA.
| Account | Type | Description |
|---|---|---|
| 0 | signer, writable | The delegator (user) |
| 1 | writable | SubscriptionAuthority PDA to create |
| 2 | mint | Token mint for this SA |
| 3 | writable | User's ATA to approve |
| 4 | system_program | System program |
| 5 | token_program | Token program |
Process:
- Validate SA PDA address derived from
["SubscriptionAuthority", user, mint] - Create SA account with delegator and mint data
- Call
Approve { source: user_ata, delegate: subscription_authority, authority: user, amount: u64::MAX }
Creates a one-time delegation with nonce-based PDA.
| Account | Type | Description |
|---|---|---|
| 0 | signer, writable | The delegator creating this delegation |
| 1 | SubscriptionAuthority PDA for this token type | |
| 2 | writable | FixedDelegation PDA being created |
| 3 | The delegatee (beneficiary) | |
| 4 | system_program | System program |
| 5 | signer, writable | The payer who funds the delegation account (optional, defaults to delegator) |
Parameters:
nonce: u64- Unique identifier to create distinct PDAs for same (delegator, delegatee) pairamount: u64- Maximum amount transferableexpiry_ts: i64- Unix timestamp when delegation expires
Process:
- Validate SubscriptionAuthority exists and belongs to delegator
- Derive and validate Delegation PDA from
["delegation", subscription_authority, delegator, delegatee, nonce] - Create Delegation account with header and terms
Creates a recurring delegation with period tracking.
| Account | Type | Description |
|---|---|---|
| 0 | signer, writable | The delegator creating this delegation |
| 1 | SubscriptionAuthority PDA for this token type | |
| 2 | writable | RecurringDelegation PDA being created |
| 3 | The delegatee (beneficiary) | |
| 4 | system_program | System program |
| 5 | signer, writable | The payer who funds the delegation account (optional, defaults to delegator) |
Parameters:
nonce: u64- Unique identifieramount_per_period: u64- Maximum amount per periodperiod_length_s: u64- Seconds in each periodstart_ts: i64- Timestamp when the first period startsexpiry_ts: i64- Delegation expiry timestamp
Process:
- Validate SubscriptionAuthority exists and belongs to delegator
- Derive and validate Delegation PDA with nonce
- Create Delegation account with header and terms
- Initialize
current_period_start_tstostart_ts - Initialize
amount_pulled_in_periodto 0
Revokes a delegation by closing the delegation PDA and returning rent to the original payer.
| Account | Type | Description |
|---|---|---|
| 0 | signer, writable | The delegator or sponsor (authority) |
| 1 | writable | Delegation PDA to close |
| 2 | writable | Receiver account (required only when delegator revokes a sponsor-funded delegation) |
Process:
- Authorize caller: must be the
delegatoror thepayer(sponsor). Sponsor requiresexpiry_ts != 0 && expiry_ts < current_ts. - Close the delegation account and return rent to the original payer.
Closes a SubscriptionAuthority PDA and returns rent to the owner.
| Account | Type | Description |
|---|---|---|
| 0 | signer, writable | The user who owns the SubscriptionAuthority PDA |
| 1 | writable | SubscriptionAuthority PDA to close |
Process:
- Verify signer matches the SA's
userfield - Verify PDA derivation from
["SubscriptionAuthority", user, token_mint] - Close account and transfer lamports to user
Emergency kill switch: Closing does not revoke existing delegation PDAs, but they become non-transferable because the SubscriptionAuthority account no longer exists. If the user re-initializes, the new
init_idinvalidates all old delegations. This allows a user to immediately cut off all delegatees in a single transaction without revoking each one individually. The delegator can still callrevoke_delegationon orphaned delegations afterward to reclaim rent.
The transfer mechanism uses specific instructions for each delegation type to validate constraints before allowing the SA to transfer tokens.
Executes a transfer for a fixed delegation.
| Account | Type | Description |
|---|---|---|
| 0 | writable | FixedDelegation PDA |
| 1 | SubscriptionAuthority PDA | |
| 2 | writable | Delegator's ATA |
| 3 | writable | Receiver's ATA |
| 4 | Token Program | |
| 5 | signer | Delegatee (beneficiary) |
| 6 | Event authority PDA | |
| 7 | This program (for self-CPI event emission) |
Parameters (in instruction data):
amount: u64- Amount to transferdelegator: Address- The delegator's public key (for verification)mint: Address- The token mint (for verification)
Process:
- Validate delegation discriminator is
FixedDelegation - Verify signer is authorized delegatee
- Check expiry and amount limits
- Deduct amount from delegation
- Execute transfer via SubscriptionAuthority
- Emit
FixedTransferEventvia self-CPI
Executes a transfer for a recurring delegation.
| Account | Type | Description |
|---|---|---|
| 0 | writable | RecurringDelegation PDA |
| 1 | SubscriptionAuthority PDA | |
| 2 | writable | Delegator's ATA |
| 3 | writable | Receiver's ATA |
| 4 | Token Program | |
| 5 | signer | Delegatee (beneficiary) |
| 6 | Event authority PDA | |
| 7 | This program (for self-CPI event emission) |
Parameters (in instruction data):
amount: u64- Amount to transferdelegator: Address- The delegator's public keymint: Address- The token mint
Process:
- Validate delegation discriminator is
RecurringDelegation - Verify signer is authorized delegatee
- Check expiry
- Update period logic (reset if new period)
- Check period limits
- Update tracking
- Execute transfer via SubscriptionAuthority
- Emit
RecurringTransferEventvia self-CPI