Skip to content

Latest commit

 

History

History
269 lines (184 loc) · 17.7 KB

File metadata and controls

269 lines (184 loc) · 17.7 KB
CAP: 0072
Title: Contract signers for Stellar accounts
Working Group:
    Owner: Dmytro Kozhevin <@dmkozh>
    Authors: Dmytro Kozhevin <@dmkozh>
    Consulted: Leigh McCulloch <@leighmcculloch>, Nicolas Barry <@MonsieurNicolas>, Anup Pani <@anupsdf>
Status: Draft
Created: 2025-09-04
Discussion: https://github.com/orgs/stellar/discussions/1763
Protocol version: TBD

Simple Summary

Provide protocol tools for customizing authentication logic for the Stellar (G-) accounts via smart contracts.

Working Group

As specified in the Preamble.

Motivation

Contract (C-) accounts on Stellar are fully customizable, but wallets get access to more of the ecosystem today with a G-account.

One type of functionality that some wallets want to adopt is passkeys, and specifically, the recovery processes available to passkeys on modern phones where they can be backed up in the cloud. However, to adopt passkeys is to adopt a contract account and step away from everything the ecosystem has only for G-accounts.

There is a wide range of possible solutions for bridging the gap between the C- and G-accounts, with different degree of complexity and technical challenges. This CAP aims at reducing the complexity by focusing on the specific issue of G-account customization for limited use cases and providing a solution that addresses this issue, but not necessarily more. For example, it's not a goal for this CAP to solve the issue of not being able to pay fees using contract-based authentication.

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

This CAP introduces a new kind of G-account signers, called 'delegated signers', that are usable only from within the smart contract environment, i.e. these signers can not be used to sign the transactions directly, but can be used to sign for SorobanAuthorizationEntry. Delegated signers are stored in the AccountEntry in the same fashion as any other G-account signer and they also can be managed via SetOptionsOp.

Delegated signers have a signature weight, similarly to the existing ed25519 account signers. However, instead of performing the cryptographic verification, delegate_account_auth (introduced in CAP-71) host function will be called for the delegated signer's address in order to perform authentication. Thus, if the delegated signer is a contract, then __check_auth will be called for it, which allows for the G-account authentication logic customization. In case if delegated signer is a G-account itself, the __check_auth implementation for G-accounts built into Soroban host will be called.

Since delegated signers can only be used for authentication in the smart contract environment, in order to fulfill the requirement for being able to use customizable authentication for managing the account, every G-account is treated as a built-in smart contract with an interface that provides all the account management capabilities necessary for the recovery flows. Unlike the Stellar Asset contract, every account on chain will be implicitly instantiated as a contract, i.e. its address will just become callable without any additional actions required from the users.

Modifications performed to delegated and regular (ed25519) signers via the G-account built-in contract require updating the account base reserves and removing the sponsorships on deletion. This sets a precedent for the classic base reserves being managed from the Soroban environment, including releasing the reserves from the sponsoring accounts on deletion. Using a new sponsorship itself won't be possible for the signers added via G-account contract, so a G-account must hold a sufficient XLM balance in order to add new signers via its contract.

Specification

XDR changes

This patch of XDR changes is based on the XDR files in commit 4b7a2ef7931ab2ca2499be68d849f38190b443ca of stellar-xdr.

diff --git a/Stellar-transaction.x b/Stellar-transaction.x
index 9a14d6e..0735db7 100644
--- a/Stellar-transaction.x
+++ b/Stellar-transaction.x
@@ -1378,7 +1378,9 @@ enum SetOptionsResultCode
     SET_OPTIONS_BAD_SIGNER = -8,             // signer cannot be masterkey
     SET_OPTIONS_INVALID_HOME_DOMAIN = -9,    // malformed home domain
     SET_OPTIONS_AUTH_REVOCABLE_REQUIRED =
