Skip to content

Latest commit

 

History

History
586 lines (437 loc) · 36.7 KB

File metadata and controls

586 lines (437 loc) · 36.7 KB

Preamble

CAP: 0067
Title: Unified Asset Events
Working Group:
    Owner: Siddharth Suresh <@sisuresh>
    Authors: Siddharth Suresh <@sisuresh>, Leigh McCulloch <@leighmcculloch>
    Consulted: Dmytro Kozhevin <@dmkozh>, Jake Urban <jake@stellar.org>, Simon Chow <simon.chow@stellar.org>
Status: Final
Created: 2025-01-13
Discussion: https://github.com/stellar/stellar-protocol/discussions/1553
Protocol version: 23

Simple Summary

Emit transfer, mint, burn, clawback, fee, and set_authorized events in Classic in the same format as what we see in Soroban so that the movement of assets and trustline updates can be tracked using a single stream of data. In addition to emitting events in Classic, update the events emitted in the Stellar Asset Contract to be semantically correct and compatible with SEP-41.

Motivation

Tracking the movement of Stellar assets today is complex because you need to consume both Soroban events emitted by the Stellar Asset Contract and ledger entry changes for Classic operations. There are also differences between Stellar assets and custom Soroban tokens that this CAP will address so those differences will be made irrelevant to the end user.

Goals Alignment

This CAP is aligned with the following Stellar Network Goals:

  • The Stellar Network should be secure and reliable, and should bias towards safety, simplicity, reliability, and performance over new functionality.

Abstract

The changes specified by this CAP -

  • Remove the admin from the topics of the mint and clawback events emitted in the SAC.
  • Update issuer semantics in the SAC so that a transfer involving the issuer will emit the semantically correct event (mint or burn).
  • Add memo support to Soroban by adding a SC_ADDRESS_TYPE_MUXED_ACCOUNT and allow the SAC to take in this type in the transfer function call.
  • Update the data field in the transfer event from an integer to a map to store additional memo information.
  • Emit an event for every movement of an Asset of types ASSET_TYPE_NATIVE, ASSET_TYPE_CREDIT_ALPHANUM4, and ASSET_TYPE_CREDIT_ALPHANUM12. in Stellar classic. All of the added events will follow the format of the existing Stellar Asset Contract events, with the exception of a new fee event to track fees paid by the source account.

Specification

XDR Changes

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

diff --git a/Stellar-contract.x b/Stellar-contract.x
index 5113005..71738cb 100644
--- a/Stellar-contract.x
+++ b/Stellar-contract.x
@@ -179,7 +179,16 @@ case CONTRACT_EXECUTABLE_STELLAR_ASSET:
 enum SCAddressType
 {
     SC_ADDRESS_TYPE_ACCOUNT = 0,
-    SC_ADDRESS_TYPE_CONTRACT = 1
+    SC_ADDRESS_TYPE_CONTRACT = 1,
+    SC_ADDRESS_TYPE_MUXED_ACCOUNT = 2,
+    SC_ADDRESS_TYPE_CLAIMABLE_BALANCE = 3,
+    SC_ADDRESS_TYPE_LIQUIDITY_POOL = 4
+};
+
+struct MuxedEd25519Account
+{
+    uint64 id;
+    uint256 ed25519;
 };
 
 union SCAddress switch (SCAddressType type)
@@ -188,6 +197,12 @@ case SC_ADDRESS_TYPE_ACCOUNT:
     AccountID accountId;
 case SC_ADDRESS_TYPE_CONTRACT:
     Hash contractId;
+case SC_ADDRESS_TYPE_MUXED_ACCOUNT:
+    MuxedEd25519Account muxedAccount;
+case SC_ADDRESS_TYPE_CLAIMABLE_BALANCE:
+    ClaimableBalanceID claimableBalanceId;
+case SC_ADDRESS_TYPE_LIQUIDITY_POOL:
+    PoolID liquidityPoolId;
 };
 
 %struct SCVal;
