Skip to content

Latest commit

 

History

History
384 lines (290 loc) · 18 KB

cap-0067.md

File metadata and controls

384 lines (290 loc) · 18 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 <[email protected]>, Simon Chow <[email protected]>
Status: Draft
Created: 2025-01-13
Discussion: https://github.com/stellar/stellar-protocol/discussions/1553
Protocol version: 23

Simple Summary

Emit transfer, mint, burn, clawback, and fee events in Classic in the same format as what we see in Soroban so that the movement of assets 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

This CAP specifies three changes -

  1. 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.
  2. Remove the admin from the topics of the mint and clawback events emitted in the SAC.
  3. Update issuer semantics in the SAC so that a transfer involving the issuer will emit the semantically correct event (mint or burn).

Specification

XDR Changes

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

diff --git a/Stellar-contract.x b/Stellar-contract.x
index 5113005..5aced97 100644
--- a/Stellar-contract.x
+++ b/Stellar-contract.x
@@ -179,7 +179,10 @@ 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_CLAIMABLE_BALANCE = 2,
+    SC_ADDRESS_TYPE_LIQUIDITY_POOL = 3,
+    SC_ADDRESS_TYPE_MUXED_ACCOUNT = 4
 };
 
 union SCAddress switch (SCAddressType type)
@@ -188,6 +191,12 @@ case SC_ADDRESS_TYPE_ACCOUNT:
     AccountID accountId;
 case SC_ADDRESS_TYPE_CONTRACT:
     Hash contractId;
+case SC_ADDRESS_TYPE_CLAIMABLE_BALANCE:
+    ClaimableBalanceID claimableBalanceId;
+case SC_ADDRESS_TYPE_LIQUIDITY_POOL:
+    PoolID liquidityPoolId;
+case SC_ADDRESS_TYPE_MUXED_ACCOUNT:
+    MuxedAccount accountId;
 };
 
 %struct SCVal;
diff --git a/Stellar-ledger.x b/Stellar-ledger.x
index 6ab63fb..79e6bab 100644
--- a/Stellar-ledger.x
+++ b/Stellar-ledger.x
@@ -446,7 +446,44 @@ struct TransactionMetaV3
                                          // Soroban transactions).
 };
 
+struct OperationMetaV2
+{
+    // We can use this to add more fields, or because it
+    // is first, to change OperationMetaV2 into a union.
+    ExtensionPoint ext;
+
+    LedgerEntryChanges changes;
+
+    ContractEvent events<>;
+    DiagnosticEvent diagnosticEvents<>;
+}
+
+struct SorobanTransactionMetaV2
+{
+    SorobanTransactionMetaExt ext;
+
+    SCVal returnValue;
+}
+
+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).
+
+    ContractEvent events<>;
+    DiagnosticEvent txDiagnosticEvents<>; // Used for diagnostic information not tied
+                                          // to an operation.                                 
+};
+
 // This is in Stellar-ledger.x to due to a circular dependency 
+// Only used before protocol 23
 struct InvokeHostFunctionSuccessPreImage
 {
     SCVal returnValue;
@@ -465,6 +502,8 @@ case 2:
     TransactionMetaV2 v2;
 case 3:
     TransactionMetaV3 v3;
+case 4:
+    TransactionMetaV4 v4;
 };

Semantics

Remove the admin from the SAC mint and clawback 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

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.

New Events

This section will go over the semantics of how the additional transfer events are emitted for each operation, as well as the fee event emitted for the fee paid by the source account. 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 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.

The Address used on the events specified below will be muxed if the classic operation uses a muxed address. This can be done using the new SC_ADDRESS_TYPE_MUXED_ACCOUNT SCAddressType. SC_ADDRESS_TYPE_MUXED_ACCOUNT will not be used within a InvokeHostFunctionOp, but this point can be revisited in the future. If this new SCAddressType is used, the type of the underlying MuxedAccount must be KEY_TYPE_MUXED_ED25519 to prevent multiple ways of setting a non-muxed account.

Fees paid by source account

For each transaction whose source account pays fees for the execution of a transaction, emit an event in the following format:

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

Where from is the account paying the fee, either the fee bump fee account or the tx source account.

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

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 being debited (seller).
  • to is the account 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.

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.

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.

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

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

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

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

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).

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 credited.
  • amount is the amount being moved out of the account and burned.

Allow Trust / Set Trustline Flags

If either operation is used to revoke authorization from a trustline that deposited into a liquidity pool then claimable balances will 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:

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.

Retroactively emitting correct events

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.

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.

New events will not be hashed into the ledger

For now, the new events 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.

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.

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