-        -10 // auth revocable is required for clawback
+        -10, // auth revocable is required for clawback
+    // A signer with unsupported type was specified.
+    SET_OPTIONS_SIGNER_TYPE_NOT_SUPPORTED = -11
 };
 
 union SetOptionsResult switch (SetOptionsResultCode code)
@@ -1999,7 +2001,10 @@ enum TransactionResultCode
     txBAD_SPONSORSHIP = -14,        // sponsorship not confirmed
     txBAD_MIN_SEQ_AGE_OR_GAP = -15, // minSeqAge or minSeqLedgerGap conditions not met
     txMALFORMED = -16,              // precondition is invalid
-    txSOROBAN_INVALID = -17         // soroban-specific preconditions were not met
+    txSOROBAN_INVALID = -17,        // soroban-specific preconditions were not met
+    // Transaction had an extra signer of type SIGNER_KEY_TYPE_SC_DELEGATED, 
+    // which is not supported.
+    txACCOUNT_DELEGATED_SIGNER_NOT_SUPPORTED = -18 
 };
 
 // InnerTransactionResult must be binary compatible with TransactionResult
diff --git a/Stellar-types.x b/Stellar-types.x
index f383d2e..ff1ed72 100644
--- a/Stellar-types.x
+++ b/Stellar-types.x
@@ -47,7 +47,8 @@ enum SignerKeyType
     SIGNER_KEY_TYPE_ED25519 = KEY_TYPE_ED25519,
     SIGNER_KEY_TYPE_PRE_AUTH_TX = KEY_TYPE_PRE_AUTH_TX,
     SIGNER_KEY_TYPE_HASH_X = KEY_TYPE_HASH_X,
-    SIGNER_KEY_TYPE_ED25519_SIGNED_PAYLOAD = KEY_TYPE_ED25519_SIGNED_PAYLOAD
+    SIGNER_KEY_TYPE_ED25519_SIGNED_PAYLOAD = KEY_TYPE_ED25519_SIGNED_PAYLOAD,
+    SIGNER_KEY_TYPE_SC_DELEGATED = 4
 };
 
 union PublicKey switch (PublicKeyType type)
@@ -74,6 +75,8 @@ case SIGNER_KEY_TYPE_ED25519_SIGNED_PAYLOAD:
         /* Payload to be raw signed by ed25519. */
         opaque payload<64>;
     } ed25519SignedPayload;
+case SIGNER_KEY_TYPE_SC_DELEGATED:
+    SCAddress delegatedSCSigner;
 };
 
 // variable size as the size depends on the signature scheme used

Stellar(G-) account contract interface

This CAP introduces a callable smart contract interface for the Stellar accounts. The following Rust trait specifies the interface as Soroban Rust SDK contract.

pub trait StellarAccountInterface {
    /// Adds a new ed25519 signer to the account with the given weight, or
    /// updates the weight if the signer already exists.
    /// 
    /// Weight must be in range (0, 255].
    fn update_ed25519_signer(env: Env, key: BytesN<32>, weight: u32);

    /// Removes an ed25519 signer from the account.    
    fn remove_ed25519_signer(env: Env, key: BytesN<32>);

    /// Adds a new delegated signer to the account with the given weight, or
    /// updates the weight if the signer already exists.
    /// 
    /// Weight must be in range (0, 255].
    ///
    /// Instead of performing signature verification for the delegated signers,
    /// their authentication logic (`__check_auth`) will be called instead via
    /// `delegate_account_auth` host function.
    fn update_delegated_signer(env: Env, signer: Address, weight: u32);

    /// Removes a delegated signer from the account.
    fn remove_delegated_signer(env: Env, signer: Address);

    /// Sets the weight for the 'master' key of the account.
    /// 
    /// The 'master' key is the public ed25519 key that identifies the account
    /// itself. Setting the weight to 0 effectively removes the master key
    /// from the account.
    fn set_master_weight(env: Env, weight: u32);

