Repository navigation
docs: authorization locks #60
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
bea758f
00f9d2b
4f8ed4a
304d296
a74ebd2
b03dd07
2343ac4
90d31ce
d5d1fa4
3e7aed6
004271c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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, | ||
| 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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please state the root-role exception consistently here and in |
||
| 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)?; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please grant a token spending limit for |
||
| ``` | ||
|
|
||
| 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)?; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [NIT] Could the recipe show where |
||
| ``` | ||
|
|
||
| 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. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| ``` | ||
|
|
||
| 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. | ||
| 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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
| `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. | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
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/AllButManageAuthoritycap as this asset account's unreserved, lockable balance. For native SOL, exclude the rent-exempt minimum. Apply the same wording inconcepts-and-permissions; 'up to that limit' and 'the wallet’s balance' currently overstate what a new lock can reserve.