Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions docs/build/guides/conventions/deploy-contract.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,18 @@ let deployed_address = env
- `with_address(env.current_contract_address(), salt)` specifies that the deployed contract's address is derived from the `Deployer` contract's own address and the given salt.
- `deploy_v2(wasm_hash, constructor_args)` deploys the contract using the provided Wasm bytecode hash, invokes its constructor with `constructor_args`, and returns the address of the newly deployed contract.

:::note[soroban-sdk v28]

`deploy_v2` is deprecated in soroban-sdk v28 in favour of `deploy_contract`, which takes a `ContractExecutable` instead of a bare Wasm hash. The example above targets the currently released SDK; on v28 the call becomes:

```rust
.deploy_contract(ContractExecutable::Wasm(wasm_hash), constructor_args)
```

`deploy_v2` still works and behaves exactly as before. The new form also accepts `ContractExecutable::ExternalRef(..)` to deploy against a shared, externally managed executable — see [Externally managed executables](./externally-managed-executables.mdx).

:::

```rust
deployed_address
```
Expand Down
224 changes: 224 additions & 0 deletions docs/build/guides/conventions/externally-managed-executables.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
---
title: Use an externally managed contract executable
description: Point many contracts at one shared Wasm hash with an executable reference entry, and upgrade the whole fleet with a single update
---

<head>
<title>Use an Externally Managed Contract Executable</title>
<meta charSet="utf-8" />
<meta
property="og:title"
content="Use an Externally Managed Contract Executable"
/>
<meta
property="og:description"
content="Point many contracts at one shared Wasm hash with an executable reference entry, and upgrade the whole fleet with a single update"
/>
</head>

## Overview

Normally a contract's executable is a Wasm hash stored on the contract itself, and upgrading it means calling into that one contract. An _externally managed executable_ moves that hash into a separate, shared place: an **executable reference entry**.

An executable reference entry is a persistent contract data entry, owned by a contract and keyed by a **tag**, whose value is a Wasm hash. Any contract can use another contract's executable reference entry as its own executable. When such a contract is invoked, its code is loaded from the Wasm hash the entry currently points at.

That indirection is the whole point. When the entry's owner re-points the entry at a new Wasm hash, **every contract that uses the entry as its executable runs the new code at its next invocation** — no per-contract upgrade call, no transaction per contract. This is the "beacon" pattern: one entry acts as a beacon that a whole fleet of contracts follows.

This feature is defined by [CAP-85](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0085.md), "Externally Managed Contract Executables."

### Availability

Executable reference entries require **protocol 28** and **soroban-sdk v28**.

:::note

Neither is released at the time of writing. Protocol 28 is listed as "Testnet, TBD" on the [software versions](../../../networks/software-versions.mdx) page, and the latest released Rust SDK is v27. The APIs below will not compile against soroban-sdk v27, and they will not work on Mainnet until protocol 28 is live there. Treat this guide as preparation, not as something to ship today.

:::

### When to use it

Reach for an executable reference when:

- A factory contract deploys many instances that all run the same implementation, and you want to upgrade them together.
- You are deploying a fleet of per-user, per-pool, or per-asset contracts and cannot afford one upgrade transaction per contract.

Stick with a plain `ContractExecutable::Wasm` upgrade when:

- You have a single standalone contract. The indirection buys you nothing, and the entry adds a TTL you have to keep alive. See [Upgrading Wasm bytecode for a deployed contract](./upgrading-contracts.mdx).
- You would have to reference an entry someone else owns. That hands them control over your contract's code. See [Trust](#trust) below.

## Managing an entry you own

`env.executable_refs()` manages the executable reference entries owned by the currently executing contract. A contract can only manage its own entries.

| Method | Purpose |
| --- | --- |
| `set(&tag, &wasm_hash)` | Create the entry, or re-point an existing one at a new Wasm hash. |
| `get(&tag) -> Option<BytesN<32>>` | Read the Wasm hash the entry points at. |
| `has(&tag) -> bool` | Check whether the entry exists. |
| `extend_ttl(&tag, threshold, extend_to)` | Extend the entry's TTL. |
| `extend_ttl_with_limits(&tag, extend_to, min_ext, max_ext)` | Extend the entry's TTL with bounds on the extension. |
| `get_ttl(&tag) -> u32` | Read the entry's TTL. Available under `testutils` only. |

`set` is the dangerous one: calling it on an existing entry changes the code of every contract using that entry. A real contract must restrict who can call it. Gate it behind an admin `require_auth()`:

