CAP: 0049
Title: Smart Contract Asset Interoperability with Wrapper
Working Group:
Owner: Jonathan Jove <@jonjove>
Authors:
Consulted: Nicolas Barry <@monsieurnicolas>, Leigh McCulloch <@leighmcculloch>, Tomer Weller <@tomerweller>
Status: Rejected
Created: 2022-05-05
Discussion: TBD
Protocol version: TBD
Allow smart contracts to interoperate with Stellar assets.
There is an existing ecosystem of assets on the Stellar Network. Smart contracts on the Stellar Network will be significantly less useful if it is not possible to interact with those assets. Therefore, we must build an interoperability layer.
Tokens are the basic unit of blockchain applications, and as such they should be very efficient to use.
It is not guaranteed that every anchor will take steps to maximize interoperability between their existing asset and smart contracts. Other organizations may try to fill that gap, but they will not necessarily have the same level of trust. For example, consider an asset X issued by account A. If A deploys smart contracts to maximize interoperability between their existing asset and other smart contracts, then these smart contracts don't require trusting an additional organization. Native support to interoperate between an existing asset and smart contracts would avoid this issue by using trust in the Stellar protocol.
ERC-20 tokens use an allowance model to limit how much of a user's balance can be spent by a specific smart contract. This makes it relatively safe to interact with a smart contract, because it cannot steal arbitrarily large amounts of money. Existing Stellar assets have no such functionality. As a consequence, it is extremely dangerous to let a smart contract execute a payment with any account (other than the contract account) as source.
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
- The Stellar Network should enable cross-border payments, i.e. payments via exchange of assets, throughout the globe, enabling users to make payments between assets in a manner that is fast, cheap, and highly usable.
This proposal provides an ERC-20 compliant interface for Stellar assets. This
is achieved by introducing two new types of ledger entry, WrappedBalanceEntry
and WrappedAllowanceEntry
, which are used to record ERC-20 state. Atop this,
a ContractID
for cross-contract invocations is introduced which can refer to
an actual smart contract or to native contracts. In this case, we provide a new
type of native contract with type ASSET_ADAPTOR
which implements the ERC-20
and compliance interfaces in terms of WrappedBalanceEntry
and
WrappedAllowanceEntry
, and which implements the wrapper interface by
transferring funds from accounts and trustlines to wrapped balance entries.
diff --git a/src/xdr/Stellar-ledger-entries.x b/src/xdr/Stellar-ledger-entries.x
index 377309f9..f4847eb5 100644
--- a/src/xdr/Stellar-ledger-entries.x
+++ b/src/xdr/Stellar-ledger-entries.x
@@ -100,7 +100,9 @@ enum LedgerEntryType
CLAIMABLE_BALANCE = 4,
LIQUIDITY_POOL = 5,
CONTRACT_CODE = 6,
- CONTRACT_DATA = 7
+ CONTRACT_DATA = 7,
+ WRAPPED_BALANCE = 8,
+ WRAPPED_ALLOWANCE = 9
};
struct Signer
@@ -499,6 +501,36 @@ struct ContractDataEntry {
SCVal *val;
};
+struct WrappedBalanceEntry
+{
+ union switch (int v)
+ {
+ case 0:
+ void;
+ }
+ ext;
+
+ AccountID owner;
+ Asset asset;
+ uint256 amount;
+ bool authorized;
+};
+
+struct WrappedAllowanceEntry
+{
+ union switch (int v)
+ {
+ case 0:
+ void;
+ }
+ ext;
+
+ AccountID transferFrom;
+ AccountID spender;
+ Asset asset;
+ uint256 amount;
+};
+
struct LedgerEntryExtensionV1
{
SponsorshipDescriptor sponsoringID;
@@ -533,6 +565,10 @@ struct LedgerEntry
ContractCodeEntry contractCode;
case CONTRACT_DATA:
ContractDataEntry contractData;
+ case WRAPPED_BALANCE:
+ WrappedBalanceEntry wrappedBalance;
+ case WRAPPED_ALLOWANCE:
+ WrappedAllowanceEntry wrappedAllowance;
}
data;
@@ -600,6 +636,21 @@ case CONTRACT_DATA:
int64 contractID;
SCVal *key;
} contractData;
+
+case WRAPPED_BALANCE:
+ struct
+ {
+ AccountID owner;
+ Asset asset;
+ } wrappedBalance;
+
+case WRAPPED_ALLOWANCE:
+ struct
+ {
+ AccountID transferFrom;
+ AccountID spender;
+ Asset asset;
+ } wrappedAllowance;
};
// list of all envelope types used in the application
diff --git a/src/xdr/Stellar-transaction.x b/src/xdr/Stellar-transaction.x
index 3d9ee3ea..fa6297a7 100644
--- a/src/xdr/Stellar-transaction.x
+++ b/src/xdr/Stellar-transaction.x
@@ -14,6 +14,20 @@ case LIQUIDITY_POOL_CONSTANT_PRODUCT:
LiquidityPoolConstantProductParameters constantProduct;
};
+enum ContractType
+{
+ SMART_CONTRACT = 0,
+ ASSET_ADAPTOR = 1
+};
+
+union ContractID switch (ContractType type)
+{
+case SMART_CONTRACT:
+ int64 contractID;
+case ASSET_ADAPTOR:
+ Asset asset;
+};
+
struct DecoratedSignature
{
SignatureHint hint; // last 4 bytes of the public key, used as a hint
Asset adaptors implement the ERC-20 interface, authorization/clawback, and wrap/unwrap. They allow smart contracts that can interoperate with ERC-20 tokens to interoperate with Stellar assets.
Specifically, asset adaptors implement the following
/******************************************************************************\
*
* ERC-20 Interface
*
\******************************************************************************/
// name is intended to comply with SEP-11
// Returns "NATIVE" for ASSET_TYPE_NATIVE
// Returns format!("{}:{}", assetCode, issuer) for ASSET_TYPE_CREDIT_ALPHANUM4
// Returns format!("{}:{}", assetCode, issuer) for ASSET_TYPE_CREDIT_ALPHANUM12
fn name() -> String;
// symbol is intended to comply with SEP-11
// Returns "NATIVE" for ASSET_TYPE_NATIVE
// Returns assetCode for ASSET_TYPE_CREDIT_ALPHANUM4
// Returns assetCode for ASSET_TYPE_CREDIT_ALPHANUM12
fn symbol() -> String;
// Returns 7
fn decimals() -> u8;
// Returns total supply
fn totalSupply() -> uint256;
// Returns balance of owner
fn balanceOf(owner: AccountID) -> uint256;
// Returns false if !isAuthorized(msg.sender)
// Returns false if !isAuthorized(to)
// Returns false if balanceOf(msg.sender) < value
// Returns false if balanceOf(to) > UINT256_MAX - value
// Debits value from msg.sender
// Credits value to to
// Returns true
fn transfer(to: AccountID, value: uint256) -> bool;
// Returns false if !isAuthorized(msg.sender)
// Returns false if !isAuthorized(from)
// Returns false if !isAuthorized(to)
// Returns false if balanceOf(from) < value
// Returns false if balanceOf(to) > UINT256_MAX - value
// Returns false if allowance(msg.sender) < value
// Debits value from Approval(from, msg.sender, asset)
// Debits value from from
// Credits value to to
// Returns true
fn transferFrom(from: AccountID, to: AccountID, value: uint256) -> bool;
// Sets ApprovalEntry {
// transferFrom: msg.sender,
// spender: spender,
// asset: asset,
// amount: value,
// }
// Returns true on success
fn approve(spender: AccountID, value: uint256) -> bool;
// Returns Allowance(owner, spender, asset).amount
fn allowance(owner: AccountID, spender: AccountID) -> uint256;
/******************************************************************************\
*
* Compliance Interface
*
\******************************************************************************/
// Returns false if msg.sender != asset.issuer
// Sets authorized for account
// Returns true
fn authorize(account: AccountID) -> bool;
// Returns false if msg.sender != asset.issuer
// Returns false if !(Account(msg.sender).flags & AUTH_REVOCABLE)
// Clears authorized for account
// Returns true
fn deauthorize(account: AccountID) -> bool;
// Returns false if account is not authorized
// Returns true
fn isAuthorized(account: AccountID) -> bool;
// Returns false if msg.sender != asset.issuer
// Returns false if !(Account(msg.sender).flags & AUTH_CLAWBACK_ENABLED_FLAG)
// Returns false if balanceOf(from) < value
// Debits value from from
// Decreases total supply by value
// Returns true
fn clawback(from: AccountID, value: uint256) -> bool;
/******************************************************************************\
*
* Wrapper Interface
*
\******************************************************************************/
// Returns false if !isAuthorized(msg.sender)
// Returns false if balanceOf(msg.sender) > UINT256_MAX - value
// Returns false if TrustLine(msg.sender, asset) doesn't exist
// Returns false if !(TrustLine(msg.sender, asset).flags & AUTHORIZED_FLAG)
// Returns false if TrustLine(msg.sender, asset).availableBalance() < value
// Debits value from TrustLine(msg.sender, asset)
// Credits value to msg.sender
// Increases total supply by value
// Returns true
fn wrap(value: uint256) -> bool;
// Returns false if !isAuthorized(msg.sender)
// Returns false if balanceOf(msg.sender) < value
// Returns false if TrustLine(msg.sender, asset) doesn't exist
// Returns false if !(TrustLine(msg.sender, asset).flags & AUTHORIZED_FLAG)
// Returns false if TrustLine(msg.sender, asset).availableLimit() < value
// Debits value from msg.sender
// Credits value to TrustLine(msg.sender, asset)
// Decreases total supply by value
// Returns true
fn unwrap(value: uint256) -> bool;
Asset adaptors don't have normal contract addresses, because they always exist
and have no on-chain representation. Instead, an asset adaptor contract address
is simply identified by a ContractID
of type ASSET_ADAPTOR
that contains the
specified asset. We will need a host function like
fn get_asset_adaptor(asset: Asset) -> ContractID;
The entire implementation will be on the host, meaning it can leverage direct access to the processor and data store.
The asset adaptor is an extension of the issuer's authority to smart contracts, without requiring trust for any other party.
An account will have to call wrap
directly in order to prepare assets from
their trustline for use in smart contracts, and these functions can be trusted
because they are part of the protocol. Once assets are wrapped, they are subject
to the ERC-20 allowance mechanism to protect them.
The standard scheme to use an ERC-20 token with a smart contract is to set an allowance for that smart contract. Setting the allowance for the smart contract is also an opportunity to simultaneously wrap Stellar assets into the adaptor.
The core assumption here is that accounts are not doing a constant mixture of Stellar operations and smart contract interactions. I anticipate that usage is much more likely to go in phases: prepare Stellar asset for utilization in a smart contract, leave it there for some time possibly moving it to other smart contracts, bring Stellar asset back. This usage pattern is reasonable given that most smart contracts take control of the asset anyway, requiring it to be withdrawn before it can be used elsewhere.
While trustlines have three authoriztion states (unauthorized, authorized to maintain liabilities, and authorized), the asset adaptor only has two. This is because the asset adaptor has no notion of liabilities, so authorized to maintain liabilities is equivalent to unauthorized.
CAP-0035 introduced the trustline flag TRUSTLINE_CLAWBACK_ENABLED_FLAG
to
preserve backwards compatibility for existing trustlines that did not expect
clawback to be possible. This is not an issue for asset adaptors because
clawback already exists, so any wrapped asset will be subject to the behavior
specified by the issuer's flags.
The issuer balance is whatever amount of asset the issuer has wrapped plus received, minus what they have unwrapped plus sent.
Rather than using trustlines, the asset adaptor actually stores uint256
values.
Because the asset adaptor does not use trustlines, contracts cannot receive classic payments.
Asset adaptors have no notion of liabilities, so the balance is the available balance.
This proposal is completely backwards compatible. It describes a new interface to existing behavior that is accessible from smart contracts, but it does not modify the existing behavior.
This proposal adds a new type of ledger entry, which will likely lead to an increase in the total size of the ledger.
None yet.
None yet.