Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"protocol/program-walkthrough",
"protocol/signing-and-execution",
"protocol/sessions-and-subaccounts",
"protocol/authorization-locks",
"protocol/recovery-model"
]
},
Expand Down Expand Up @@ -159,6 +160,7 @@
"examples/secp256r1_authority",
"examples/sessions",
"examples/sub_accounts",
"examples/authorization_locks",
"examples/v2_features",
"examples/multi_authority",
"examples/migrating-from-squads"
Expand Down
193 changes: 193 additions & 0 deletions examples/authorization_locks.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
---
title: "Authorization Locks"
description: "Reserve wallet funds for a delegated role, then capture or release them"
---

This recipe gives a card-processor role the ability to place holds on a wallet's
funds, the way a card network authorizes a payment before settling it. Read
[Authorization Locks](/protocol/authorization-locks) for the full rules.

The flow:

1. The owner adds a delegated role with `ManageAuthorizationLocks` and a
spending limit.
2. The delegate locks part of the wallet's balance.
3. The delegate captures the hold by releasing it and spending in one
transaction, or releases it without spending. Otherwise the hold expires.

## Rust SDK

### Grant the delegated role

The delegate can lock any asset it has a spending limit for, up to that limit,

@edo-chan edo-chan Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please describe the delegated cap as the remaining spending allowance after its existing active holds on the same asset, and the All/AllButManageAuthority cap as this asset account's unreserved, lockable balance. For native SOL, exclude the rent-exempt minimum. Apply the same wording in concepts-and-permissions; 'up to that limit' and 'the wallet’s balance' currently overstate what a new lock can reserve.

and only for up to `max_duration_slots`. It cannot change its own
`max_duration_slots`.

A role with `All` or `AllButManageAuthority` also needs
`ManageAuthorizationLocks` to lock. It can then lock up to the wallet's balance,
still within its `max_duration_slots`. To let the root role lock, have it grant

@edo-chan edo-chan Oct 29, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please state the root-role exception consistently here and in concepts-and-permissions: a non-root role cannot set or change its own max_duration_slots, while the root role can set or change its own bound. The protocol page already explains this exception.

itself the permission:

```rust
use swig_sdk::types::UpdateAuthorityData;

wallet.update_authority(
0,
UpdateAuthorityData::AddActions(vec![Permission::ManageAuthorizationLocks {
max_duration_slots: 216_000,
}]),
)?;
```

```rust
use swig_sdk::{authority::AuthorityType, Permission};

wallet.add_authority(
AuthorityType::Ed25519,
card_authority.pubkey().as_ref(),
vec![
// Holds may last at most ~1 day at 400ms slots (less as slot time drops).
Permission::ManageAuthorizationLocks {
max_duration_slots: 216_000,
},
// Backs the role's SOL holds and spending.
Permission::Sol {
amount: 2_000_000_000,
recurring: None,
},
],
)?;
```

### Place a hold

With the wallet switched to the delegate's role:

```rust
let slot = wallet.get_current_slot()?;

// Hold 1.5 SOL for the next 400 slots.
wallet.add_sol_authorization_lock(1_500_000_000, slot + 400)?;

// Hold tokens in the wallet's associated token account for `mint`.
// SPL Token and Token-2022 mints are both supported.
wallet.add_token_authorization_lock(&mint, 600, slot + 400)?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please grant a token spending limit for mint before this call, and include the equivalent permission in the CLI example. Both grant examples currently give the delegate only a SOL limit, so the subsequent token lock fails with AuthorizationLockExceedsSpendLimit.

```

For a token account that is not the wallet's ATA, call
`add_authorization_lock(asset_account, mint.to_bytes(), amount, expiry_slot)`
directly. For native SOL, pass the wallet address and an all-zero mint.

### Read holds

```rust
// Every stored lock, including expired locks that have not been pruned.
let all = wallet.get_authorization_locks()?;

// Only locks that still bind at the current slot.
let active = wallet.get_active_authorization_locks()?;
for lock in active {
println!(
"lock {} reserves {} until slot {} (role {})",
lock.lock_id, lock.amount, lock.expiry_slot, lock.role_id
);
}
```

`display_swig()` also prints an **AUTHORIZATION LOCKS** section and shows each
role's `ManageAuthorizationLocks` bound.