diff --git a/Stellar-ledger.x b/Stellar-ledger.x
index 0fc03e2..963acc4 100644
--- a/Stellar-ledger.x
+++ b/Stellar-ledger.x
@@ -434,6 +434,41 @@ struct TransactionMetaV3
                                          // Soroban transactions).
 };
 
+struct OperationMetaV2
+{
+    ExtensionPoint ext;
+
+    LedgerEntryChanges changes;
+
+    ContractEvent events<>;
+};
+
+struct SorobanTransactionMetaV2
+{
+    SorobanTransactionMetaExt ext;
+
+    SCVal* returnValue;
+};
+
+// Transaction-level events happen at different stages of the ledger apply flow
+// (as opposed to the operation events that all happen atomically when 
+// transaction is applied).
+// This enum represents the possible stages during which an event has been
+// emitted.
+enum TransactionEventStage {
+    // The event has happened before any one of the transactions has its 
+    // operations applied.
+    TRANSACTION_EVENT_STAGE_BEFORE_ALL_TXS = 0,
+    // The event has happened immediately after operations of the transaction
+    // have been applied.
+    TRANSACTION_EVENT_STAGE_AFTER_TX = 1,
+    // The event has happened after every transaction had its operations 
+    // applied.
+    TRANSACTION_EVENT_STAGE_AFTER_ALL_TXS = 2
+}
+
+// Represents a transaction-level event in metadata.
+// Currently this is limited to the fee events (when fee is charged or 
+// refunded).
+struct TransactionEvent {    
+    TransactionEventStage stage;  // Stage at which an event has occurred.
+    ContractEvent event;  // The contract event that has occurred.
+}
+
+struct TransactionMetaV4
+{
+    ExtensionPoint ext;
+
+    LedgerEntryChanges txChangesBefore;  // tx level changes before operations
+                                         // are applied if any
+    OperationMetaV2 operations<>;        // meta for each operation
+    LedgerEntryChanges txChangesAfter;   // tx level changes after operations are
+                                         // applied if any
+    SorobanTransactionMetaV2* sorobanMeta; // Soroban-specific meta (only for
+                                           // Soroban transactions).
+
+    TransactionEvent events<>; // Used for transaction-level events (like fee payment)
+    DiagnosticEvent diagnosticEvents<>; // Used for all diagnostic information
+};
+
+
 // This is in Stellar-ledger.x to due to a circular dependency 
 struct InvokeHostFunctionSuccessPreImage
 {
@@ -453,6 +541,8 @@ case 2:
     TransactionMetaV2 v2;
 case 3:
     TransactionMetaV3 v3;
+case 4:
+    TransactionMetaV4 v4;
 };
 
 // This struct groups together changes on a per transaction basis

New host functions

The diff is based on commit 9a24835ec6d75c526fb128a1d73b92e7d7becfa7 of rs-soroban-env.

diff --git a/soroban-env-common/env.json b/soroban-env-common/env.json
index d421dca2..4249efc6 100644
--- a/soroban-env-common/env.json
+++ b/soroban-env-common/env.json
@@ -2403,6 +2403,32 @@
                     ],
                     "return": "Void",
                     "docs": "Authorizes sub-contract calls for the next contract call on behalf of the current contract. Every entry in the argument vector corresponds to `InvokerContractAuthEntry` contract type that authorizes a tree of `require_auth` calls on behalf of the current contract. The entries must not contain any authorizations for the direct contract call, i.e. if current contract needs to call contract function F1 that calls function F2 both of which require auth, only F2 should be present in `auth_entries`."
+                },
+                {
+                    "export": "4",
+                    "name": "get_address_from_muxed_address",
+                    "args": [
+                        {
+                            "name": "muxed_address",
+                            "type": "MuxedAddressObject"
+                        }
+                    ],
+                    "return": "AddressObject",
+                    "docs": "Returns the address corresponding to the provided MuxedAddressObject as a new AddressObject. Note, that MuxedAddressObject consists of the address and multiplexing id, so this conversion just strips the multiplexing id from the input muxed address.",
+                    "min_supported_protocol": 23
+                },
+                {
+                    "export": "5",
+                    "name": "get_id_from_muxed_address",
+                    "args": [
+                        {
+                            "name": "muxed_address",
+                            "type": "MuxedAddressObject"
+                        }
+                    ],
+                    "return": "U64Val",
+                    "docs": "Returns the multiplexing id corresponding to the provided MuxedAddressObject as a U64Val.",
+                    "min_supported_protocol": 23
                 }
             ]
         },