    /// Updates the signature thresholds for the account.
    /// 
    /// `None` values leave the corresponding threshold unchanged.
    /// 
    /// `low` threshold is only used for authorizing a few non-sensitive Stellar
    //  operations, such as bumping the account's sequence number.
    /// 
    /// `med` threshold is used for authorizing most of the operations, 
    /// including the Smart Contract interactions authorized via `require_auth`.
    /// 
    /// `high` threshold is used for managing the account itself. Note, that
    /// all the functions in this interface require high threshold, while still
    /// being authorized via `require_auth`.
    fn update_thresholds(env: Env, low: Option<u32>, med: Option<u32>, high: Option<u32>);
}

Classic transaction semantics

A new type of G-account signer SIGNER_KEY_TYPE_SC_DELEGATED is added. It is supported in most of the contexts where the Signer struct is used, such as the SetOptionsOp, which thus allows adding/removing/updating the delegated signers. Only delegated signers with types convertible to the Soroban Address are supported, i.e. those with types SC_ADDRESS_TYPE_ACCOUNT and SC_ADDRESS_TYPE_CONTRACT. Otherwise, the operation will fail with SET_OPTIONS_SIGNER_TYPE_NOT_SUPPORTED error.

The only context where SIGNER_KEY_TYPE_SC_DELEGATED is explicitly not supported is the extraSigners transaction precondition, which allows users specifying the SignerKey that must sign a transaction. If SignerKey is of type SIGNER_KEY_TYPE_SC_DELEGATED, then the transaction will be considered not valid with the txACCOUNT_DELEGATED_SIGNER_NOT_SUPPORTED error.

Delegated signers are ignored during the transaction signature verification and they can't even be logically matched to the transaction signature hint due to SCAddress payload.

Smart contract semantics

G-account authentication

The algorithm for verifying detached (non-SOURCE_ACCOUNT) smart contract authorization payload in Soroban host is updated to enable delegated account support. The authentication is updated in the following way:

  • get_delegated_signers_for_current_auth_check host function (defined in CAP-71) is used to retrieve all the delegated signers corresponding to the authentication
  • If the total number of signatures and delegated signers is 0 or exceeds MAX_ACCOUNT_SIGNATURES (20), fail
  • If the delegated signers are not sorted in the ascending order, or contain duplicates, fail
  • If any delegated signer is not stored in the AccountEntry, fail
  • Call delegate_account_auth for every delegated signer, and fail if the call fails
  • Add the weight of every delegated signer to the signature weight
  • Process the regular signatures passed with the authorization entries according to CAP-46-11 and add their weights to the total weight
  • Compare the total signature weight to the required threshold (MEDIUM or HIGH, see details below)

An additional change is made in order to use the proper signature threshold for the account management. Currently, G-account authentication rules in Soroban use MEDIUM threshold when authenticating an account for any Soroban operation, i.e. the built-in G-account contract ignores the authorization context completely.

With this CAP, if any contract call on a G-account is present in authorization context, then the threshold requirement will be raised to HIGH.

G-account contract (GAC)

Every G-account on-chain gets an implicit contract 'instance' which is just represented by the account entry itself. The contract will be called GAC further for simplicity.

When a contract call is performed on a G-address, the implementation of GAC built into host will handle the call. This is similar to the Stellar Asset contract handling (SAC), with the only difference being that a non-contract address is being used for routing the calls.

AccountEntry updates from Soroban host

All GAC operations have to update the AccountEntry that belongs to the corresponding G-account. Updates to the signers require following the base reserve semantics in the same fashion as for the SetOptions operation.

When a signer is added to the AccountEntry, the number of account sub-entries is increased, so the base reserve has to go up. If the account does not have sufficient balance for increasing the base reserve, then the function call will fail. Sponsorship is not supported for the Soroban operations in general, so there is no way to sponsor the base reserve instead.

When a signer is removed from the AccountEntry, it might be sponsored. If there is no sponsorship, then the sub-entries count is just reduced for the entry. If there is a sponsorship, then information is updated accordingly in both the affected account and its sponsor, i.e. base reserve is returned to the sponsor. This update does not require additional authorization from the sponsor, so it can be performed by the Soroban Host by just changing 2 account entries accordingly.

