Authentication delegation for custom accounts
Specification
CAP: 0071-01
Title: Authentication delegation for custom accounts
Working Group:
Owner: Dmytro Kozhevin <@dmkozh>
Authors: Dmytro Kozhevin <@dmkozh>
Consulted:
Status: Final
Created: 2025-09-10
Discussion: https://github.com/orgs/stellar/discussions/1784
Protocol version: 27
Simple Summary
Add a built-in way for custom (smart contract) accounts to delegate the authentication logic to other addresses.
Working Group
As specified in the Preamble.
Motivation
Custom (smart contract-based) accounts on Stellar had the capability to delegate their authentication logic to a different address starting from their introduction in protocol 20. This can be achieved via calling require_auth_for_args for an Address (or multiple addresses) inside the __check_auth function of the custom account instead of, or in addition to, performing signature validation. The authentication would then be requested from these addresses, thus potentially triggering their __check_auth functions.
This feature has been experimented with in some custom account implementations, and is generally deemed useful, as it allows for creating more flexible and 'modular' custom accounts. A significant motivating use case for the authentication delegation is also introduced in CAP-72: it adds the authentication logic customization capability to the Stellar accounts via adding contracts as additional account signers. This is the same case of authentication delegation, with the only difference being that the authentication procedure for the Stellar accounts is built into the protocol.
However, since the current delegation support happens to be just a by-product of the authorization framework design, there are significant user and developer experience flaws with the approach:
- It is hard to properly simulate delegated authorization. When simulation records the necessary authorization payloads, it doesn't ever enter
__check_auth, as it doesn't have sufficient information to call it. Thus users need to build the authorization payload of the inner call themselves and then they also need to run simulation again.- This could be somewhat simplified on the simulation side by introducing a mode in which authorization data may be partially initialized, which would simplify the inner payload building. However, this approach still comes with complexity for developers (such as the need to do more simulation calls), as well as the added simulation complexity.
- Every account for which
__check_authis called requires an authorization entry in the transaction, with its own nonce. That increases transaction cost and further increases complexity. - It is hard to forward the authorization context to the delegated signers. To do that, one needs to pass the whole context into the
require_auth_for_argscall. But that means that the whole context needs to be present in the authorization payload of the delegated signer, which bloats the transaction size unnecessarily, and increases the complexity for the developers if they want to write logic dependent on the context.
Most of these issues, besides double simulation, can be mitigated with a rather small protocol change proposed in the CAP.
Goals Alignment
This CAP is aligned with the following Stellar Network Goals:
- The Stellar Network should make it easy for developers of Stellar projects to create highly usable products
Abstract
A new authorization host function delegate_account_auth is introduced for delegating the authentication logic from an Addresses __check_auth function to a different address. Also, a new type of Soroban authorization credentials is introduced that allows specifying the delegate addresses together with their signatures.
Each of the delegated addresses has its own signature and its own authentication logic defined by the address executable. However, the signature payload and authorization context are shared among all the signers and are equivalent to the signature payload and the context of the 'top-level' address to which the authorization entry belongs.
delegate_account_auth calls can also be nested recursively, and every authentication call will still inherit the 'top-level' arguments.
Another host function called get_delegated_signers_for_current_auth_check is also introduced for standardizing and simplifying the delegated signers access by providing the direct access to the delegated signers populated in the authorization payload.
With these changes only a single authorization entry is necessary to accommodate an arbitrary number of potentially nested delegated signers, which reduces the transaction size greatly and simplifies the simulation logic.
Specification
XDR changes
The final cumulative XDR diff for CAP-71 is in CAP-71. The XDR changes relevant to this sub-CAP are the introduction of ENVELOPE_TYPE_SOROBAN_AUTHORIZATION_WITH_ADDRESS, SorobanDelegateSignature, SorobanAddressCredentialsWithDelegates, and SOROBAN_CREDENTIALS_ADDRESS_WITH_DELEGATES.
New host functions
The diff is based on commit f5153ff476ce7e68ee0bda278188cded135ff301 of rs-soroban-env.
diff --git a/soroban-env-common/env.json b/soroban-env-common/env.json
index 50293483..269862fc 100644
--- a/soroban-env-common/env.json
+++ b/soroban-env-common/env.json
@@ -2467,6 +2467,25 @@
"return": "Val",
"docs": "Returns the executable corresponding to the provided address. When the address does not exist on-chain, returns `Void` value. When it does exist, returns a value of `AddressExecutable` contract type. It is an enum with `Wasm` value and the corresponding Wasm hash for the Wasm contracts, `StellarAsset` value for Stellar Asset contract instances, and `Account` value for the 'classic' (G-) accounts.",
"min_supported_protocol": 23
+ },
+ {
+ "export": "7",
+ "name": "get_delegated_signers_for_current_auth_check",
+ "args": [],
+ "return": "VecObject",
+ "docs": "Returns a vector of `Address`es of all the delegated signers that have attached signatures to authorize the current account contract invocation. **Important**: These are user-provided inputs and should be treated accordingly, in a similar fashion to the actual signatures. Specifically, the account contract must ensure that these signers actually belong to it, and perform authentication for every one of them via `delegate_account_auth`. This may only be called within `__check_auth` contract function inside a custom account.",
+ "min_supported_protocol": 27
+ },
+ {
+ "export": "8",
+ "name": "delegate_account_auth",
+ "args": [
+ {
+ "name": "address",
+ "type": "AddressObject"
+ }
+ ],
+ "return": "Void",
+ "docs": "Delegates the custom account authentication to the provided address. This is only available when called within `__check_auth` contract function inside a custom account. This call will require the `address` to have authorized exactly the same call tree as the one being authorized by the current `__check_auth` call. Specifically, the same signature payload and the same context will be passed into `address`s authorization check. Panics if the call has not been authorized, or if called not from within `__check_auth`.",
+ "min_supported_protocol": 27
}
]
},
Semantics
SorobanAddressCredentialsWithDelegates validation
When Soroban host accepts authorization entries (SorobanAuthorizationEntry) it performs some basic validation on them (such as signatures being well-formed host values). For credentials of type SorobanAddressCredentialsWithDelegates the following additional validation rules are added:
- Every
SorobanDelegateSignaturearray (both top-leveldelegatesfield and everynestedDelegatesfield) must be sorted in the increasing order of theaddressfield - Every
SorobanDelegateSignaturearray must contain no duplicate addresses
If any of these conditions is violated, the host function invocation will fail immediately before entering the contract.
delegate_account_auth function
A general mechanism for supporting authentication delegation is introduced, both for use to support the delegation for G-accounts, as well as for use in any custom (contract-based) accounts.
delegate_account_auth host function is introduced by this CAP. It may only be called from the reserved __check_auth function, i.e. only when performing account authentication. All the addresses for which delegate_account_auth is called must be present in SorobanAddressCredentialsWithDelegates credentials in the delegates or nestedDelegates fields. Every such address will have __check_auth called for it with the same signature payload and context as for the top-level address the credentials belong to.
More formally, the algorithm for handling delegate_account_auth is defined as follows:
- Define 'top level'
__check_authcall for non-source addressAas a call that occurs whenrequire_auth[_for_args]is matched toAfor the first time, as per CAP-46-11.- Note, that implementation of
__check_authfor G-accounts is built into host and is not observed as a contract call; however that doesn't affect the algorithm.
- Note, that implementation of
- Define top level call arguments as
A.__check_auth(P, S(A), C)wherePis the 32-byte signature payload,S(A)is the signature from theAs auth entry, andCis the authorization context derived from the auth entry. - For every
__check_authcall, definecurrent_delegate_listas a list of delegated signer addresses with the corresponding signatures and ausedflag. - For top level call, define
current_delegate_listas the value ofdelegatesinsideSorobanAddressCredentialsWithDelegatesinAs auth entry, or an empty list ifSorobanAddressCredentialsis used. Mark every delegate as unused by setting theirusedflag tofalse. - Any time when
delegate_account_authis called for an addressB:- Search for the first unused (
used == false) occurrence ofBincurrent_delegate_listand store the found indexi. If there is no such occurrence, fail authentication process. - Set
current_delegate_list[i].usedtotrue. - Call
B.__check_auth(P, current_delegate_list[i].signature, C).- For the call, set
current_delegate_listto the value ofcurrent_delegate_list[i].nestedDelegatesand mark every delegate as unused.
- For the call, set
- Search for the first unused (
get_delegated_signers_for_current_auth_check function
get_delegated_signers_for_current_auth_check is a supplementary host function that simplifies the usage of delegate_account_auth. It returns the current_delegate_list populated as per the algorithm in the previous section as a vector of addresses.
This allows account contracts to have Void/empty signature when only the delegated signers are being used, which further reduces the transaction size and complexity, and simplifies the contract itself.
Signature payload for SorobanAddressCredentialsWithDelegates
When SorobanAddressCredentialsWithDelegates type of credentials is used, the signature payload expected by the protocol is SHA-256 hash of HashIDPreimage XDR with ENVELOPE_TYPE_SOROBAN_AUTHORIZATION_WITH_ADDRESS variant. The contents of the payload preimage are equivalent to ENVELOPE_TYPE_SOROBAN_AUTHORIZATION, as defined by CAP-46-11. The only difference is that address field must be filled in as well with the value of the top-level address to which the respective credentials belong.
Design Rationale
New signature payload type
While it would be easier to just re-use the existing payload, that would open up a potential vulnerability.
Specifically, if an account uses only delegated signers, then signatures for a delegated signer could be replayed by using the delegated signer's address in place of the original account's address in the invocation. For example, if account A needs to sign a token transfer token.transfer(A, to, 100) via delegated signer D and token implementation only calls A.require_auth_for_args((to, 100)), then the signature of D would be reusable in token.transfer(D, to, 100) call. That would happen because ENVELOPE_TYPE_SOROBAN_AUTHORIZATION is not explicitly attached to the account address, it's implicitly linked to it via cryptographic signature. Thus the issue would only happen for the contracts that don't explicitly require authorization for the address as an invocation argument.
In order to prevent the vulnerability in all the cases, a new payload type is introduced that fixes the issue by explicitly adding the address to the payload and thus explicitly linking the nonce and signatures to the top-level account.
Note, that in the 'legacy' delegation flow that currently exists in the protocol the same kind of linking is achieved due to the need for D to explicitly authorize A.__check_auth call (i.e. linking happens in D's authorized invocation specification within the signed payload).
Delegated signer recursive nesting
The ability to nest the delegated signers makes protocol a bit more complex and requires 4 bytes of additional space in credentials even if it's not being used. This is introduced mostly for the sake of completeness and consistency, so that any accounts could be used as delegated signers (including ones that have delegated signers of their own). This may also potentially help some complex protocols that may come in the future. The relative protocol complexity increase is pretty marginal and thus should be tolerable.
Delegated signer getter
get_delegated_signers_for_current_auth_check function is technically optional for this CAP, as smart accounts could get away with defining a special signature type to account for the delegated signers.
However, this creates some unnecessary duplication in the transaction data (as delegated signers would need to be specified twice), and introduces unnecessary fragmentation into how delegated signers work, as different contracts may use slightly different UDTs to represent them in signatures. This CAP already provides a standardized approach for storing the delegated signers in the transaction, so there is no good reason not to benefit from that on the contract implementation side as well, while also avoiding duplication.
Protocol Upgrade Transition
The proposed host functions will use the standard mechanism for protocol-gating the host functions and will become available in protocol 27. The new credential type will also only be available starting from protocol 27, otherwise including it into transaction will invalidate the transaction.
Backwards Incompatibilities
This CAP does not introduce any backward incompatibilities.
Resource Utilization
The new host function will have the appropriate metering. No new cost types need to be introduced, as the operations can lean on the existing metering primitives.
Security Concerns
Delegated signers increase the account implementation complexity and thus may increase the probability of it being vulnerable to exploits. However, this CAP doesn't significantly change the risk surface, as similar functionality already exists in the protocol.
Similarly to the existing authorization framework code, the new authorization-related code is very sensitive and must be thoroughly reviewed. Given the correct implementation there aren't significant new concerns from the protocol standpoint.
Test Cases
TBD
Implementation
TBD
Appendix: Simulation flow
While simulation is not a part of the protocol, it is an important part of the overall Stellar developer experience, so it's useful to mention it in order to put this CAP into context.
Even with this CAP, two simulation runs are necessary in order to come up with the correct transaction footprint and resources. Note, that two simulation runs is a general issue that any custom account likely needs to deal with. There is a possibility to avoid the second simulation run via supplying all the signers and 'dummy' signatures for them, but that change is unrelated to this CAP.
However, the process is still significantly simplified and streamlined. Specifically, the flow for any account A with any number of delegated signers will look like the following (for simplicity a call where only a single authorization entry needs to be signed is described, but this scales to an arbitrary number of payloads):
- Send a simulation request in the recording authorization mode
- Receive simulation response that contains the invocation part of the authorization entry for
A - Build
HashIDPreimage::ENVELOPE_TYPE_SOROBAN_AUTHORIZATION_WITH_ADDRESS(withA's address, invocation tree, network ID, nonce, and signature expiration ledger as inputs) and compute its SHA-256 hashH. - Compute the signature for
A, depending on the account implementation (e.g. signHwithAs key, or just pass aVoidvalue if only delegate signers are used) and attach it as a top-level signature in the outputSorobanAddressCredentialsWithDelegates - For every delegated signer
D, signHaccording to the implementation and attachDs address and signature toSorobanAddressCredentialsWithDelegates. In case of nested accounts, the signature must go to the appropriate node of the tree (the assumption is that wallet knows the intricacies of the account authentication scheme, which is necessary anyways to perform signing) - Attach the final
SorobanAddressCredentialsWithDelegateswith all the signatures toSorobanAuthorizationEntryin the transaction and send it for another round of simulation in the enforcing mode - Use the resources from the second simulation run for sending the final transaction to the network
Preamble
Discussion
0 linked threads