Semantics

Remove the admin from the SAC mint, clawback, and set_authorized events

The mint event will look like:

contract: asset, topics: ["mint", to:Address, sep0011_asset:String], data: amount:i128

The clawback event will look like:

contract: asset, topics: ["clawback", from:Address, sep0011_asset:String], data: amount:i128

The set_authorized event will look like:

contract: asset, topics: ["set_authorized", id:Address, sep0011_asset:String], data: authorize:bool

Emit the semantically correct event for a Stellar Asset Contract transfer when the issuer is involved

At the moment, if the issuer is the sender in a Stellar Asset Contract transfer, the asset will be minted. If the issuer is the recipient, the asset will be burned. The event emitted in both scenarios, however, is the transfer event. This CAP changes that behavior to instead emit the mint/burn event.

Multiplexing support

Multiplexing (muxing) is a technique used by the Stellar users to represent custodial accounts, i.e. accounts that are represented by a single on-chain entity, but may have an arbitrary number of off-chain, 'virtual' balances. Stellar protocol currently provides two ways for supporting the account 'multiplexing' ('muxing'): using the transaction memo to identify the payment destination, and the MuxedAccount type that can identify the source or destination of most of the Stellar operations.

This CAP ensures that muxing information is represented in the events, and also extends the multiplexing functionality to Soroban operations.

MuxedAddressObject host object

A new host object type called MuxedAddressObject is added to represent multiplexed addresses (only account addresses as of this CAP). While the regular host objects used to have 1:1 mapping to SCVal, due to changes in this CAP it's necessary to change the semantics and have 1:n mapping between SCVal::SCV_ADDRESS (and the respective ScAddress) and the host objects. Specifically, semantics of the mapping between ScAddress and host objects are updated in the following fashion:

  • (unchanged) SC_ADDRESS_TYPE_ACCOUNT and SC_ADDRESS_TYPE_CONTRACT still correspond to AddressObject
  • SC_ADDRESS_TYPE_MUXED_ACCOUNT corresponds to MuxedAddressObject
  • SC_ADDRESS_TYPE_CLAIMABLE_BALANCE and SC_ADDRESS_TYPE_LIQUIDITY_POOL are disallowed by host and any conversion from XDR to host objects that involves these address kinds will fail (including passing these in the authorization payloads, function arguments, decoding the XDR blobs via host function etc.).

Other than the special XDR mapping, MuxedAddressObject is a regular host object and thus it's not implicitly compatible with AddressObject type. Thus the contracts that expect the regular AddressObject as an input argument will fail if ScAddress::SC_ADDRESS_TYPE_MUXED_ACCOUNT is passed to them (the failure is expected and is the same kind of failure as for passing any other object kind beyond AddressObject).

MuxedAddressObject is meant to identify the multiplexed addresses for the special cases that require them. Thus we only provide the host functions that get the two parts of MuxedAddressObject:

  • get_address_from_muxed_address returns the regular address part as AddressObject
  • get_id_from_muxed_address returns the multiplexing identifier as U64Val
SC_ADDRESS_TYPE_MUXED_ACCOUNT is prohibited in storage keys

