CAP: 0078
Title: Host functions for performing limited TTL extensions
Working Group:
Owner: Dmytro Kozhevin <@dmkozh>
Authors: Dmytro Kozhevin <@dmkozh>
Consulted:
Status: Implemented
Created: 2025-12-16
Discussion: https://github.com/orgs/stellar/discussions/1825
Protocol version: 26
Add a way to limit the maximum TTL extension for contract data and code entries.
As specified in the Preamble.
The existing TTL extension host functions (such as extend_contract_data_ttl, extend_contract_instance_and_code_ttl) provide a way to extend contract data or code entry TTL by specifying the new value of TTL that the entry has to have and a minimum TTL extension threshold for reducing the frequency of TTL extensions in case if the function is called often. This interface allows developers to specify TTL extension policies such as 'if an entry TTL is less than 29 days, extend it to have 30 days TTL'.
While the current approach allows distributing the fees among the contract users to some degree, in case if contract usage patterns are uneven, some users may end up paying much more than the others. For example, with the policy example above a user may end up paying for TTL extension anywhere between 1 day and 30 days. More fine-grained control over TTL extension would help the developers that want to prioritize the rent fee stability and fairness.
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
New host functions extend_contract_data_ttl_v2 and extend_contract_instance_and_code_ttl_v2 are introduced for extending the contract data and contract instance and/or code TTLs. The new host functions are similar to the old versions as they allows extending TTL of a contract data or code entry to the target value. Unlike the existing functions, these additionally provide an explicit way to specify the minimum TTL extension (instead of the threshold parameter in the old interface), and the maximum TTL extension (not available in the old interface).
The diff is based on commit 30ab5d1e2a642f18f3ae94cdb3a2798c3123f049 of rs-soroban-env.
diff --git a/soroban-env-common/env.json b/soroban-env-common/env.json
index 945ada2c..96efb4c8 100644
--- a/soroban-env-common/env.json
+++ b/soroban-env-common/env.json
@@ -1514,6 +1514,64 @@
"return": "AddressObject",
"docs": "Creates the contract instance on behalf of `deployer`. Created contract must be created from a Wasm that has a constructor. `deployer` must authorize this call via Soroban auth framework, i.e. this calls `deployer.require_auth` with respective arguments. `wasm_hash` must be a hash of the contract code that has already been uploaded on this network. `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": 22
+ },
+ {
+ "export": "f",
+ "name": "extend_contract_data_ttl_v2",
+ "args": [
+ {
+ "name": "k",
+ "type": "Val"
+ },
+ {
+ "name": "t",
+ "type": "StorageType"
+ },
+ {
+ "name": "extend_to",
+ "type": "U32Val"
+ },
+ {
+ "name": "min_extension",
+ "type": "U32Val"
+ },
+ {
+ "name": "max_extension",
+ "type": "U32Val"
+ }
+ ],
+ "return": "Void",
+ "docs": "Extend the contract data entry's TTL to be up to `extend_to` ledgers, where TTL is defined as `entry_live_until_ledger_seq - current_ledger_seq`. The TTL extension only actually happens if it exceeds `min_extension`, otherwise this function is a no-op. The amount of extension ledgers will not exceed `max_extension` ledgers. If attempting to extend the entry past the maximum allowed value (defined as the current ledger + `max_entry_ttl` - 1), and the entry is `Persistent`, its new `live_until_ledger_seq` will be clamped to the max; if the entry is `Temporary`, the function traps.",
+ "min_supported_protocol": 26
+ },
+ {
+ "export": "g",
+ "name": "extend_contract_instance_and_code_ttl_v2",
+ "args": [
+ {
+ "name": "contract",
+ "type": "AddressObject"
+ },
+ {
+ "name": "extension_scope",
+ "type": "ContractTTLExtension"
+ },
+ {
+ "name": "extend_to",
+ "type": "U32Val"
+ },
+ {
+ "name": "min_extension",
+ "type": "U32Val"
+ },
+ {
+ "name": "max_extension",
+ "type": "U32Val"
+ }
+ ],
+ "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 exceeds `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
}
]
},
diff --git a/soroban-env-common/src/storage_type.rs b/soroban-env-common/src/storage_type.rs
index d72886e3..3f2d0d48 100644
--- a/soroban-env-common/src/storage_type.rs
+++ b/soroban-env-common/src/storage_type.rs
@@ -32,3 +32,13 @@ impl TryFrom<StorageType> for ContractDataDurability {
}
declare_wasmi_marshal_for_enum!(StorageType);
+
+
+#[repr(u64)]
+#[derive(Debug, FromPrimitive, PartialEq, Eq, Clone, Copy)]
+pub enum ContractTTLExtension {
+ InstanceAndCode = 0,
+ Instance = 1,
+ Code = 2,
+}
+declare_wasmi_marshal_for_enum!(ContractTTLExtension);extend_contract_data_ttl_v2 adds 0 or more ledgers to the liveUntilLedgerSeq of the contract data entry defined by the ID of the current contract, key k passed in the argument and storage type t. The computation of TTL extension involves the following definitions:
liveUntilLedgerSeqis the last ledger sequence number for which the entry is still considered to be alive, after that it is considered expired.- TTL is defined as
TTL = liveUntilLedgerSeq - currentLedgerSeq, wherecurrentLedgerSeqis the sequence number of the ledger where transaction is executed. - TTL extension is defined as
TTL_ext = TTL_new - TTL_curr = liveUntilLedgerSeq_new - liveUntilLedgerSeq_curr, where_curr/_neware the values before and after executing the function respectively. maxEntryTTLis a State Archival network setting.
With these definitions, the extension algorithm is defined as follows:
- If
tisStorageType::Instance, the function traps - If
max_extension < min_extension, the function traps - If
extend_to <= TTL_currthe function returns without performing any changes - Compute the initial TTL extension
TTL_ext_init = extend_to - TTL_curr - Compute maximum extension allowed by the network
max_network_extension = maxEntryTTL - TTL_curr - If storage type
tisTemporary, andTTL_ext_init > max_network_extension, the function traps - Clamp the initial extension to not exceed network/argument limits:
TTL_ext_final = min(TTL_ext_init, max_extension, max_network_extension) - If
TTL_ext_final < min_extension, the function returns without performing any changes liveUntilLedgerSeqof the entry is set to beliveUntilLedgerSeq + TTL_ext_final
extend_contract_instance_and_code_ttl_v2 TTL extension semantics are the same as for extend_contract_data_ttl_v2. The only difference is how the ledger keys to extend are specified.
contract argument identifies the contract ID associated with the ledger entries. extension_scope is an enum argument that identifies whether contract's instance (ContractTTLExtension::Instance), code (ContractTTLExtension::Code), or both (ContractTTLExtension::InstanceAndCode) will get extended.
If a built-in contract instance is being extended (currently, only Stellar Asset contract is built-in), then the code extension requests are ignored without raising an error.
Minimum extension argument replaces threshold argument from the old TTL extension functions. Its role is still to reduce the extension frequency, which reduces the user fees (as every TTL update incurs the write fee) and the ledger write load. However, unlike threshold, min_extension specifies the minimum number of ledgers to extend the entry TTL (i.e. add to its TTL), and not the absolute TTL threshold that must be crossed. This is done for the sake of consistency with maximum extension, and provides arguably more clear way of managing the TTL extension policy. For example, a typical way to use the threshold is to prevent extensions that are shorter than e.g. one day. With threshold parameters developers needed to compute the threshold as extend_to - 1 day in ledgers. With min_extension parameter 1 day in ledgers is passed explicitly as min_extension, and there is no dependency on the extension target.
Maximum extension argument addresses the extension strategy mentioned in the 'Motivation' section. Developers may set extension strategies like 'extend TTL to 30 days with min extension of 1 day and max extension of 1 day', which would result in any user extending the entry TTL by just 1 day as long as its current TTL is anywhere between 0 and 29 days.
Extension strategies that rely on max_extension may result in relatively more frequent updates of the TTL entries. For example, with the strategy described in the previous section up to 30 extensions may happen subsequently if the entry TTL has almost expired, while without max_extension only 1 extension would happen. However, for the reasonable strategies the absolute difference is not too significant compared to the overall scale of ledger writes, and it's already possible to create spammy strategies even without utilizing max_extension by just setting the min_extension threshold to 1 ledger.
For most of the use cases setting max_extension less than extend_to for temporary entry would be a mistake, as typically lifetime of temporary entries is very sensitive and must be set precisely (for example, temporary nonce entries must not be archived until the respective signature has expired). Protocol could make setting max_extension lower than necessary to reach extend_to an error, but there may be a small fraction of use cases where the ability to use lower max_extension is actually desired. This is consistent with how threshold is treated for the old TTL extension functions: an extension may be skipped for certain threshold values.
The justification for failing when the network limit is exceeded is that the network limit might change and contract generally can't know about that, which is why it conservatively aborts execution. However, in case of the user-provided arguments there is less motivation for the protocol to make assumptions on behalf of the users.
SDK harness may be provided to reduce possibility of an error when using extension for the temporary entries.
The protocol has already accumulated several functions for extending both contract code and instance, and one of code and instance separately. In order to limit the host interface bloat and also to reduce the number of the necessary host function imports, all these variants were condensed into a single function with an additional enum argument to specify the scope of the extension.
This CAP does not introduce any backward incompatibilities. The existing TTL extension host functions will still be supported in all the future protocols as to not break the existing contracts.
Heavy use of max_extension may lead to increase of TTL writes for the protocols that use it, but the overall expected impact should be low. TTL write fees can be increased if necessary in order to encourage lowering the TTL write frequency.
This doesn't introduce any new risks.
TBD
TBD