```rust
#![no_std]

use soroban_sdk::{
contract, contractimpl, contracttype, Address, BytesN, Env, String,
};

#[contracttype]
#[derive(Clone)]
enum DataKey {
Admin,
}

#[contract]
pub struct Beacon;

#[contractimpl]
impl Beacon {
pub fn __constructor(env: Env, admin: Address) {
env.storage().instance().set(&DataKey::Admin, &admin);
}

/// Publish the executable reference entry keyed by `tag`, pointing it at
/// `wasm_hash`. If the entry already exists, every contract using it as
/// its executable runs `wasm_hash` at its next invocation, so only the
/// admin may call this.
pub fn publish(env: Env, tag: String, wasm_hash: BytesN<32>) {
let admin: Address = env.storage().instance().get(&DataKey::Admin).unwrap();
admin.require_auth();

env.executable_refs().set(&tag, &wasm_hash);
}

/// Read back the Wasm hash the entry currently points at.
pub fn published(env: Env, tag: String) -> Option<BytesN<32>> {
env.executable_refs().get(&tag)
}

/// Keep the entry alive. Anyone may call this — extending a TTL is safe.
pub fn extend(env: Env, tag: String) {
env.executable_refs().extend_ttl(&tag, 100_000, 500_000);
}
}
```

The `wasm_hash` passed to `set` must be the hash of Wasm that has **already been uploaded** with `env.deployer().upload_contract_wasm(...)` (or the `stellar contract upload` CLI command). `set` panics otherwise.

## Deploying a contract that uses a reference

Pass `ContractExecutable::ExternalRef` to `deploy_contract` instead of `ContractExecutable::Wasm`. The reference names the entry's `owner` and its `tag`:

```rust
use soroban_sdk::{
contract, contractimpl, Address, BytesN, ContractExecutable, ContractExecutableRef, Env,
String,
};

#[contract]
pub struct Factory;

#[contractimpl]
impl Factory {
/// Deploy a contract whose executable is read from the entry keyed by
/// `tag` and owned by `owner`.
pub fn deploy(env: Env, owner: Address, tag: String, salt: BytesN<32>) -> Address {
env.deployer().with_current_contract(salt).deploy_contract(
ContractExecutable::ExternalRef(ContractExecutableRef { owner, tag }),
(),
)
}
}
```

The last argument is the constructor arguments, exactly as with a Wasm deploy. Pass `()` when the contract has no constructor or a zero-argument one, or a tuple such as `(a, b)` to forward arguments to `__constructor`. As always, the deployed address is derived from the deployer address and the salt.

The referenced entry must already exist when `deploy_contract` runs, or the call panics.

## Switching a deployed contract onto a reference

An already-deployed contract can replace its own executable with a reference, the same way it would upgrade to a new Wasm hash:

```rust
use soroban_sdk::{
contract, contractimpl, Address, ContractExecutable, ContractExecutableRef, Env, String,
};

#[contract]
pub struct Joinable;

#[contractimpl]
impl Joinable {
/// Join the fleet following the entry keyed by `tag` and owned by `owner`.
/// Gate this behind the contract's own admin check in a real contract.
pub fn follow(env: Env, owner: Address, tag: String) {
env.deployer()
.update_current_contract(ContractExecutable::ExternalRef(ContractExecutableRef {
owner,
tag,
}));
}
}
```

`owner` may be the current contract itself, which is how a contract switches to an entry it owns and manages.

As with any executable update, the change does not take effect immediately — the executable is replaced only after the invocation finishes successfully. The referenced entry must exist at the time of the call, or the call panics.

## Rules and gotchas

The protocol enforces rules on executable reference entries, which is why they are managed through `env.executable_refs()` rather than the ordinary storage functions.

### Trust

:::caution

The owner of an executable reference entry controls the code of every contract that references it. Re-pointing the entry replaces those contracts' logic entirely — including any logic that guards their balances or their storage.

Reference an entry only if you own it yourself, or if you trust the owner as much as you would trust an admin key on your own contract. If you are considering referencing a third party's entry, look at who can call their `set`-equivalent function before you deploy.

:::

### The Wasm must already be uploaded

`set` requires the 32-byte hash of Wasm that is already on the ledger, uploaded via `Deployer::upload_contract_wasm`. It panics if no such Wasm exists.

### Entries can never be removed

Entries always have **persistent** durability, and **once created, an entry can never be removed**. Like any persistent entry it can be archived when its TTL expires and later restored, but there is no delete. Choose your tags deliberately — a tag you publish is a tag you own forever.

### The entry must exist at deploy or update time

Both `deploy_contract` and `update_current_contract` read the entry when they run. If the entry does not exist, the call panics. Publish the entry before you deploy anything that points at it.

### Tags do not collide with ordinary storage keys — with one caveat

Entries are stored in the owning contract's persistent storage under a protocol-defined key type, `ExecutableTag`. They do not collide with ordinary storage keys, including a `String` key holding the same value: `env.storage().persistent().set(&tag, ...)` and `env.executable_refs().set(&tag, ...)` write two different entries.

