瀏覽代碼

Soroban: Add `extendTtl` Builtin Method (#1709)

This PR adds support for `extendPersistentTtl()` method in Soroban,
along with a test and documentation. Also, the Soroban testing
infrastructure has been refactored to allow more flexible environment
manipulation.

[Documentation for
`extend_ttl`](https://developers.stellar.org/docs/build/smart-contracts/getting-started/storing-data#managing-contract-data-ttls-with-extend_ttl)
Fixes #1669

#### Changes 
- Added support for `extendPersistentTtl` as a method on `uint64` for
the Soroban target.
- In the Soroban SDK, `extend_ttl` is a generic function (`IntoVal<Env,
Val>`)
```rust
pub fn extend_ttl<K>(&self, key: &K, threshold: u32, extend_to: u32)
where
    K: IntoVal<Env, Val>,
```
but Solidity does not support that, so it's implemented as a method
instead.
- One assertion in the `different_storage_types` test is affected due to
changes in diagnostic capture. A follow-up PR will address this.

---------

Signed-off-by: Tarek <tareknaser360@gmail.com>
Tarek Elsayed 9 月之前
父節點
當前提交
fe96d0dab3

+ 44 - 0
docs/language/builtins.rst

@@ -666,3 +666,47 @@ Assuming `arg1` is 512 and `arg2` is 196, the output to the log will be ``foo en
     When formatting integers in to decimals, types larger than 64 bits require expensive division.
     Be mindful this will increase the gas cost. Larger values will incur a higher gas cost.
     Alternatively, use a hexadecimal ``{:x}`` format specifier to reduce the cost.
+
+
+extendTtl(uint32 threshold, uint32 extend_to) 
++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+
+The ``extendTtl()`` method allows extending the time-to-live (TTL) of a contract storage entry.
+
+If the entry's TTL is below threshold ledgers, this function updates ``live_until_ledger_seq`` such that TTL equals ``extend_to``. The TTL is defined as:
+
+.. math::
+
+TTL = live_until_ledger_seq - current_ledger
+
+
+.. note:: This method is only available on the Soroban target
+
+.. code-block:: solidity
+
+    /// Extends the TTL for the `count` persistent key to 5000 ledgers
+    /// if the current TTL is smaller than 1000 ledgers
+    function extend_ttl() public view returns (int64) {
+        return count.extendTtl(1000, 5000);
+    }
+
+
+
+For more details on managing contract data TTLs in Soroban, refer to the docs for `TTL <https://developers.stellar.org/docs/build/smart-contracts/getting-started/storing-data#managing-contract-data-ttls-with-extend_ttl>`_.
+
+extendInstanceTtl(uint32 threshold, uint32 extend_to)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++
+
+The extendInstanceTtl() function extends the time-to-live (TTL) of contract instance storage.
+
+If the TTL for the current contract instance and code (if applicable) is below threshold ledgers, this function extends ``live_until_ledger_seq`` such that TTL equals ``extend_to``.
+
+.. note:: This is a global function, not a method, and is only available on the Soroban target
+
+.. code-block:: solidity
+
+    /// Extends the TTL for the contract instance storage to 10000 ledgers
+    /// if the current TTL is smaller than 2000 ledgers
+    function extendInstanceTtl() public view returns (int64) {
+        return extendInstanceTtl(2000, 10000);
+    }

+ 38 - 0
src/codegen/expression.rs

@@ -30,6 +30,7 @@ use crate::sema::{
     expression::ResolveTo,
 };
 use crate::Target;
+use core::panic;
 use num_bigint::{BigInt, Sign};
 use num_traits::{FromPrimitive, One, ToPrimitive, Zero};
 use solang_parser::pt::{self, CodeLocation, Loc};
@@ -2327,6 +2328,43 @@ fn expr_builtin(
 
             code(loc, *contract_no, ns, opt)
         }
+        ast::Builtin::ExtendTtl => {
+            let mut arguments: Vec<Expression> = args
+                .iter()
+                .map(|v| expression(v, cfg, contract_no, func, ns, vartab, opt))
+                .collect();
+
+            // var_no is the first argument of the builtin
+            let var_no = match arguments[0].clone() {
+                Expression::NumberLiteral { value, .. } => value,
+                _ => panic!("First argument of extendTtl() must be a number literal"),
+            }
+            .to_usize()
+            .expect("Unable to convert var_no to usize");
+            let var = ns.contracts[contract_no].variables.get(var_no).unwrap();
+            let storage_type_usize = match var
+            .storage_type
+            .clone()
+            .expect("Unable to get storage type") {
+                solang_parser::pt::StorageType::Temporary(_) => 0,
+                solang_parser::pt::StorageType::Persistent(_) => 1,
+                solang_parser::pt::StorageType::Instance(_) => panic!("Calling extendTtl() on instance storage is not allowed. Use `extendInstanceTtl()` instead."),
+            };
+
+            // append the storage type to the arguments
+            arguments.push(Expression::NumberLiteral {
+                loc: *loc,
+                ty: Type::Uint(32),
+                value: BigInt::from(storage_type_usize),
+            });
+
+            Expression::Builtin {
+                loc: *loc,
+                tys: tys.to_vec(),
+                kind: (&builtin).into(),
+                args: arguments,
+            }
+        }
         _ => {
             let arguments: Vec<Expression> = args
                 .iter()

+ 8 - 0
src/codegen/mod.rs

@@ -97,6 +97,8 @@ impl From<inkwell::OptimizationLevel> for OptimizationLevel {
 pub enum HostFunctions {
     PutContractData,
     GetContractData,
+    ExtendContractDataTtl,
+    ExtendCurrentContractInstanceAndCodeTtl,
     LogFromLinearMemory,
     SymbolNewFromLinearMemory,
     VectorNew,
@@ -111,6 +113,8 @@ impl HostFunctions {
         match self {
             HostFunctions::PutContractData => "l._",
             HostFunctions::GetContractData => "l.1",
+            HostFunctions::ExtendContractDataTtl => "l.7",
+            HostFunctions::ExtendCurrentContractInstanceAndCodeTtl => "l.8",
             HostFunctions::LogFromLinearMemory => "x._",
             HostFunctions::SymbolNewFromLinearMemory => "b.j",
             HostFunctions::VectorNew => "v._",
@@ -1794,6 +1798,8 @@ pub enum Builtin {
     WriteUint256LE,
     WriteBytes,
     Concat,
+    ExtendTtl,
+    ExtendInstanceTtl,
 }
 
 impl From<&ast::Builtin> for Builtin {
@@ -1856,6 +1862,8 @@ impl From<&ast::Builtin> for Builtin {
             ast::Builtin::PrevRandao => Builtin::PrevRandao,
             ast::Builtin::ContractCode => Builtin::ContractCode,
             ast::Builtin::StringConcat | ast::Builtin::BytesConcat => Builtin::Concat,
+            ast::Builtin::ExtendTtl => Builtin::ExtendTtl,
+            ast::Builtin::ExtendInstanceTtl => Builtin::ExtendInstanceTtl,
             _ => panic!("Builtin should not be in the cfg"),
         }
     }

+ 4 - 0
src/codegen/tests.rs

@@ -59,6 +59,8 @@ fn test_builtin_conversion() {
         ast::Builtin::WriteUint256LE,
         ast::Builtin::WriteString,
         ast::Builtin::WriteBytes,
+        ast::Builtin::ExtendTtl,
+        ast::Builtin::ExtendInstanceTtl,
     ];
 
     let output: Vec<codegen::Builtin> = vec![
@@ -115,6 +117,8 @@ fn test_builtin_conversion() {
         codegen::Builtin::WriteUint256LE,
         codegen::Builtin::WriteBytes,
         codegen::Builtin::WriteBytes,
+        codegen::Builtin::ExtendTtl,
+        codegen::Builtin::ExtendInstanceTtl,
     ];
 
     for (i, item) in input.iter().enumerate() {

+ 15 - 0
src/emit/soroban/mod.rs

@@ -39,6 +39,19 @@ impl HostFunctions {
                 .context
                 .i64_type()
                 .fn_type(&[ty.into(), ty.into()], false),
+            // https://github.com/stellar/stellar-protocol/blob/2fdc77302715bc4a31a784aef1a797d466965024/core/cap-0046-03.md#ledger-host-functions-mod-l
+            // ;; If the entry's TTL is below `threshold` ledgers, extend `live_until_ledger_seq` such that TTL == `extend_to`, where TTL is defined as live_until_ledger_seq - current ledger.
+            // (func $extend_contract_data_ttl (param $k_val i64) (param $t_storage_type i64) (param $threshold_u32_val i64) (param $extend_to_u32_val i64) (result i64))
+            HostFunctions::ExtendContractDataTtl => bin
+                .context
+                .i64_type()
+                .fn_type(&[ty.into(), ty.into(), ty.into(), ty.into()], false),
+            // ;; If the TTL for the current contract instance and code (if applicable) is below `threshold` ledgers, extend `live_until_ledger_seq` such that TTL == `extend_to`, where TTL is defined as live_until_ledger_seq - current ledger.
+            // (func $extend_current_contract_instance_and_code_ttl (param $threshold_u32_val i64) (param $extend_to_u32_val i64) (result i64))
+            HostFunctions::ExtendCurrentContractInstanceAndCodeTtl => bin
+                .context
+                .i64_type()
+                .fn_type(&[ty.into(), ty.into()], false),
             HostFunctions::LogFromLinearMemory => bin
                 .context
                 .i64_type()
@@ -279,6 +292,8 @@ impl SorobanTarget {
         let host_functions = [
             HostFunctions::PutContractData,
             HostFunctions::GetContractData,
+            HostFunctions::ExtendContractDataTtl,
+            HostFunctions::ExtendCurrentContractInstanceAndCodeTtl,
             HostFunctions::LogFromLinearMemory,
             HostFunctions::SymbolNewFromLinearMemory,
             HostFunctions::VectorNew,

+ 150 - 1
src/emit/soroban/target.rs

@@ -1,6 +1,7 @@
 // SPDX-License-Identifier: Apache-2.0
 
 use crate::codegen::cfg::HashTy;
+use crate::codegen::Builtin;
 use crate::codegen::Expression;
 use crate::emit::binary::Binary;
 use crate::emit::soroban::{HostFunctions, SorobanTarget};
@@ -19,6 +20,8 @@ use inkwell::values::{
 
 use solang_parser::pt::{Loc, StorageType};
 
+use num_traits::ToPrimitive;
+
 use std::collections::HashMap;
 
 // TODO: Implement TargetRuntime for SorobanTarget.
@@ -460,7 +463,153 @@ impl<'a> TargetRuntime<'a> for SorobanTarget {
         function: FunctionValue<'b>,
         ns: &Namespace,
     ) -> BasicValueEnum<'b> {
-        unimplemented!()
+        emit_context!(bin);
+
+        match expr {
+            Expression::Builtin {
+                kind: Builtin::ExtendTtl,
+                args,
+                ..
+            } => {
+                // Get arguments
+                // (func $extend_contract_data_ttl (param $k_val i64) (param $t_storage_type i64) (param $threshold_u32_val i64) (param $extend_to_u32_val i64) (result i64))
+                assert_eq!(args.len(), 4, "extendTtl expects 4 arguments");
+                // SAFETY: We already checked that the length of args is 4 so it is safe to unwrap here
+                let slot_no = match args.first().unwrap() {
+                    Expression::NumberLiteral { value, .. } => value,
+                    _ => panic!(
+                        "Expected slot_no to be of type Expression::NumberLiteral. Actual: {:?}",
+                        args.get(1).unwrap()
+                    ),
+                }
+                .to_u64()
+                .unwrap();
+                let threshold = match args.get(1).unwrap() {
+                    Expression::NumberLiteral { value, .. } => value,
+                    _ => panic!(
+                        "Expected threshold to be of type Expression::NumberLiteral. Actual: {:?}",
+                        args.get(1).unwrap()
+                    ),
+                }
+                .to_u64()
+                .unwrap();
+                let extend_to = match args.get(2).unwrap() {
+                    Expression::NumberLiteral { value, .. } => value,
+                    _ => panic!(
+                        "Expected extend_to to be of type Expression::NumberLiteral. Actual: {:?}",
+                        args.get(2).unwrap()
+                    ),
+                }
+                .to_u64()
+                .unwrap();
+                let storage_type = match args.get(3).unwrap() {
+                    Expression::NumberLiteral { value, .. } => value,
+                    _ => panic!(
+                    "Expected storage_type to be of type Expression::NumberLiteral. Actual: {:?}",
+                    args.get(3).unwrap()
+                ),
+                }
+                .to_u64()
+                .unwrap();
+
+                // Encode the values (threshold and extend_to)
+                // See: https://github.com/stellar/stellar-protocol/blob/master/core/cap-0046-01.md#tag-values
+                let threshold_u32_val = (threshold << 32) + 4;
+                let extend_to_u32_val = (extend_to << 32) + 4;
+
+                // Call the function
+                let function_name = HostFunctions::ExtendContractDataTtl.name();
+                let function_value = bin.module.get_function(function_name).unwrap();
+
+                let value = bin
+                    .builder
+                    .build_call(
+                        function_value,
+                        &[
+                            bin.context.i64_type().const_int(slot_no, false).into(),
+                            bin.context.i64_type().const_int(storage_type, false).into(),
+                            bin.context
+                                .i64_type()
+                                .const_int(threshold_u32_val, false)
+                                .into(),
+                            bin.context
+                                .i64_type()
+                                .const_int(extend_to_u32_val, false)
+                                .into(),
+                        ],
+                        function_name,
+                    )
+                    .unwrap()
+                    .try_as_basic_value()
+                    .left()
+                    .unwrap()
+                    .into_int_value();
+
+                value.into()
+            }
+            Expression::Builtin {
+                kind: Builtin::ExtendInstanceTtl,
+                args,
+                ..
+            } => {
+                // Get arguments
+                // (func $extend_contract_data_ttl (param $k_val i64) (param $t_storage_type i64) (param $threshold_u32_val i64) (param $extend_to_u32_val i64) (result i64))
+                assert_eq!(args.len(), 2, "extendTtl expects 2 arguments");
+                // SAFETY: We already checked that the length of args is 2 so it is safe to unwrap here
+                let threshold = match args.first().unwrap() {
+                    Expression::NumberLiteral { value, .. } => value,
+                    _ => panic!(
+                        "Expected threshold to be of type Expression::NumberLiteral. Actual: {:?}",
+                        args.get(1).unwrap()
+                    ),
+                }
+                .to_u64()
+                .unwrap();
+                let extend_to = match args.get(1).unwrap() {
+                    Expression::NumberLiteral { value, .. } => value,
+                    _ => panic!(
+                        "Expected extend_to to be of type Expression::NumberLiteral. Actual: {:?}",
+                        args.get(2).unwrap()
+                    ),
+                }
+                .to_u64()
+                .unwrap();
+
+                // Encode the values (threshold and extend_to)
+                // See: https://github.com/stellar/stellar-protocol/blob/master/core/cap-0046-01.md#tag-values
+                let threshold_u32_val = (threshold << 32) + 4;
+                let extend_to_u32_val = (extend_to << 32) + 4;
+
+                // Call the function
+                let function_name = HostFunctions::ExtendCurrentContractInstanceAndCodeTtl.name();
+                let function_value = bin.module.get_function(function_name).unwrap();
+
+                let value = bin
+                    .builder
+                    .build_call(
+                        function_value,
+                        &[
+                            bin.context
+                                .i64_type()
+                                .const_int(threshold_u32_val, false)
+                                .into(),
+                            bin.context
+                                .i64_type()
+                                .const_int(extend_to_u32_val, false)
+                                .into(),
+                        ],
+                        function_name,
+                    )
+                    .unwrap()
+                    .try_as_basic_value()
+                    .left()
+                    .unwrap()
+                    .into_int_value();
+
+                value.into()
+            }
+            _ => unimplemented!("unsupported builtin"),
+        }
     }
 
     /// Return the return data from an external call (either revert error or return values)

+ 2 - 0
src/sema/ast.rs

@@ -1779,6 +1779,8 @@ pub enum Builtin {
     TypeInterfaceId,
     TypeRuntimeCode,
     TypeCreatorCode,
+    ExtendTtl,
+    ExtendInstanceTtl,
 }
 
 #[derive(PartialEq, Eq, Clone, Debug)]

+ 25 - 2
src/sema/builtin.rs

@@ -36,8 +36,19 @@ pub struct Prototype {
 }
 
 // A list of all Solidity builtins functions
-pub static BUILTIN_FUNCTIONS: Lazy<[Prototype; 27]> = Lazy::new(|| {
+pub static BUILTIN_FUNCTIONS: Lazy<[Prototype; 28]> = Lazy::new(|| {
     [
+        Prototype {
+            builtin: Builtin::ExtendInstanceTtl,
+            namespace: None,
+            method: vec![],
+            name: "extendInstanceTtl",  
+            params: vec![Type::Uint(32), Type::Uint(32)],
+            ret: vec![Type::Int(64)],
+            target: vec![Target::Soroban],
+            doc: "If the TTL for the current contract instance and code (if applicable) is below `threshold` ledgers, extend `live_until_ledger_seq` such that TTL == `extend_to`, where TTL is defined as live_until_ledger_seq - current ledger.",
+            constant: false,
+        },
         Prototype {
             builtin: Builtin::Assert,
             namespace: None,
@@ -547,8 +558,20 @@ pub static BUILTIN_VARIABLE: Lazy<[Prototype; 17]> = Lazy::new(|| {
 });
 
 // A list of all Solidity builtins methods
-pub static BUILTIN_METHODS: Lazy<[Prototype; 27]> = Lazy::new(|| {
+pub static BUILTIN_METHODS: Lazy<[Prototype; 28]> = Lazy::new(|| {
     [
+        Prototype {
+            builtin: Builtin::ExtendTtl,
+            namespace: None,
+            // FIXME: For now as a PoC, we are only supporting this method for type `uint64`
+            method: vec![Type::StorageRef(false, Box::new(Type::Uint(64)))],
+            name: "extendTtl",
+            params: vec![Type::Uint(32), Type::Uint(32)], // Parameters `threshold` and `extend_to` of type `uint32`
+            ret: vec![Type::Int(64)],
+            target: vec![Target::Soroban],
+            doc: "If the entry's TTL is below `threshold` ledgers, extend `live_until_ledger_seq` such that TTL == `extend_to`, where TTL is defined as live_until_ledger_seq - current ledger.",
+            constant: false,
+        },
         Prototype {
             builtin: Builtin::ReadInt8,
             namespace: None,

+ 15 - 3
tests/soroban.rs

@@ -19,9 +19,13 @@ pub struct SorobanEnv {
     compiler_diagnostics: Diagnostics,
 }
 
-pub fn build_solidity(src: &str) -> SorobanEnv {
+pub fn build_solidity<F>(src: &str, configure_env: F) -> SorobanEnv
+where
+    F: FnOnce(&mut SorobanEnv),
+{
     let (wasm_blob, ns) = build_wasm(src);
-    SorobanEnv::new_with_contract(wasm_blob).insert_diagnostics(ns.diagnostics)
+
+    SorobanEnv::new_with_contract(wasm_blob, configure_env).insert_diagnostics(ns.diagnostics)
 }
 
 fn build_wasm(src: &str) -> (Vec<u8>, Namespace) {
@@ -64,9 +68,15 @@ impl SorobanEnv {
         self
     }
 
-    pub fn new_with_contract(contract_wasm: Vec<u8>) -> Self {
+    pub fn new_with_contract<F>(contract_wasm: Vec<u8>, configure_env: F) -> Self
+    where
+        F: FnOnce(&mut SorobanEnv),
+    {
         let mut env = Self::new();
+        configure_env(&mut env);
+
         env.register_contract(contract_wasm);
+
         env
     }
 
@@ -88,6 +98,8 @@ impl SorobanEnv {
             args_soroban.push_back(arg)
         }
         println!("args_soroban: {:?}", args_soroban);
+        // To avoid running out of fuel
+        self.env.cost_estimate().budget().reset_unlimited();
         self.env.invoke_contract(addr, &func, args_soroban)
     }
 

+ 1 - 0
tests/soroban_testcases/cross_contract_calls.rs

@@ -15,6 +15,7 @@ fn simple_cross_contract() {
             }
         }
     }"#,
+        |_| {},
     );
 
     let caller = runtime.deploy_contract(

+ 2 - 0
tests/soroban_testcases/math.rs

@@ -15,6 +15,7 @@ fn math() {
             }
         }
     }"#,
+        |_| {},
     );
 
     let arg: Val = 5_u64.into_val(&runtime.env);
@@ -56,6 +57,7 @@ fn math_same_name() {
         }
     }
     "#,
+        |_| {},
     );
 
     let addr = src.contracts.last().unwrap();

+ 1 - 0
tests/soroban_testcases/mod.rs

@@ -3,3 +3,4 @@ mod cross_contract_calls;
 mod math;
 mod print;
 mod storage;
+mod ttl;

+ 3 - 0
tests/soroban_testcases/print.rs

@@ -14,6 +14,7 @@ fn log_runtime_error() {
                 return count;
             }
         }"#,
+        |_| {},
     );
 
     let addr = src.contracts.last().unwrap();
@@ -34,6 +35,7 @@ fn print() {
                 print("Hello, World!");
             }
         }"#,
+        |_| {},
     );
 
     let addr = src.contracts.last().unwrap();
@@ -57,6 +59,7 @@ fn print_then_runtime_error() {
                 return count;
             }
         }"#,
+        |_| {},
     );
 
     let addr = src.contracts.last().unwrap();

+ 2 - 0
tests/soroban_testcases/storage.rs

@@ -19,6 +19,7 @@ fn counter() {
                 return count;
             }
         }"#,
+        |_| {},
     );
 
     let addr = src.contracts.last().unwrap();
@@ -62,6 +63,7 @@ fn different_storage_types() {
         sesa3--;
     }
 }"#,
+        |_| {},
     );
 
     let addr = src.contracts.last().unwrap();

+ 381 - 0
tests/soroban_testcases/ttl.rs

@@ -0,0 +1,381 @@
+// SPDX-License-Identifier: Apache-2.0
+
+use crate::build_solidity;
+use soroban_sdk::testutils::storage::{Instance, Persistent, Temporary};
+use soroban_sdk::testutils::Ledger;
+
+#[test]
+fn ttl_basic_persistent() {
+    let runtime = build_solidity(
+        r#"contract counter {
+            /// Variable to track the count. Stored in persistent storage
+            uint64 public persistent count = 11;
+
+            /// Extends the TTL for the `count` persistent key to 5000 ledgers
+            /// if the current TTL is smaller than 1000 ledgers
+            function extend_ttl() public view returns (int64) {
+                return count.extendTtl(1000, 5000);
+            }
+        }"#,
+        |env| {
+            env.env.ledger().with_mut(|li| {
+                // Current ledger sequence - the TTL is the number of
+                // ledgers from the `sequence_number` (exclusive) until
+                // the last ledger sequence where entry is still considered
+                // alive.
+                li.sequence_number = 100_000;
+                // Minimum TTL for persistent entries - new persistent (and instance)
+                // entries will have this TTL when created.
+                li.min_persistent_entry_ttl = 500;
+            });
+        },
+    );
+
+    let addr = runtime.contracts.last().unwrap();
+
+    // initial TTL
+    runtime.env.as_contract(addr, || {
+        // There is only one key in the persistent storage
+        let key = runtime
+            .env
+            .storage()
+            .persistent()
+            .all()
+            .keys()
+            .first()
+            .unwrap();
+        assert_eq!(runtime.env.storage().persistent().get_ttl(&key), 499);
+    });
+
+    // Extend persistent entry TTL to 5000 ledgers - now it is 5000.
+    runtime.invoke_contract(addr, "extend_ttl", vec![]);
+
+    runtime.env.as_contract(addr, || {
+        // There is only one key in the persistent storage
+        let key = runtime
+            .env
+            .storage()
+            .persistent()
+            .all()
+            .keys()
+            .first()
+            .unwrap();
+        assert_eq!(runtime.env.storage().persistent().get_ttl(&key), 5000);
+    });
+}
+
+#[test]
+fn ttl_basic_temporary() {
+    let runtime = build_solidity(
+        r#"contract temp_counter {
+            /// Variable stored in temporary storage
+            uint64 temporary tempCount = 7;
+
+            /// Extend the temporary entry TTL to become at least 7000 ledgers,
+            /// when its TTL is smaller than 3000 ledgers.
+            function extend_temp_ttl() public view returns (int64) {
+                return tempCount.extendTtl(3000, 7000);
+            }
+        }"#,
+        |env| {
+            env.env.ledger().with_mut(|li| {
+                // Current ledger sequence - the TTL is the number of
+                // ledgers from the `sequence_number` (exclusive) until
+                // the last ledger sequence where entry is still considered
+                // alive.
+                li.sequence_number = 100_000;
+                // Minimum TTL for temporary entries - new temporary
+                // entries will have this TTL when created.
+                li.min_temp_entry_ttl = 100;
+            });
+        },
+    );
+
+    let addr = runtime.contracts.last().unwrap();
+
+    // initial TTL
+    runtime.env.as_contract(addr, || {
+        let key = runtime
+            .env
+            .storage()
+            .temporary()
+            .all()
+            .keys()
+            .first()
+            .unwrap();
+        assert_eq!(runtime.env.storage().temporary().get_ttl(&key), 99);
+    });
+
+    // Extend temporary entry TTL to 7000 ledgers - now it is 7000.
+    runtime.invoke_contract(addr, "extend_temp_ttl", vec![]);
+
+    runtime.env.as_contract(addr, || {
+        let key = runtime
+            .env
+            .storage()
+            .temporary()
+            .all()
+            .keys()
+            .first()
+            .unwrap();
+        assert_eq!(runtime.env.storage().temporary().get_ttl(&key), 7000);
+    });
+}
+
+#[test]
+#[should_panic(
+    expected = "Calling extendTtl() on instance storage is not allowed. Use `extendInstanceTtl()` instead."
+)]
+fn ttl_instance_wrong() {
+    let _runtime = build_solidity(
+        r#"contract instance_counter {
+            uint64 instance instanceCount = 3;
+            
+            function extendInstanceTtl() public view returns (int64) {
+                return instanceCount.extendTtl(700, 3000);
+            }
+        }"#,
+        |env| {
+            env.env.ledger().with_mut(|li| {
+                li.sequence_number = 100_000;
+            });
+        },
+    );
+}
+
+#[test]
+fn ttl_instance_correct() {
+    let runtime = build_solidity(
+        r#"contract instance_counter {
+            /// Variable stored in instance storage
+            uint64 instance instanceCount = 3;
+
+            /// Extends the TTL for the instance storage to 10000 ledgers
+            /// if the current TTL is smaller than 2000 ledgers
+            function extendInstanceTtl() public view returns (int64) {
+                return extendInstanceTtl(2000, 10000);
+            }
+        }"#,
+        |env| {
+            env.env.ledger().with_mut(|li| {
+                // Current ledger sequence - the TTL is the number of
+                // ledgers from the `sequence_number` (exclusive) until
+                // the last ledger sequence where entry is still considered
+                // alive.
+                li.sequence_number = 100_000;
+                // Minimum TTL for persistent entries - new persistent (and instance)
+                // entries will have this TTL when created.
+                li.min_persistent_entry_ttl = 500;
+                // Minimum TTL for temporary entries - new temporary
+                // entries will have this TTL when created.
+                li.min_temp_entry_ttl = 100;
+                // Maximum TTL of any entry. Note, that entries can have their TTL
+                // extended indefinitely, but each extension can be at most
+                // `max_entry_ttl` ledger from the current `sequence_number`.
+                li.max_entry_ttl = 15000;
+            });
+        },
+    );
+
+    let addr = runtime.contracts.last().unwrap();
+
+    // Initial TTL for instance storage
+    runtime.env.as_contract(addr, || {
+        assert_eq!(runtime.env.storage().instance().get_ttl(), 499);
+    });
+
+    // Extend instance TTL to 10000 ledgers
+    runtime.invoke_contract(addr, "extendInstanceTtl", vec![]);
+    runtime.env.as_contract(addr, || {
+        assert_eq!(runtime.env.storage().instance().get_ttl(), 10000);
+    });
+}
+
+/// This test is adapted from
+/// [Stellar Soroban Examples](https://github.com/stellar/soroban-examples/blob/f595fb5df06058ec0b9b829e9e4d0fe0513e0aa8/ttl).
+#[test]
+fn ttl_combined() {
+    let runtime = build_solidity(
+        r#"
+        contract ttl_storage {
+            uint64 public persistent pCount = 11;
+            uint64 temporary tCount = 7;
+            uint64 instance iCount = 3;
+
+            function extend_persistent_ttl() public view returns (int64) {
+                return pCount.extendTtl(1000, 5000);
+            }
+
+            function extend_temp_ttl() public view returns (int64) {
+                return tCount.extendTtl(3000, 7000);
+            }
+
+            function extendInstanceTtl() public view returns (int64) {
+                return extendInstanceTtl(2000, 10000);
+            }
+        }"#,
+        |env| {
+            env.env.ledger().with_mut(|li| {
+                li.sequence_number = 100_000;
+                li.min_persistent_entry_ttl = 500;
+                li.min_temp_entry_ttl = 100;
+                li.max_entry_ttl = 15000;
+            });
+        },
+    );
+
+    let addr = runtime.contracts.last().unwrap();
+
+    // Verify initial TTLs
+    runtime.env.as_contract(addr, || {
+        let pkey = runtime
+            .env
+            .storage()
+            .persistent()
+            .all()
+            .keys()
+            .first()
+            .unwrap();
+        let tkey = runtime
+            .env
+            .storage()
+            .temporary()
+            .all()
+            .keys()
+            .first()
+            .unwrap();
+        assert_eq!(runtime.env.storage().persistent().get_ttl(&pkey), 499);
+        assert_eq!(runtime.env.storage().instance().get_ttl(), 499);
+        assert_eq!(runtime.env.storage().temporary().get_ttl(&tkey), 99);
+    });
+
+    // Extend persistent storage TTL
+    runtime.invoke_contract(addr, "extend_persistent_ttl", vec![]);
+    runtime.env.as_contract(addr, || {
+        let pkey = runtime
+            .env
+            .storage()
+            .persistent()
+            .all()
+            .keys()
+            .first()
+            .unwrap();
+        assert_eq!(runtime.env.storage().persistent().get_ttl(&pkey), 5000);
+    });
+
+    // Extend instance storage TTL
+    runtime.invoke_contract(addr, "extendInstanceTtl", vec![]);
+    runtime.env.as_contract(addr, || {
+        assert_eq!(runtime.env.storage().instance().get_ttl(), 10000);
+    });
+
+    // Extend temporary storage TTL
+    runtime.invoke_contract(addr, "extend_temp_ttl", vec![]);
+    runtime.env.as_contract(addr, || {
+        let tkey = runtime
+            .env
+            .storage()
+            .temporary()
+            .all()
+            .keys()
+            .first()
+            .unwrap();
+        assert_eq!(runtime.env.storage().temporary().get_ttl(&tkey), 7000);
+    });
+
+    // Bump ledger sequence by 5000
+    runtime.env.ledger().with_mut(|li| {
+        li.sequence_number = 105_000;
+    });
+
+    // Verify TTL after ledger increment
+    runtime.env.as_contract(addr, || {
+        let pkey = runtime
+            .env
+            .storage()
+            .persistent()
+            .all()
+            .keys()
+            .first()
+            .unwrap();
+        let tkey = runtime
+            .env
+            .storage()
+            .temporary()
+            .all()
+            .keys()
+            .first()
+            .unwrap();
+        assert_eq!(runtime.env.storage().persistent().get_ttl(&pkey), 0);
+        assert_eq!(runtime.env.storage().instance().get_ttl(), 5000);
+        assert_eq!(runtime.env.storage().temporary().get_ttl(&tkey), 2000);
+    });
+
+    // Re-extend all TTLs
+    runtime.invoke_contract(addr, "extend_persistent_ttl", vec![]);
+    runtime.invoke_contract(addr, "extendInstanceTtl", vec![]);
+    runtime.invoke_contract(addr, "extend_temp_ttl", vec![]);
+
+    // Final TTL verification
+    runtime.env.as_contract(addr, || {
+        let pkey = runtime
+            .env
+            .storage()
+            .persistent()
+            .all()
+            .keys()
+            .first()
+            .unwrap();
+        let tkey = runtime
+            .env
+            .storage()
+            .temporary()
+            .all()
+            .keys()
+            .first()
+            .unwrap();
+        assert_eq!(runtime.env.storage().persistent().get_ttl(&pkey), 5000);
+        assert_eq!(runtime.env.storage().instance().get_ttl(), 5000); // Threshold not met, remains the same
+        assert_eq!(runtime.env.storage().temporary().get_ttl(&tkey), 7000);
+    });
+}
+
+#[test]
+#[should_panic(expected = "[testing-only] Accessed contract instance key that has been archived.")]
+fn test_persistent_entry_archival() {
+    let runtime = build_solidity(
+        r#"
+        contract persistent_cleanup {
+            uint64 public persistent pCount = 11;
+
+            function extend_persistent_ttl() public view returns (int64) {
+                return pCount.extendTtl(1000, 10000);
+            }
+
+            function extendInstanceTtl() public view returns (int64) {
+                return extendInstanceTtl(2000, 10000);
+            }
+        }"#,
+        |env| {
+            env.env.ledger().with_mut(|li| {
+                li.sequence_number = 100_000;
+                li.min_persistent_entry_ttl = 500;
+                li.min_temp_entry_ttl = 100;
+                li.max_entry_ttl = 15000;
+            });
+        },
+    );
+
+    let addr = runtime.contracts.last().unwrap();
+
+    // Extend instance TTL
+    runtime.invoke_contract(addr, "extendInstanceTtl", vec![]);
+
+    // Bump ledger sequence by 10001 (one past persistent TTL)
+    runtime.env.ledger().with_mut(|li| {
+        li.sequence_number = 110_001;
+    });
+
+    // This should panic as the persistent entry is archived
+    runtime.invoke_contract(addr, "extend_persistent_ttl", vec![]);
+}