### Release or prune a hold

```rust
wallet.remove_authorization_lock(lock_id)?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[NIT] Could the recipe show where lock_id comes from? The create helpers return only a transaction signature, so the release and capture snippets never establish the identifier. Once the SDK returns a transaction-correlated ID, carry it through this flow.

```

The creating role can release its lock at any time. After a lock expires, any
role with `All`, `AllButManageAuthority`, `ManageAuthority`, or
`ManageAuthorizationLocks` can prune it.

### Capture a hold

To capture, release the lock and spend the funds in the same transaction. Build
both instructions with the delegate's `SwigInstructionBuilder`:

```rust
use solana_system_interface::instruction::transfer;
use swig_sdk::{Ed25519ClientRole, SwigInstructionBuilder};

let mut builder = SwigInstructionBuilder::new(
swig_id,
Box::new(Ed25519ClientRole::new(card_authority.pubkey())),
fee_payer.pubkey(),
card_role_id,
);
let slot = rpc_client.get_slot()?;

// Pass the wallet's rent claimer here if one is configured.
let mut instructions = builder.remove_authorization_lock(lock_id, None, Some(slot))?;
instructions.extend(builder.sign_v2_instruction(
vec![transfer(&wallet_address, &merchant, 1_500_000_000)],
Some(slot),
)?);
// Sign with the fee payer and the card authority, then send.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[NIT] Could this recipe include the complete signed-and-sent transaction for capture? Release and SignV2 must execute in one transaction, but the example stops after assembling instructions, so readers cannot run the central flow as shown.

```

If the spend is larger than the released hold and would dip into another
active hold, the whole transaction fails with `AuthorizationLockViolated`.

## CLI

The `swig` CLI has the same operations. Each command also takes
`--authority-type`, `--authority`, and `--authority-kp`, or falls back to the
saved default authority.

Grant the delegated role. Repeat `--permissions` once per permission:

```bash
swig add-authority --swig-id <ID> \
--new-authority <CARD_PUBKEY> --new-authority-type Ed25519 \
--permissions '{"type":"manageAuthorizationLocks","maxDurationSlots":216000}' \
--permissions '{"type":"sol","amount":2000000000}'
```

Place, list, and release holds as the delegate:

```bash
# Hold 1.5 SOL for 400 slots (or use --expiry-slot <SLOT>)
swig add-authorization-lock --swig-id <ID> --amount 1500000000 --duration-slots 400

# Hold tokens in the wallet's ATA for a mint
swig add-authorization-lock --swig-id <ID> --mint <MINT> --amount 600 --duration-slots 400

# List active holds; add --all to include expired, unpruned holds
swig list-authorization-locks --swig-id <ID>

# Release a hold, or prune an expired one
swig remove-authorization-lock --swig-id <ID> --lock-id <LOCK_ID>
```

Revoke the delegate's ability to place new holds. This is allowed even while it
holds active locks:

```bash
swig update-authority --swig-id <ID> --authority-to-update-id <ROLE_ID> \
--operation RemoveActionsByType --action-types ManageAuthorizationLocks
```

In interactive mode (`swig -i`), use the **Authorization Locks** menu.

## Common errors

| Error | Cause |
|-------|-------|
| `PermissionDeniedMissingPermission` | the role lacks `ManageAuthorizationLocks`, even if it has `All` |
| `InvalidAuthorizationLockExpiry` | expiry is not in the future, or exceeds the role's `max_duration_slots` |
| `AuthorizationLockDurationSelfUpdate` | a role tried to set or change its own `max_duration_slots` |
| `AuthorizationLockExceedsSpendLimit` | the role's holds on this asset would exceed its spending limit |
| `AuthorizationLockExceedsBalance` | active holds on the account would exceed its balance |
| `AuthorizationLockNotOwner` | another role tried to release an active hold |
| `AuthorizationLockActive` | the operation (remove role, close wallet, ...) is blocked by an active hold |
| `AuthorizationLockViolated` | a transaction would spend a locked account below its reserved amount |

See [Error Types](/reference/error-types) for codes.
127 changes: 127 additions & 0 deletions protocol/authorization-locks.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
---
title: "Authorization Locks"
sidebarTitle: "Authorization Locks"
---