GAC functions

Every GAC function calls require_auth for the corresponding G-account. Authentication procedure will require HIGH signature threshold, as per G-account authentication semantics described in the authentication section.

The following sections describe semantics of all the GAC functions.

update_ed25519_signer

Adds a new ed25519 signer with the provided 32-byte public key to the account. If the signer already exists, updates its weight instead.

Fails if a new signer is being added and an account already has MAX_SIGNERS (20) signers.

remove_ed25519_signer

Removes an existing ed25519 signer from the account.

Fails if the signer does not exist.

update_delegated_signer

Adds a new delegated signer with the provided Address and the provided weight. If the signer already exists, updates the weight instead.

Fails if a new signer is being added and an account already has MAX_SIGNERS (20) signers.

remove_delegated_signer

Removes an existing delegated signer from the account.

Fails if the signer does not exist.

set_master_weight

Sets the weight of the 'master' key, i.e. the public key that identifies the account.

update_thresholds

Updates the signature thresholds of the account when the corresponding arguments are set for the low/medium/high thresholds.

Design Rationale

Implicit account contracts

There is no need to explicitly created a contract instance for the G-accounts. This reduces the complexity compared to the Stellar Asset contract and reduces the necessary ledger state size. While this approach means that there is no way to opt out from the account being accessible via GAC, the account implementation has the same authorization requirements as the existing account management operation (SetOptionsOp), so there is no additional risk surface or cost induced by the implicit GAC instances.

Account base reserve management from Soroban

Until now, Soroban used to only modify a few fields in the 'classic' entries, which haven't required making any changes to the account base reserves and sponsorships. This CAP introduces modifications that require updating the base reserves and sponsorships from Soroban.

This has a somewhat non-intuitive consequence of Soroban authorization being used to modify XLM balance of the account for the fee purposes, which hasn't been the case before. However, the behavior is consistent with the classic protocol itself, so in the end it's unlikely to cause significant confusion for the users.

There is also no full feature parity with the classic protocol, as using the sponsorships is not possible in Soroban, and thus the specific account that entries are being added to has to hold XLM balance.

These issues could be avoided by moving the new signers to a separate contract data entry that is subject to the State Archival. The sponsorships removals would still need to be performed on the Soroban side (unless we forfeit remove_ed25519_signer function). This approach allows anyone to pay fee for creating the new entries. However, the downside of the contract data approach as it requires much more Soroban semantics support in the 'classic' part of protocol vs the amount of the 'classic' support in Soroban proposed in this CAP. State archival would affect every operation that works with the account signers (including the transaction validation). Since it's not cheap to tell if an entry is archived, it is not possible to provide a clear error to the users in case if the entry is archived without introducing the DOS risks. The change scope is just generally larger and thus more risky, and wallets would have a harder time adapting to the extended account structure.

CAP-71 dependency

This CAP benefits from the authentication mechanism introduced in CAP-71. It allows using a single authorization entry for an account with delegated signers and simplifies the simulation and overall developer experience.

This CAP could technically be implemented without CAP-71 by using the existing delegation approach (described in CAP-71 motivation section). However, that would come with a much worse developer experience, and is also hard to change going forward. That's why CAP-71 is a pre-requisite for this CAP.

Security Concerns

A new way to modify G-account settings is introduced. As usual for the sensitive authorization-related code, the main risk lies in the implementation correctness, thus it has to be reviewed and tested thoroughly. However, the design itself does not introduce any new risks from the protocol standpoint: the new account interface has the same high signature threshold as the SetOptions operation and only allows performing modifications that are a subset of SetOptions.

Adding delegated, contract-based signers comes with a risk for a user: every contract signer added to the account has to be a trusted and audited contract. This risk exists for any contract-based account in general. The users don't have to add contract signers to the account - if they don't do that, they are not subject to any new risks.

Test Cases

TBD

Implementation

TBD