Multiplexed accounts are 'virtual' accounts that are meant to only exist off-chain. While with this CAP it will be possible for any contract to process MuxedAddressObject in an arbitrary fashion, most of the time it would be a mistake to use the whole muxed address in a contract data storage key (as it's purpose by definition is to support off-chain multiplexing). Thus as an additional precaution Soroban host will produce an error in case if SC_ADDRESS_TYPE_MUXED_ACCOUNT is present in the contract data key SCVal(either directly, or anywhere in the nested containers). This restriction is applied for all the kinds of the contract data storage, including the instance storage.

In case if the users do want to use MuxedAddressObject for multiplexing on-chain, they can still use the provided host functions to decompose the address into two values and store them in a user-defined data structure.

Update the SAC transfer function to support muxed addresses

The transfer function of the Stellar Asset contract will be updated to accept AddressObject or MuxedAddressObject both for the to argument of the transfer function. If a MuxedAddressObject is passed, then the transfer function will behave as if a respective AddressObject has been passed. The only semantics difference compared to using a regular AddressObject is the event payload to follow the format described in the following section.

Emit a map as the data field in the transfer and mint event if muxed information is being emitted for the destination.

The data field is currently an integer that represents the amount. If no muxed information is being emitted, this will not change. If we're emitting muxed information, then the data field on the transfer and mint event will be an SCVal of type SCV_MAP. In that case, the key for each map entry will be an SCSymbol with the name of the field in data (eg. "amount"), and the value will be represented by the types specified by the events below.

The general transfer event format that involves muxed accounts is topics: ["transfer", from:<non-muxed ScAddress>, to:<non-muxed ScAddress>] data: { amount: i128, to_muxed_id:u64|bytes|string }, where to_muxed_id is present. The mint event format that involves muxed accounts is topics: ["mint", to:<non-muxed ScAddress>] data: { amount: i128, to_muxed_id:u64|bytes|string }. The muxed account is always represented as a pair of (non-muxed ScAddress, muxed_id) , i.e the pair of (to, to_muxed_id) identifies the muxed transfer destination. Non-muxed accounts are represented as a singular from/to value in the event topics and the to_muxed_id key won't be present in the data map.

The mapping between the input muxed addresses and the emitted events is generically defined as follows:

  1. MuxedAccount (or ScAddress that wraps MuxedAccount, i.e SC_ADDRESS_TYPE_MUXED_ACCOUNT) with KEY_TYPE_MUXED_ED25519 variant set will be represented as a pair of ScVal::SCV_ADDRESS with ScAddress type SC_ADDRESS_TYPE_ACCOUNT and the same ed25519 key as in the MuxedAccount, and ScVal::SCV_U64 muxed_id with the same value as the id field of the MuxedAccount.
  2. If transaction memo is not MEMO_NONE, and the classic operation in the transaction has a destination that is a non-muxed classic account (i.e. is representable by SC_ADDRESS_TYPE_ACCOUNT address), then the transfer event destination will be represented as a pair of ScVal::SCV_ADDRESS with to ScAddress type SC_ADDRESS_TYPE_ACCOUNT and the same ed25519 key as in the destination account, and an to_muxed_id ScVal containing the transaction memo. The memo is mapped to ScVal in the following fashion:
  • MEMO_ID is represented by SCV_U64
  • MEMO_TEXT is represented by SCV_STRING
  • MEMO_HASH and MEMO_RETURN are represented as SCV_BYTES, i.e. these memo types are indistinguishable in the event

Note, that the memo mapping rules above imply that in case if the destination of the transfer or mint is not a classic account (i.e. a claimable balance, or liquidity pool), or a muxed account itself, then the transaction memo will not be represented in the unified events at all.

Here is how the rules above generally apply to the generated unified events (the event-specific sections also contain more detailed information where necessary):

  • For transfer and mint events coming directly from Soroban (i.e. from Stellar Asset contract) we will represent the input KEY_TYPE_MUXED_ED25519 muxed account addresses according to the rule 1
  • For the transfer and mint events emitted due to non-Soroban value movement we make best-effort mapping based on the input MuxedAccounts and the transaction memo:
    • If the destination is a MuxedAccount with KEY_TYPE_MUXED_ED25519, then to_muxed_id will be emitted according to rule 1
    • If the destination is a non-muxed classic account (i.e. MuxedAccount with KEY_TYPE_ED25519 variant set), then emit to_muxed_id according to rule 2

New Events for Representing Fees

When a transaction has a fee charged or refunded, emit an event in the following format in the transaction-level events field of the TransactionMetaV4:

contract: native asset, topics: ["fee", from:Address], data: amount:i128

Where from is the account paying the fee or receiving the fee refund, either the fee bump fee account for fee bump transactions, or the transaction source account otherwise. amount represents the fee charged (when positive) or refunded (when negative).

Transaction-level events also have TransactionEventStage defined in order to identify when an event has occurred within the ledger application flow. The fee events are emitted with the following stages:

  • Initial fee charged is always emitted with TRANSACTION_EVENT_STAGE_BEFORE_ALL_TXS stage, i.e. fee is charged before any transactions applied
  • Fee refund events are only emitted when the refund is non-zero (as of protocol 23 that may only be true for Soroban transactions) and the stage depends on the protocol:
    • Before protocol 23 the stage is TRANSACTION_EVENT_STAGE_AFTER_TX, i.e. the fee is refunded immediately after the transaction has been applied
    • Starting from protocol 23 the stage is TRANSACTION_EVENT_STAGE_AFTER_ALL_TXS, i.e. the fee is refunded after every transaction has been applied

No more transaction level events may be emitted as of protocol 23. The future protocols may introduce more fee events that will follow the same pattern of identifying the stage when the event has occurred.

The final fee paid can be calculated by taking the sum of the amounts from all fee events.

New Events for Operations

This section will go over the semantics of how the additional transfer/mint/burn/clawback, set_authorized events are emitted for each operation. These events will be emitted through the events<> field in the new OperationMetaV2. Soroban events will be moved to OperationMetaV2. The hash of the current soroban events will still exist under INVOKE_HOST_FUNCTION_SUCCESS as it does today. It's also important to note that nothing is being removed from meta, and in fact, the emission of the events (as well as the new meta version) mentioned in this section will be configurable through a flag.

Note that the contract field for these events corresponds to the Stellar Asset Contract address for the respective asset. The Stellar Asset Contract instance is not required to be deployed for the asset. The events will be published using the reserved contract address regardless of deployment status.

Payment

Emit one of the following events -

For a payment not involving the issuer, or if both the sender and receiver are the issuer:

contract: asset, topics: ["transfer", from:Address, to:Address, sep0011_asset:String], data: amount:i128

When sending from an issuer:

contract: asset, topics: ["mint", to:Address, sep0011_asset:String], data: amount:i128

When sending to an issuer:

contract: asset, topics: ["burn", from:Address, sep0011_asset:String], data: amount:i128

As specified in the section to update the transfer and mint events data field, to_muxed_id can be added to the data map.

Path Payment Strict Send / Path Payment Strict Receive

For each movement of the asset created by the path payment, emit one of the following -

contract: assetA, topics: ["transfer", from:Address, to:Address, sep0011_asset:String], data: amount:i128
contract: assetB, topics: ["transfer", from:Address, to:Address, sep0011_asset:String], data: amount:i128

If from is the issuer on a side of the trade, emit the following instead for that side of the trade:

contract: asset, topics: ["mint", to:Address, sep0011_asset:String], data: amount:i128

If to is the issuer on a side of the trade, emit the following instead for that side of the trade:

contract: asset, topics: ["burn", from:Address, sep0011_asset:String], data: amount:i128
  • from is the account or the liquidity pool (represented by the new SC_ADDRESS_TYPE_LIQUIDITY_POOL address type) being debited (seller).
  • to is the account or liquidity pool (represented by the new SC_ADDRESS_TYPE_LIQUIDITY_POOL address type) being credited (buyer).

The trades within a path payment are conceptually between the source account and the owner of the offers. Those are the addresses that'll appear on the event pairs specified above. At the end of all the trades, we need to emit one more transfer (or burn if the destination is the issuer) event to indicate a transfer from the source account to the destination account. The amount will be equivalent to the sum of the destination asset received on the trades of the final hop.

Note that if the path payment has an empty path and sendAsset == destAsset, then the operation is effectively a regular payment, so emit an event following the specifications of the payment section.

As specified in the section to update the transfer and mint events data field, to_muxed_id can be added to the data map, but only for the final transfer to the destination.

Create Account

Emit the following event:

contract: native asset, topics: ["transfer", from:Address, to:Address, sep0011_asset:String], data: amount:i128
  • from is the account being debited (creator).
  • to is the account being credited (created).
  • amount is the starting native balance.

As specified in the section to update the transfer and mint events data field, to_muxed_id can be added to the data map. The destination on a CreateAccountOp can't be muxed, so to_muxed_id, will only be added to the data map if the transaction memo is set.

Merge Account

Emit the following event:

contract: native asset, topics: ["transfer", from:Address, to:Address, sep0011_asset:String], data: amount:i128
  • from is the account being debited (merged).
  • to is the account being credited (merged into).
  • amount is the merged native balance.

As specified in the section to update the transfer and mint events data field, to_muxed_id can be added to the data map.

Create Claimable Balance

Emit the following event:

contract: asset, topics: ["transfer", from:Address, to:Address, sep0011_asset:String], data: amount:i128
  • from is the account being debited.
  • to is the claimable balance being created. The type of this address will be SC_ADDRESS_TYPE_CLAIMABLE_BALANCE.
  • amount is the amount moved into the claimable balance.

If an asset is a movement from the issuer of the asset, instead emit for the movement:

contract: asset, topics: ["mint", to:Address, sep0011_asset:String], data: amount:i128

As specified in the section to update the transfer and mint events data field, the to address will be a claimable balance id, so it does not make sense to add the transaction memo into the event. Therefore, to_muxed_id will not be emitted in this case.

Claim Claimable Balance

Emit the following event:

contract: asset, topics: ["transfer", from:Address, to:Address, sep0011_asset:String], data: amount:i128
  • from is the claimable balance. The type of this address will be SC_ADDRESS_TYPE_CLAIMABLE_BALANCE.
  • to is the account being credited
  • amount is the amount in the claimable balance

If the claim is a movement to the issuer of the asset, instead emit for the movement:

contract: asset, topics: ["burn", from:Address, sep0011_asset:String], data: amount:i128

As specified in the section to update the transfer and mint events data field, to_muxed_id can be added to the data map on the transfer event. The destination on a claimable balance's claimant cannot be muxed, so to_muxed_id will only be added to the data map if the transaction memo is set.

Liquidity Pool Deposit

Emit the following events:

contract: assetA, topics: ["transfer", from:Address, to:Address, sep0011_asset:String], data: amount:i128
contract: assetB, topics: ["transfer", from:Address, to:Address, sep0011_asset:String], data: amount:i128
  • from is the account being debited.
  • to is the liquidity pool being credited. The type of this address will be SC_ADDRESS_TYPE_LIQUIDITY_POOL.
  • amount is the amount moved into the liquidity pool.

If an asset is a movement from the issuer of the asset, instead emit for the movement:

contract: asset, topics: ["mint", to:Address, sep0011_asset:String], data: amount:i128

As specified in the section to update the transfer and mint events data field, the to address will be a liquidity pool id, so it does not make sense to add the transaction memo into the event. Therefore, to_muxed_id will not be emitted in this case.

Liquidity Pool Withdraw

Emit the following events:

contract: assetA, topics: ["transfer", from:Address, to:Address, sep0011_asset:String], data: amount:i128
contract: assetB, topics: ["transfer", from:Address, to:Address, sep0011_asset:String], data: amount:i128
  • from is the liquidity pool. The type of this address will be SC_ADDRESS_TYPE_LIQUIDITY_POOL.
  • to is the account being credited.
  • amount is the amount moved out of the liquidity pool.

If an asset is issued by the withdrawer, instead emit for the movement of the issued asset:

contract: asset, topics: ["burn", from:Address, sep0011_asset:String], data: amount:i128

As specified in the section to update the transfer and mint events data field, to_muxed_id can be added to the data map on the transfer event.

Manage Buy Offer / Manage Sell Offer / Create Passive Sell Offer

Emit two events per offer traded against. Each pair of events represents both sides of a trade. This does mean zero events can be emitted if the resulting offer is not marketable -

contract: assetA, topics: ["transfer", from:Address, to:Address, sep0011_asset:String], data: amount:i128
contract: assetB, topics: ["transfer", from:Address, to:Address, sep0011_asset:String], data: amount:i128

If from is the issuer on a side of the trade, emit the following instead for that side of the trade:

contract: asset, topics: ["mint", to:Address, sep0011_asset:String], data: amount:i128

If to is the issuer on a side of the trade, emit the following instead for that side of the trade:

contract: asset, topics: ["burn", from:Address, sep0011_asset:String], data: amount:i128
  • from is the account being debited (seller).
  • to is the account being credited (buyer).

to_muxed_id will not be set for events emitted by any of these offer operations.

Clawback / Clawback Claimable Balance

Emit the following event:

contract: asset, topics: ["clawback", from:Address, sep0011_asset:String], data: amount:i128
  • from is the account or claimable balance being debited.
  • amount is the amount being moved out of the account and burned.

Allow Trust / Set Trustline Flags

If a trustline has the AUTHORIZED_FLAG added or removed, as a result of one of these operations, then a set_authorized event should be emitted.

contract: asset, topics: ["set_authorized", id:Address, sep0011_asset:String], data: authorize:bool
  • id is the account that the trustline belongs to.
  • authorize is a bool that determines if the trustline is now authorized to send and receive payments for this asset.

If either operation is used to revoke authorization from a trustline that deposited into a liquidity pool then claimable balances can be created for the withdrawn assets (See CAP-0038 for more info). If any claimable balances are created due to this scenario, emit the following event for each claimable balance:

contract: asset, topics: ["transfer", from:Address, to:Address, sep0011_asset:String], data: amount:i128
  • from is the liquidity pool. The type of this address will be SC_ADDRESS_TYPE_LIQUIDITY_POOL.
  • to is the claimable balance being created. The type of this address will be SC_ADDRESS_TYPE_CLAIMABLE_BALANCE.
  • amount is the amount moved into the claimable balance.

If the an asset in the liquidity pool is being withdrawn for the issuer, then no claimable balance will be created for that asset, so instead emit a burn event -

contract: asset, topics: ["burn", from:Address, sep0011_asset:String], data: amount:i128

Due to the fact that to is a claimable balance id, we will not emit to_muxed_id in the transfer event.

Inflation

Emit a mint event for each inflation winner:

contract: asset, topics: ["mint", to:Address, sep0011_asset:String], data: amount:i128
  • to is one of the winning account addresses.
  • amount is the amount of the native asset won.

to_muxed_id will not be set for events emitted by the Inflation operation.

Retroactively emitting events

The events specified above not only need to be emitted going forward, but also on replay for all ledgers from genesis so balances can be built from events. Some Stellar Asset Contract events are also being updated (removing the admin from mint/clawback, and emitting mint/burn instead of transfer when the issuer if from or to). Due to the fact that Soroban events are hashed into the ledger, we will continue to emit Stellar Asset Contract events as they were emitted in previous protocols. This will, however, cause issues with rebuilding balances on replay, so there will be a configuration flag added that will result in [V20,V22] events being emitted in the V23 format for the scenarios just mentioned. This does mean that the events can't be used to verify the hash of the events in InvokeHostFunctionResult, but this flag will be disabled by default.

Pre-protocol 8 bug

Another relevant detail for replaying events is that prior to protocol 8, there was a bug that could result in the minting/burning of XLM. To allow for the ability to build balances with only events, we not only need to emit the events specified in this CAP from genesis, but also handle that bug properly.

For both the mint and burn scenario, the affected account will be the source account, and that should be the only account in an operation with a balance difference not entirely reflected by the events specified in this CAP. If we take the total diff of XLM in an Operations OperationMeta (similar to what the ConservationOfLumens invariant does) and emit that diff as a mint/burn event for the source account, then consumers should be able to track balances correctly. For every operation, if the source account is found to be affected, then exactly one event will be emitted - either an XLM mint or an XLM burn. To make sure the events are emitted in a way where the balances don't go negative and make sense, any XLM mint should be emitted first in OperationMetaV2, and burns should be emitted last.

Design Rationale

Remove the admin from the SAC mint and clawback events

The admin isn't relevant information when a mint or clawback occurs, and it hinders compatibility with SEP-41 for when these two events are added to it because the admin is an implementation detail. For a custom token, an admin doesn't need to be a single Address, or an admin may not required at all to emit either event.

TransactionMetaV4

This CAP introduces a new TransactionMeta version, TransactionMetaV4. Now that we're emitting events for more than just Soroban, this allows us to clean up the structure of meta because TransactionMetaV3 assumed events would only be emitted for Soroban. This change also allows us to emit events at the operation layer instead of the transaction layer using the new OperationMetaV2 type. Transaction level events like fee will still be emitted at the transaction level under TransactionMetaV4.events.

It's important to note that transaction meta is not part of the protocol, so the emission of TransactionMetaV4 instead of TransactionMetaV3 can be done using a config flag, allowing consumers of meta to switch on their own time.

Emit the semantically correct event instead of no longer allowing the issuer to transfer due to missing a trustline

The Stellar Asset Contract special cases the issuer logic because issuers can't hold a trustline for their own assets. This matches the logic in Classic. The special case was unnecessary however because the Stellar Asset Contract provides the mint and burn functions. This CAP could instead just remove the special case and allow transfers involving the issuer to fail due to a missing trustline, but this would break any contracts that rely on this behavior (it's not known at this time if contracts like this exist, but we could check if there are any transfers on pubnet that involve the issuer). That's why this CAP chooses to instead emit the correct event in this scenario.