An authorization lock reserves part of one wallet asset until an expiry slot. It
works like a card authorization hold: a role reserves funds now and captures
them later, and nothing else in the wallet can spend those funds in the
meantime.

Locks are stored on the Swig config account and are enforced on every
`SignV2` execution, for every role, including `All`.

## What a lock covers

Each lock records:

| Field | Meaning |
|-------|---------|
| `lock_id` | sequential id, unique within the wallet |
| `role_id` | the role that created the lock |
| `asset_account` | the wallet address (native SOL) or one token account owned by it |
| `mint` | the token mint, or all zeros for native SOL |
| `amount` | the reserved amount, in lamports or token base units |
| `expiry_slot` | the slot at which the lock stops binding |

A lock is active while `current_slot < expiry_slot`. Expired locks no longer
bind anything, but they stay stored until someone prunes them. A wallet can
store at most 32 locks, expired ones included.

## How locks are enforced

While a lock is active:

- No transaction may leave the locked account below
`min(balance_before, reserved)`, where `reserved` is the sum of active locks
on that account. The account can still receive funds, and anything above the
reserved amount can still be spent.
- A locked token account must keep the wallet as its authority, with no
delegate and no foreign close authority. A mint's freeze authority can still
freeze or thaw the account.
- The wallet cannot be closed with `CloseSwigV1`, and a locked token account
cannot be closed with `CloseTokenAccountV1`.

Native SOL locks only count lamports above the wallet address's rent-exempt
minimum. Token locks need an initialized token account. Token-2022 mints with a
permanent delegate are rejected, because a permanent delegate could move the
tokens without the wallet's signature.

## Who can create a lock

`AddAuthorizationLockV1` creates a lock. Each lock carries its own amount and
expiry. The acting role must hold `ManageAuthorizationLocks
{ max_duration_slots }`. `All` alone is not enough.

- **Duration (every role):** `expiry_slot - current_slot` must be at most the
role's `max_duration_slots`.
- **Amount, for roles that can spend everything:** a role that also holds
`All` or `AllButManageAuthority` can lock up to the account's unreserved
balance.
- **Amount, for every other role:** the role can lock any SOL or token asset,
but its active locks on that asset, including the new one, must fit within
what its own spending limits for the asset still allow. For example, a role
with `ManageAuthorizationLocks` and limits on SOL, USDC and USDT can lock each
of those up to its limit, and cannot lock an asset it has no limit for.

A role cannot set or change its own `max_duration_slots`. Another role with
`ManageAuthority` (or `All`) grants or changes it, and a role may always drop
the permission from itself. The root role is the exception: no other role can
manage it, so it sets its own bound.

A limited role's spending limits keep backing its locks. After every `SignV2` the
role's remaining allowance must still cover its active locks, so a role cannot
spend the allowance that backs its own reservation.

The payer funds the config account's rent growth for each new lock.

## Capturing, releasing, and pruning

`RemoveAuthorizationLockV1` deletes a lock.

- **Release:** the creating role can remove its own lock at any time. To
capture a reservation, put the release and the spend in the same
transaction: release the lock, then execute the transfer through `SignV2`.
- **Prune:** after a lock expires, any role holding `All`,
`AllButManageAuthority`, `ManageAuthority`, or `ManageAuthorizationLocks`
can remove it to reclaim space.
- No other role can remove an active lock. This includes the root `All` role.

Rent freed by removal goes to the configured
[rent claimer](/developer-sdk/rent-claimers) if one is set, otherwise to the
payer.

## Effect on role management

A lock is only useful if its creator stays able to capture it. While a role
holds an active lock:

- it cannot be removed with `RemoveAuthorityV1`
- another role cannot replace its authority with `ReplaceAuthorityV1`
- another role cannot update its actions with `UpdateAuthorityV1`, except to
remove only `ManageAuthorizationLocks`

Removing `ManageAuthorizationLocks` stops the role from creating new locks.
Existing locks still run to expiry, and the creator can still release them. A
delegate therefore cannot make itself irrevocable by continually renewing locks.

Because every lock is bounded by its creator's `max_duration_slots`, the
longest a role can be pinned is the duration someone else granted it.

