Externally managed contract executables
Specification
CAP: 0085
Title: Externally managed contract executables
Working Group:
Owner: Dmytro Kozhevin <@dmkozh>
Authors: Dmytro Kozhevin <@dmkozh>, Alex Mootz <@mootz12>
Consulted:
Status: Final
Created: 2026-06-30
Discussion: https://github.com/orgs/stellar/discussions/1969
Protocol version: 28
Simple Summary
Introduce a new contract executable type that refers to an entry managed by another contract, thus allowing for atomic upgrades of many contracts at once.
Working Group
As specified in the Preamble.
Motivation
The factory pattern is commonly used in smart contract protocols to deploy many contract instances that share the same Wasm (a "fleet" of contracts). When that shared Wasm needs to be upgraded, the admin must update each instance individually. This is fine for small fleets, for which it would be possible to batch the updates together into a single atomic transaction. However, if the fleet is too large to fit update into a single transaction (due to exceeding the transaction resource limits), then the atomic upgrade is not possible, which creates a window during the rollout where some instances may run the old Wasm and some run the new.
Because some upgrades carry security fixes, or are not backwards-compatible with the previous version, there is incentive for the upgrade to apply atomically across the entire fleet. Moreover, this is a manual process that puts the burden of tracking all the active instances on the admin, which is error-prone and can lead to some instances being left behind.
Ecosystems that manage reusable code via proxies, such as Ethereum, achieve this with the beacon proxy pattern.
Stellar has no equivalent today. Each contract instance defines its own Wasm hash, and we do not provide host functions equivalent to the proxy primitives in other chains (like fallback or delegatecall) that beacon implementations rely on.
This CAP proposes fixing this by extending the contract executable definition to support references to an updatable hash.
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
The CAP introduces a new contract executable type that refers to an entry managed by another contract, thus allowing for atomic upgrades of many contracts at once. The reference consists of the 'owner' contract address and a tag string. The owner contract is responsible for storing and updating the Wasm hash in a contract data entry keyed by the tag string.
In order to reduce the probability of a mistake, such as removing the executable entry, or pointing it at a non-existent Wasm, the CAP also introduces a new object type, ExecutableTagObject that must be used for the tag keys. ExecutableTagObject is just a wrapper around a string, but it allows the protocol to perform additional validation on the values. Specifically, the entries with the tag key can not be removed, and during the update the new value must be a Wasm hash of an already uploaded Wasm.
In order to allow developers to use the new executable type, the CAP introduces host functions for creating the contracts with the executable reference, and for updating the executable of the current contract to a reference. Executable references are also automatically supported by the CREATE_CONTRACT/CREATE_CONTRACT_V2 variants of InvokeHostFunctionOp operation, as it is added to the ContractExecutable union that the operation already uses.
The new executable type is also added to the ContractExecutable contract type enum on the host side, which is used in the authorization context passed to the custom accounts, which allows the custom accounts to consider the executable reference when authorizing the contract creation.
Specification
XDR changes
The diff is based on the XDR files in commit 68fa1ac55692f68ad2a2ca549d0a283273554439 of stellar-xdr.
diff --git a/Stellar-contract.x b/Stellar-contract.x
index 0e67dc3..b1c8593 100644
--- a/Stellar-contract.x
+++ b/Stellar-contract.x
@@ -70,7 +70,11 @@ enum SCValType
// symbolic SCVals used as the key for ledger entries for a contract's
// instance and an address' nonce, respectively.
SCV_LEDGER_KEY_CONTRACT_INSTANCE = 20,
- SCV_LEDGER_KEY_NONCE = 21
+ SCV_LEDGER_KEY_NONCE = 21
+
+#ifdef CAP_0085
+ ,SCV_EXECUTABLE_TAG = 22
+#endif
};
enum SCErrorType
@@ -165,8 +169,18 @@ struct Int256Parts {
enum ContractExecutableType
{
CONTRACT_EXECUTABLE_WASM = 0,
- CONTRACT_EXECUTABLE_STELLAR_ASSET = 1
+ CONTRACT_EXECUTABLE_STELLAR_ASSET = 1
+#ifdef CAP_0085
+ ,CONTRACT_EXECUTABLE_EXTERNAL_REF = 2
+#endif
+};
+
+#ifdef CAP_0085
+struct ContractExecutableExternalRef {
+ SCAddress executable_owner;
+ SCString tag;
};
+#endif
union ContractExecutable switch (ContractExecutableType type)
{
@@ -174,6 +188,10 @@ case CONTRACT_EXECUTABLE_WASM:
Hash wasm_hash;
case CONTRACT_EXECUTABLE_STELLAR_ASSET:
void;
+#ifdef CAP_0085
+case CONTRACT_EXECUTABLE_EXTERNAL_REF:
+ ContractExecutableExternalRef external_ref;
+#endif
};
enum SCAddressType
@@ -285,6 +303,11 @@ case SCV_LEDGER_KEY_CONTRACT_INSTANCE:
void;
case SCV_LEDGER_KEY_NONCE:
SCNonceKey nonce_key;
+
+#ifdef CAP_0085
+case SCV_EXECUTABLE_TAG:
+ SCString executable_tag;
+#endif
};
struct SCMapEntry
New host functions
The diff is based on commit 5538f572f1a68cb6a4bc2182e23e2f6e1ddd4520 of rs-soroban-env.
diff --git a/soroban-env-common/env.json b/soroban-env-common/env.json
index fa41a862..23dcc678 100644
--- a/soroban-env-common/env.json
+++ b/soroban-env-common/env.json
@@ -1708,6 +1708,65 @@
"return": "Void",
"docs": "Extend the contract instance and/or corresponding code entry TTL to be up to `extend_to` ledgers, where TTL is defined as `entry_live_until_ledger_seq - current_ledger_seq`. `extension_scope` defines whether contract instance, code, or both will be extended. The TTL extension only actually happens if it is at least `min_extension`, otherwise this function is a no-op. The amount of extension ledgers will not exceed `max_extension` ledgers. If attempting to extend an entry past the maximum allowed value (defined as the current ledger + `max_entry_ttl` - 1), its new `live_until_ledger_seq` will be clamped to the max.",
"min_supported_protocol": 26
+ },
+ {
+ "export": "h",
+ "name": "create_executable_tag",
+ "args": [
+ {
+ "name": "tag_string",
+ "type": "StringObject"
+ }
+ ],
+ "return": "ExecutableTagObject",
+ "docs": "Creates a new `ExecutableTag` object holding the contents of the provided string. The tag acts as a key identifying an executable reference contract data entry. Executable reference entries may be used by other contracts to fetch their executable.",
+ "min_supported_protocol": 28
+ },
+ {
+ "export": "i",
+ "name": "create_external_ref_contract",
+ "args": [
+ {
+ "name": "deployer",
+ "type": "AddressObject"
+ },
+ {
+ "name": "executable_owner",
+ "type": "AddressObject"
+ },
+ {
+ "name": "tag",
+ "type": "ExecutableTagObject"
+ },
+ {
+ "name": "salt",
+ "type": "BytesObject"
+ },
+ {
+ "name": "constructor_args",
+ "type": "VecObject"
+ }
+ ],
+ "return": "AddressObject",
+ "docs": "Creates the contract instance on behalf of `deployer`. `deployer` must authorize this call via Soroban auth framework, i.e. this calls `deployer.require_auth` with respective arguments. Executable is read from the `executable_owner` contract storage entry keyed by `tag`. Currently the only supported external executable kind is hash of an existing Wasm. `salt` is used to create a unique contract id. `constructor_args` are forwarded into created contract's constructor (`__constructor`) function. Returns the address of the created contract.",
+ "min_supported_protocol": 28
+ },
+ {
+ "export": "j",
+ "name": "update_current_contract_executable_ref",
+ "args": [
+ {
+ "name": "executable_owner",
+ "type": "AddressObject"
+ },
+ {
+ "name": "tag",
+ "type": "ExecutableTagObject"
+ }
+ ],
+ "return": "Void",
+ "docs": "Replaces the executable of the current contract with the provided executable reference. Executable is read from the `executable_owner` contract storage entry keyed by `tag`. Currently the only supported external executable kind is hash of an existing Wasm.",
+ "min_supported_protocol": 28
}
]
},
Semantics
ExecutableTagObject host object type
ExecutableTagObject is a new host object type that wraps a string, and corresponds to the new variant SCV_EXECUTABLE_TAG of SCVal with SCString payload.
ExecutableTagObject may only be created by create_executable_tag host function, and may not be manipulated by any other means.
ExecutableTagObject behaves like a normal host object, except special handling in the storage functions. Specifically, for a ExecutableTagObject storage key the following rules apply:
del_contract_datahost function will always panic.put_contract_datahost function will perform the durability and value validation. Durability must be persistent. The value must be aBytesObjectcontaining 32-bytes hash of an existing Wasm contract, i.e. an additional storage lookup will be performed to ensure that the respectiveContractCodeentry exists. Otherwise, the host function will panic.
No other storage functions are affected by the new ExecutableTagObject type, so the executable reference entries can be read or have their TTL extended via existing host functions, just like any other contract data entry.
ContractExecutable enum update
ContractExecutable is a host-side contract type counterpart of the respective ContractExecutable XDR enum. It may be consumed by the contracts (such as custom accounts) and the event consumers (when observing executable contract update events). The new variant ExternalRef(ContractExecutableRef) is added to the enum, where ContractExecutableRef is a contract type struct defined as follows in the Soroban contract SDK terms:
#[contracttype]
pub struct ContractExecutableRef {
pub owner: Address,
pub tag: String,
}
#[contracttype]
pub enum ContractExecutable {
Wasm(BytesN<32>),
StellarAsset,
ExternalRef(ContractExecutableRef),
}
When converted to SCVal, the ExternalRef contract executable would be represented as SCVec([SCSymbol("ExternalRef"), SCMap([(SCSymbol("owner"), SCAddress(<owner_address>)), (SCSymbol("tag"), SCString(<tag>))])]).
Contract creation and update with executable reference
create_external_ref_contract host function creates a new contract instance with the executable reference. The function validates if there indeed exists a persistent contract data entry for the executable_owner contract keyed by the provided ExecutableTagObject tag, or else panics. It is guaranteed by the ExecutableTagObject that a valid existing Wasm is referred by the entry.
On validation success, create_external_ref_contract creates a new contract instance with CONTRACT_EXECUTABLE_EXTERNAL_REF variant of ContractExecutable and returns the address of the created contract, which is derived from the deployer address and the provided salt in the same way as for the other contract creation host functions (create_contract and create_contract_v2). The constructor arguments are forwarded to the created contract's constructor function. If contract has no constructor arguments, the constructor_args argument must be an empty vector.
update_current_contract_executable_ref host function updates the executable of the current contract to the executable reference. The executable is validated in the same way as for create_external_ref_contract, and on success the current contract's executable is replaced with the executable reference. Similarly to update_current_contract_wasm host function, the update is applied only after the current contract function returns successfully.
update_current_contract_executable_ref emits the same system event with executable_update , as update_current_contract_wasm host function. The event emits ContractExecutable contract type SCVal for both new and old executables, so thanks to the update described in the above section, the event will be able to identify the external reference executables.
Contracts may freely change executable between the direct Wasm hash and executable reference.
InvokeHostFunctionOp operation update
InvokeHostFunctionOp operation is updated to support the new executable type. Specifically when handling, the CREATE_CONTRACT and CREATE_CONTRACT_V2 variants of the operation are updated to handle the CONTRACT_EXECUTABLE_EXTERNAL_REF variant of ContractExecutable. The reference validation and resolution semantics match the semantics of create_external_ref_contract host function.
Instance and code TTL update function updates
extend_current_contract_instance_and_code_ttl and extend_contract_instance_and_code_ttl host functions are meant to extend the TTL of all the necessary entries to keep the contract accessible and executable. Thus, for the contracts with executable reference, the functions will extend the TTL of the contract instance entry, executable reference entry in the owner contract, and the Wasm code entry referred by the executable reference.
extend_contract_code_ttl host function is semantically meant to extend the TTL of the executable entry (without extending the instance) and thus for the contracts with executable reference, the function will extend the TTL of the executable reference entry in the owner contract, and the Wasm code entry referred by the executable reference.
extend_contract_instance_and_code_ttl_v2 host function uses a more general approach to the contract TTL extensions and provides ContractTtlExtension argument that defines the scope of the extension as 'code', 'instance', and 'instance and code'. To keep the semantics of this function simple, instead of introducing special cases for the executable reference, the function will extend the TTL of both reference entry and the respective contract code entry for the scopes that includes code, i.e. 'code' and 'instance and code' scopes, similarly to how extend_contract_code_ttl extends the TTL of both reference entry and the respective contract code entry.
ContractExecutable update for custom accounts
Host passes a vector of ContractAuthorizationContext contract type structs to the __check_auth function of custom accounts. For the contract creation authorizations, the context contains the ContractExecutable enum that describes the executable of the contract being created. As the new executable type is added to the ContractExecutable enum (as specified in the section above), the custom accounts will be able to see the executable reference and make decisions based on it.
The new enum variant may not be parsed by the custom accounts that are not aware of the new executable type, so they won't be able to authorize the contract creation with external executable reference. This is expected, as we err on the side of caution and don't make any assumptions about whether the custom account makes any decisions based on the authorized executable type.
get_address_executable host function update
get_address_executable host function returns the executable for a given address. For the contracts with executable reference, the function will return AddressExecutable::Wasm(resolved_wasm_hash), where resolved_wasm_hash is the Wasm hash referred by the executable reference.
Design Rationale
Alternative approaches to atomic upgrades
The proposed approach has been chosen over the following alternatives:
delegatecall&fallbackprimitives -delegatecallprovides contract with an ability to call into another contract's code, andfallbackallows the contract to handle calls to functions that are not defined in the contract. These primitives do allow proxying multiple contracts to a single implementation. However, these are very broad primitives that come with a number of security concerns, and they are generally hard to make compatible with the Soroban design principles. The contract implementation would also need to be quite complex.- Direct inheritance of another contract's executable - this approach would on paper be less complex than the proposed one (e.g. we wouldn't need the special executable tagging mechanism), but it also comes with a number of hidden issues and edge cases. For example, the contract implementation would need to distinguish between the cases where it's used for the admin operations (e.g. updating the executable) and when it's used for executing the actual contract logic.
Besides the security and implementation complexity concerns, the proposed approach also has an added benefit of allowing a single contract to manage multiple executables, which is not possible with the direct inheritance approach.
Executable tag type
ExecutableTagObject introduction is not strictly necessary to achieve the goals of this CAP. We could instead:
- Use an arbitrary
Valas a tag. This would simplify the CAP a bit (no new host object type), but it would also increase the potential error surface, as the protocol can only enforce reference to be valid at the moment of contract creation or self-update, but it can't prevent the owner contract from removing the entry or changing it to a non-existent Wasm hash later. - Use a function host functions to explicitly manage the executable reference entries. This has the same level of error prevention as this CAP. However, the approach with multiple functions is more brittle usability-wise: e.g. if we add just a function to update the entry, the developers won't be able to tell what is the current value of the reference, also they won't be able to manage its TTL. We could add even more bespoke functions for more storage operations, but that would make the change more complex and bloat the host function set without much value.
The proposed approach combines the benefits of both approaches: it allows the protocol to enforce the reference validity, while also allowing developers to manage the executable reference entries with existing storage host functions.
get_address_executable not returning the executable reference
For contracts with executable references, the get_address_executable host function could return the executable reference itself. However, the point of this function is to allow developers to introspect the implementation for a given contract address (usually for the purpose of vetting 'trusted' implementations). Returning the executable reference would not be helpful in this case, as the reference can't be resolved by a third-party contract as Soroban doesn't provide a way to perform lookups in other contract's storage. Thus, instead of obfuscating the actual executable by returning the reference, we resolve the reference and return the actual Wasm hash.
Additionally, this approach avoids introducing a new enum variant to the AddressExecutable enum and thus reduces the CAP backwards incompatibility surface - all the users of get_address_executable will keep working as expected.
Protocol Upgrade Transition
The proposed functionality will be available starting from protocol version TBD.
Backwards Incompatibilities
As mentioned in the specification, there is a minor backwards incompatibility for the custom accounts that are not aware of the new executable type. They won't be able to authorize the contract creation with external executable reference, if they parse the authorization context Vals.
Running into this incompatibility requires a very specific setup where a custom account is used to authorize contract creation with executable reference, which is not a common scenario. It's also expected from the robust custom account implementations to be upgradeable, which would allow developers to adapt to the new executable type.
Thus, we expect the impact of this incompatibility to be very low.
Resource Utilization
The new host functions are going to be metered using the existing metering primitives. No other resource utilization changes are expected.
Security Concerns
The contracts with executable references are explicitly owned by the reference owner contract, as it can update the executable reference entry at any time and get arbitrary access to the contract. This is the expected behavior for the intended use cases (where every contract instance is either explicitly owned by the manager, or has a high level of trust to the manager). However, unlike in the case of re-using someone else's trusted Wasm implementation, it's not safe for the developers to just inherit implementation from a manager contract. This concern may only be really solved at the level of documentation and education, as the protocol can't enforce any trust relationships between the contracts.
Test Cases
TBD
Implementation
TBD
Preamble
Discussion
0 linked threads