Classic events will not be hashed into the ledger

For now, the new events being added for the non-soroban operations will not be hashed into the ledger to give us more flexibility while we figure out if we want to transform more of the meta ledger entry changes into events. We can start hashing the events at a later point. Soroban events will continued to be hashed as they are in protocol 22.

New SCAddressType types

This CAP adds two new SCAddressType types - SC_ADDRESS_TYPE_CLAIMABLE_BALANCE and SC_ADDRESS_TYPE_LIQUIDITY_POOL. These types are used in the topic of an event where the address is not a contract or a stellar account.

Order of precedence for to_muxed_id on the transfer and mint events

The transfer and mint event can emit muxed information for to, where the transaction memo will be forwarded to to_muxed_id for classic if the destination is not muxed, but the CAP has specified that if the destination is muxed and the transaction memo is set for classic events, then we set to_muxed_id using an order of precedence. We use the destinations muxed information over the transaction memo. The alternative was to also emit a transaction level tx_memo event, but we determined that this was not necessary as setting both a muxed destination account and a tx memo is an edge case without a relevant use case. If a consumer wants the tx memo as well, they can just look for it in the transaction.

No diagnostics in OperationMetaV2

We currently clear all OperationMeta when a transaction fails, but with the way OperationMetaV2 is setup in the CAP, we wouldn't be able to do that if there was a diagnostics vector inside OperationMetaV2 is used. We would have to keep around each OperationMetaV2 object, and clear the non diagnostic internals. Instead of making this change that downstream consumers may not expect, using the transaction level diagnostics vector for all diagnostics is sufficient. Classic operations won't emit any diagnostics in V23, and if we want them to in the future, we can just modify the diagnostic events to contain the operation id if that information is needed.

Protocol Upgrade Transition

On the protocol upgrade, the SAC will start emitting the mint and clawback events without the admin topic. Also, the transfer event will not be emitted for transfers involving the issuer. Instead, the appropriate mint/burn will be emitted.

The unified events will not be part of the protocol, so they can be enabled with a configuration flag at anytime.

Backwards Incompatibilities

Resource Utilization

The additional events will use more resources if a node chooses to emit them.

Security Concerns

Future work

Test Cases

Implementation