The caveat: a caller can construct an `ExecutableTag` key off-chain and pass it into a contract as a `Val`. A contract that writes caller-supplied `Val`s into persistent storage can therefore collide with its own executable reference entries. If your contract accepts untyped `Val` storage keys from callers, wrap them in your own key type rather than using them directly.

### Keep the entry's TTL alive

An executable reference entry is a persistent entry with its own TTL, and it is needed to resolve the code of every contract that references it. If it is archived, those contracts cannot be invoked until it is restored.

`Deployer::extend_ttl`, `Deployer::extend_ttl_for_code`, and `Deployer::extend_ttl_with_limits` extend the TTL of the contract instance, the code, **and any executable reference entry needed to resolve that code**, so the usual instance-and-code bumping covers the entry too. The owner can also extend the entry directly with `env.executable_refs().extend_ttl(...)`. See [Extending a contract's Wasm TTL](./extending-wasm-ttl.mdx) and [Storage strategies](../storage/storage-strategies.mdx).

## The other variant

`ContractExecutable` has two variants. `ContractExecutable::ExternalRef(ContractExecutableRef { owner, tag })` is the one described above; `ContractExecutable::Wasm(wasm_hash)` is the ordinary case, where the contract's executable is a specific uploaded Wasm blob:

```rust
env.deployer()
.with_current_contract(salt)
.deploy_contract(ContractExecutable::Wasm(wasm_hash), ());

env.deployer()
.update_current_contract(ContractExecutable::Wasm(new_wasm_hash));
```

For the plain-Wasm workflows, see [Deploy a contract from installed Wasm bytecode using a deployer contract](./deploy-contract.mdx) and [Upgrading Wasm bytecode for a deployed contract](./upgrading-contracts.mdx).
14 changes: 13 additions & 1 deletion docs/build/guides/conventions/upgrading-contracts.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,18 @@ mod test;

Source: https://github.com/stellar/soroban-examples/blob/v23.0.0/upgradeable_contract/old_contract/src/lib.rs

:::note[soroban-sdk v28]

`update_current_contract_wasm` is deprecated in soroban-sdk v28 in favour of `update_current_contract`, which takes a `ContractExecutable` instead of a bare Wasm hash. The example above targets the currently released SDK; on v28 the call becomes:

```rust
env.deployer().update_current_contract(ContractExecutable::Wasm(new_wasm_hash));
```

`update_current_contract_wasm` still works and behaves exactly as before. The new form also accepts `ContractExecutable::ExternalRef(..)`, which points the contract at an executable reference entry owned by a contract, so the owner of that entry controls which Wasm the contract runs — see [Externally managed executables](./externally-managed-executables.mdx).

:::

## How it works

When upgrading a contract, the key function used is `env.deployer().update_current_contract_wasm`, which takes the Wasm hash of the new contract as a parameter. Here's a step-by-step breakdown of how this process works:
Expand All @@ -104,7 +116,7 @@ When upgrading a contract, the key function used is `env.deployer().update_curre
- It then requires the admin's authorization (`admin.require_auth()`) to proceed.
- Finally, it updates the contract with the new Wasm bytecode (`env.deployer().update_current_contract_wasm(new_wasm_hash)`).

5. The `update_current_contract_wasm` host function will also emit a `SYSTEM` contract [event] that contains the old and new wasm reference, allowing downstream users to be notified when a contract they use is updated. The event structure will have `topics = ["executable_update", old_executable: ContractExecutable, new_executable: ContractExecutable]` and `data = []`.
5. The `update_current_contract_wasm` host function will also emit a `SYSTEM` contract [event] that contains the old and new wasm reference, allowing downstream users to be notified when a contract they use is updated. The event structure will have `topics = ["executable_update", old_executable: ContractExecutable, new_executable: ContractExecutable]` and `data = []`. From protocol 28 a `ContractExecutable` in these topics may also be an external executable reference rather than a Wasm hash.

[event]: ../../../learn/fundamentals/stellar-data-structures/events.mdx#event-types

Expand Down
2 changes: 1 addition & 1 deletion docs/build/guides/storage/storage-strategies.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -589,7 +589,7 @@ The deployment is deterministic: the salt hashes the sorted token pair, so a pai
- Isolation: pair-local entries for pair A do not conflict with pair-local entries for pair B. Transactions that touch shared token, router, or factory entries still contend.
- Sharding buys parallelism, not headroom: network-wide per-ledger resource caps apply across all contracts combined.
- Cross-entity operations become cross-contract calls (CPU + footprint per hop); a router contract usually papers over this — Soroswap's router holds exactly one storage key: the factory address.
- Fleet upgrades are real operational work (N contracts to upgrade), and each instance needs its own TTL extensions. The upgrade half will be solvable by [CAP-85](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0085.md) in protocol 28 — a beacon pattern: the fleet shares one externally managed executable, so a single update upgrades every instance.
- Fleet upgrades are real operational work (N contracts to upgrade), and each instance needs its own TTL extensions. The upgrade half will be solvable by [CAP-85](https://github.com/stellar/stellar-protocol/blob/master/core/cap-0085.md) in protocol 28 — a beacon pattern: the fleet shares one [externally managed executable](../conventions/externally-managed-executables.mdx), so a single update upgrades every instance.

**In the wild**

Expand Down
12 changes: 12 additions & 0 deletions docs/build/smart-contracts/example-contracts/deployer.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,18 @@ The `env.deployer().with_address(env.current_contract_address(), salt)` call may

The `deploy_v2()` function performs the actual deployment using the provided `wasm_hash`. The implementation of the new contract is defined by the Wasm file uploaded under `wasm_hash`. `constructor_args` are the arguments that will be passed to the constructor of the contract that is being deployed. If the deployed contract has no constructor, empty argument vector should be passed.

:::note[soroban-sdk v28]

`deploy_v2` is deprecated in soroban-sdk v28 in favour of `deploy_contract`, which takes a `ContractExecutable` instead of a bare Wasm hash. The example above targets the currently released SDK; on v28 the call becomes:

```rust
.deploy_contract(ContractExecutable::Wasm(wasm_hash), constructor_args)
```

`deploy_v2` still works and behaves exactly as before. The new form also accepts `ContractExecutable::ExternalRef(..)` to deploy against a shared, externally managed executable — see [Externally managed executables](../../guides/conventions/externally-managed-executables.mdx).

:::

:::tip

Only the `wasm_hash` itself is stored per contract ID thus saving the ledger space and fees.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ e.deployer()
.deploy_v2(token_wasm_hash, (admin, decimal, name, symbol))
```

:::note[soroban-sdk v28]

`deploy_v2` is deprecated in soroban-sdk v28 in favour of `deploy_contract`, which takes a `ContractExecutable` instead of a bare Wasm hash: `deploy_contract(ContractExecutable::Wasm(token_wasm_hash), (admin, decimal, name, symbol))`. Constructor arguments are passed the same way. See [Externally managed executables](../../../../build/guides/conventions/externally-managed-executables.mdx).

:::

In tests, the same arguments are passed to `register`:

```rust
Expand Down
2 changes: 1 addition & 1 deletion docs/learn/fundamentals/stellar-data-structures/events.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ struct TransactionMetaV4
There are three `ContractEventType`'s -

1. `CONTRACT` events are events emitted by contracts that use the `contract_event` host function to convey state changes.
2. `SYSTEM` events are events emitted by the host. At the moment, there's only one system event emitted by the host. It is emitted when the `update_current_contract_wasm` host function is called, where `topics = ["executable_update", old_executable: ContractExecutable, new_executable: ContractExecutable]` and `data = []`.
2. `SYSTEM` events are events emitted by the host. At the moment, there's only one system event emitted by the host. It is emitted when the `update_current_contract_wasm` host function is called, where `topics = ["executable_update", old_executable: ContractExecutable, new_executable: ContractExecutable]` and `data = []`. From protocol 28 a `ContractExecutable` in these topics may be an external executable reference as well as a Wasm hash.
3. `DIAGNOSTIC` events are meant for debugging and will not be emitted unless the host instance explicitly enables it. You can read more about this below.

## What are diagnosticEvents?
Expand Down
6 changes: 6 additions & 0 deletions docs/learn/migrate/evm/smart-contract-deployment.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -618,6 +618,12 @@ pub fn create_contract(
}
```

:::note[soroban-sdk v28]

`deploy_v2` is deprecated in soroban-sdk v28 in favour of `deploy_contract`, which takes a `ContractExecutable` instead of a bare Wasm hash: `deploy_contract(ContractExecutable::Wasm(token_wasm_hash), (admin, decimal, name, symbol))`. `deploy_v2` still works and behaves exactly as before. See [Externally managed executables](../../../build/guides/conventions/externally-managed-executables.mdx).

:::

</TabItem>

<TabItem value="token_interface.rs" label="src/token_interface.rs">
Expand Down
1 change: 1 addition & 0 deletions routes.txt
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
/docs/build/guides/conventions/deploy-sac-with-code
/docs/build/guides/conventions/error-enum
/docs/build/guides/conventions/extending-wasm-ttl
/docs/build/guides/conventions/externally-managed-executables
/docs/build/guides/conventions/upgrading-contracts
/docs/build/guides/conventions/wasm-metadata
/docs/build/guides/conventions/workspace
Expand Down