diff --git a/core/README.md b/core/README.md index cb6244d4b..ecb343a85 100644 --- a/core/README.md +++ b/core/README.md @@ -118,6 +118,7 @@ | [CAP-0060](cap-0060.md) | Update to Wasmi register machine| Graydon Hoare | Accepted | | [CAP-0072](cap-0072.md) | Contract signers for Stellar accounts | Dmytro Kozhevin | Draft | | [CAP-0084](cap-0084.md) | Muxed Contract Addresses | Jake Urban | Draft | +| [CAP-0085](cap-0085.md) | Externally managed contract executables | Dmytro Kozhevin | Draft | ### Rejected Proposals | Number | Title | Author | Status | diff --git a/core/cap-0085.md b/core/cap-0085.md new file mode 100644 index 000000000..fcdce11b6 --- /dev/null +++ b/core/cap-0085.md @@ -0,0 +1,316 @@ +``` +CAP: 0085 +Title: Externally managed contract executables +Working Group: + Owner: Dmytro Kozhevin <@dmkozh> + Authors: Dmytro Kozhevin <@dmkozh>, Alex Mootz <@mootz12> + Consulted: +Status: Draft +Created: 2026-06-30 +Discussion: https://github.com/orgs/stellar/discussions/1969 +Protocol version: TBD +``` + +## 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](https://docs.openzeppelin.com/contracts/5.x/api/proxy#beacon) 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 mddiffcheck.base=68fa1ac55692f68ad2a2ca549d0a283273554439 +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 mddiffcheck.ignore=true +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_data` host function will always panic. +- `put_contract_data` host function will perform the durability and value validation. Durability must be persistent. The value must be a `BytesObject` containing 32-bytes hash of an existing Wasm contract, i.e. an additional storage lookup will be performed to ensure that the respective `ContractCode` entry 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: + +```rust +#[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()), (SCSymbol("tag"), SCString())])])`. + +### 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](#contractexecutable-enum-update), 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` host function is meant to extend the TTL of all the necessary entries to keep the current contract accessible and executable. Thus, for the contracts with executable reference, the function will extend the TTL of the current contract instance entry, 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. + +### `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](#contractexecutable-enum-update) 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` & `fallback` primitives - `delegatecall` provides contract with an ability to call into another contract's code, and `fallback` allows 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 `Val` as 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](#contractexecutable-update-for-custom-accounts), 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 `Val`s. + +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