<Note>
An existing wallet's root role cannot lock until it grants itself

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[NIT] Could this existing-wallet note also say locks require a V2 wallet and link the migration path? Both add and remove handlers call require_swig_v2, so granting the permission alone is insufficient for a legacy wallet.

`ManageAuthorizationLocks`, for example with `UpdateAuthorityV1` `AddActions`
on role 0.
</Note>

## Durations are measured in slots

`max_duration_slots` and `expiry_slot` count slots, not seconds. Slot time is
dropping toward 200ms, so a duration sized for 400ms slots ends sooner in
wall-clock time than intended. See
[time-based limits](/protocol/concepts-and-permissions#time-based-limits-are-measured-in-slots).

## Use it from the SDK and CLI

See the [authorization locks recipe](/examples/authorization_locks) for Rust SDK
and CLI examples.
10 changes: 9 additions & 1 deletion protocol/concepts-and-permissions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -56,13 +56,15 @@ Actions are typed permission records attached to roles.
| Program access | `Program`, `ProgramAll`, `ProgramCurated`, `ProgramScope` | CPI access from the wallet |
| Staking | `StakeLimit`, `StakeRecurringLimit`, `StakeAll` | stake-related execution |
| Sub-accounts | `SubAccount` | sub-account creation, signing, and control |
| Authorization locks | `ManageAuthorizationLocks` | reserving wallet funds within the role's own limits |
| Lifecycle and recovery | `CloseSwigAuthority`, `RecoveryAuthority` | closing wallets and constrained recovery |

## Time-based limits are measured in slots

Every time-bounded permission in Swig counts **slots**, not seconds. This
applies to `SolRecurringLimit`, `TokenRecurringLimit`, `StakeRecurringLimit`,
their destination variants, and session durations on `*Session` authorities.
their destination variants, session durations on `*Session` authorities, and
the `max_duration_slots` bound on `ManageAuthorizationLocks`.

<Warning>
Solana is reducing slot time from 400ms to 200ms in four steps
Expand Down Expand Up @@ -114,6 +116,12 @@ The two permission families fail in opposite directions, so round accordingly:
- `ProgramCurated` is looser than per-program rules, but still not fully open.
- `ProgramScope` is the narrowest program control. It binds a program, a target
account, and numeric field offsets so the program can enforce scoped limits.
- `ManageAuthorizationLocks { max_duration_slots }` is required for every
authorization lock, including for `All`. Each lock lasts at most
`max_duration_slots`. With `All` or `AllButManageAuthority` a role can lock
up to the wallet's balance; otherwise it can lock up to its own spending
limits. A role cannot change its own bound. Active locks bind every role.
See [Authorization Locks](/protocol/authorization-locks).

## Common role shapes

Expand Down
6 changes: 5 additions & 1 deletion protocol/program-walkthrough.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ In practice, most wallets move through this path:
1. create the wallet with `CreateV1`
2. add or update roles with the authority-management instructions
3. execute wallet actions through `SignV2`
4. optionally layer on sessions, sub-accounts, or recovery
4. optionally layer on sessions, sub-accounts, authorization locks, or recovery
5. eventually migrate, transfer assets, or close the wallet

## Instruction families
Expand All @@ -26,6 +26,7 @@ In practice, most wallets move through this path:
| General execution | `SignV2` | authenticate a role and execute wallet instructions |
| Sub-accounts | `CreateSubAccountV1`, `WithdrawFromSubAccountV1`, `SubAccountSignV1`, `ToggleSubAccountV1` | manage segregated balances and sub-account execution |
| Recovery | `RecoverAuthorityV1` | rotate one signer authority through the constrained recovery path |
| Authorization locks | `AddAuthorizationLockV1`, `RemoveAuthorizationLockV1` | reserve part of a wallet asset until an expiry slot, then release or prune it |

## Wallet creation and role management

Expand Down Expand Up @@ -64,6 +65,9 @@ These flows are important, but they are extensions around the main role model:
path
- `RecoverAuthorityV1` rotates an authority through a narrower recovery rule
set
- authorization-lock instructions reserve funds that `SignV2` then refuses to
spend below, for every role, until the lock is released or expires. See
[Authorization Locks](/protocol/authorization-locks)

Use the dedicated execution pages when you are building or debugging one of
those flows.
Expand Down
Loading