Rust Smart Contract (Stellar timelock)

Let’s pick the code apart for a timelock smart contract and with a bit of help from chatGPT and the Stellar documentation we’ll make sure we understand every part of it.

https://github.com/stellar/soroban-examples/blob/main/timelock/src/lib.rs

alice-and-bob

Timelock

This is how it works with Alice and Bob:

  1. Scenario: Alice wants to deposit tokens into the contract so that Bob can claim them later.
  2. Initiating the Deposit: When Alice calls the deposit function, the contract checks that the action is being initiated by her address.
  3. Authorization Check: The contract uses an authorization check to confirm that Alice is the one making the deposit. If Bob tried to call the deposit function instead, the contract would reject the action because Bob doesn’t have permission.
  4. Deposit Execution: Since Alice is authorized, the contract transfers the specified tokens from Alice’s account to the contract’s address.
  5. Claiming the Tokens: Later, Bob can claim the tokens only if the conditions set by Alice (like the time constraints) are met. The contract ensures that only Bob can claim these tokens because he is listed as a designated claimant.
  6. Security: This mechanism ensures that only Alice can control the deposited tokens, preventing anyone else, including Bob, from manipulating the deposit or claiming the funds without authorization.

In summary, the contract protects the funds by ensuring that only the designated account (Alice) can perform specific actions (depositing), while allowing another account (Bob) to claim the funds later under the right conditions.

enums, structs, functions and 1 x impl

#![no_std]

#![no_std]

The #![no_std] directive in Rust indicates that the code is intended to run in a “no standard library” environment. Here’s what that means:

  1. Minimal Environment: By using no_std, the code does not rely on Rust’s standard library, which is designed for general-purpose programming and includes features like dynamic memory allocation, file I/O, and more. This makes the code lighter and more suitable for environments with limited resources.
  2. Use Cases: This is commonly used in embedded systems, operating systems, or blockchain contracts (like the one you’re working with) where you need tight control over resources and where the standard library’s features might not be available.
  3. Alternatives: Instead of the standard library, the code can use core functionalities provided by the core library, which is a subset of the standard library that includes basic types, traits, and functions.
  4. Implications:
  • You may not have access to certain features that are part of the standard library, such as collections like Vec or String. However, you can use other libraries specifically designed for no_std environments, such as alloc.
  • Error handling and other functionalities might require more manual implementations or alternative libraries.

In summary, #![no_std] is used to build lightweight and resource-efficient applications in constrained environments, ensuring that the code remains functional without the overhead of the standard library.

use soroban_sdk::{contract, contractimpl, contracttype, token, Address, Env, Vec};

This line is importing several items from the soroban_sdk crate, which is essential for developing smart contracts on the Soroban blockchain. Here’s a breakdown of each component:

Breakdown of the imports

  1. use soroban_sdk::{...}: This syntax imports multiple modules and types from the soroban_sdk crate into the current scope, making them available for use without needing to prefix them with soroban_sdk::.
  2. contract: A macro that defines a smart contract. It sets up the contract structure and allows you to implement various functions associated with it.
  3. contractimpl: A macro used to implement the contract’s logic. It allows you to define the functions that can be called on the contract, effectively linking them to the contract defined by the contract macro.
  4. contracttype: A macro that defines custom data types used in the contract. This is useful for defining structured data that the contract will manage.
  5. token: A module that provides functions and types related to token operations, such as transferring tokens or interacting with token contracts.
  6. Address: A type representing the address of an account or contract on the blockchain. It’s used to specify where tokens should be sent or received.
  7. Env: A type representing the execution environment of the contract. It provides access to the blockchain state, functions, and storage, allowing the contract to interact with the network.
  8. Vec: A type representing a growable array, similar to vectors in other programming languages. It’s used to store a dynamic number of elements, such as lists of addresses. (Linear memory, not “the heap” !).

In essence, this line sets up the necessary tools to create a smart contract on the Soroban platform, allowing developers to define contract logic, manage tokens, and interact with the blockchain environment effectively.

More about the ‘token’

token Module Overview

The token module provides essential functionality for handling tokens on the Soroban blockchain. This is particularly important since many decentralized applications (dApps) and smart contracts rely on the movement and management of digital assets (tokens).

Key Features of the token Module

  • Token Transfers:
  • Transfer Functionality: The module allows contracts to send tokens from one address to another. This is crucial for facilitating transactions between users and other contracts.
  • Authorization: It typically checks that the sender has enough balance and that the transfer is authorized by the token owner.
  • Interacting with Token Contracts:
  • Token Management: The module provides functions to interact with existing token contracts. This includes querying balances, approving token spending, and more.
  • Standardized Operations: It follows a standardized approach to token handling, making it easier for developers to work with various tokens consistently.
  • Compatibility:
  • The token module is designed to work seamlessly with other parts of the Soroban SDK, allowing developers to build complex interactions between contracts and tokens.
  • Event Emission:
  • When token transfers occur, the module can emit events that can be tracked on the blockchain. This helps in monitoring transactions and auditing.
  • Types and Structures:
  • The module includes various types and structures that represent token-related data, such as the token’s address, balance, and metadata. This makes it easier to manage and manipulate tokens programmatically.

Example Use Cases

  • Payment Contracts: A contract that allows users to pay for services using tokens can utilize the token module to handle transactions securely.
  • Rewards Distribution: Contracts that distribute tokens as rewards can leverage the module to manage how and when tokens are sent to users.
  • Voting Systems: In a decentralized voting system, the token module can facilitate voting rights by transferring tokens that represent votes.
token interface - transfer docs

Overall, the token module is a vital component of the soroban_sdk, enabling developers to implement robust token functionality within their smart contracts. It abstracts the complexities of token management, allowing developers to focus on building innovative dApps and ensuring secure and efficient transactions.

[contracttype]

#[derive(Clone)]
#[contracttype]
pub enum DataKey {
    Init,
    Balance,
}

The use of #[contracttype] in this context is part of the Soroban smart contract framework, and it plays a crucial role in defining custom data types that can be stored and manipulated within a smart contract.

Breakdown of the Code

  • #[derive(Clone)]:
  • This attribute allows the DataKey enum to implement the Clone trait, which means you can create duplicate instances of DataKey. Cloning is useful in scenarios where you might need to pass around instances without transferring ownership.
  • #[contracttype]:
  • This macro indicates that the DataKey enum (see below) is a type that can be stored in the Soroban contract’s persistent storage. By marking it with #[contracttype], you make it recognizable to the Soroban runtime as a structured data type that can be serialized and deserialized when interacting with the blockchain.
  • pub enum DataKey:
  • Here, you’re defining a public enumeration called DataKey. Enums are a way to define a type that can be one of several predefined values. In this case, DataKey can take on one of two values: Init and Balance.
  • Init and Balance:
  • Init: This variant likely represents the initialization state of the contract. It could be used to check if the contract has been set up correctly before allowing further actions.
  • Balance: This variant is intended to hold the balance information within the contract. It can be used to reference the stored claimable balance of tokens, allowing the contract to manage the token amounts effectively.

Use in the Contract Logic

  • Storage Management: The DataKey enum allows for a clear, type-safe way to interact with the contract’s storage. When storing data, you can use these enum values as keys, ensuring that each piece of data is associated with a specific purpose (e.g., whether the contract is initialized or what the current balance is).
  • Readability and Maintainability: By using an enum to define storage keys, the code becomes more readable. It’s clear what each key represents, making it easier to maintain the contract and understand its logic at a glance.
  • Safety: The #[contracttype] macro ensures that the enum can be properly stored and retrieved, minimizing potential issues with data integrity when interacting with the blockchain. The Soroban runtime handles the serialization and deserialization of these types, allowing developers to focus on the business logic.

In summary, using #[contracttype] in conjunction with enums like DataKey enhances the functionality of Soroban smart contracts by allowing for structured, type-safe, and easily manageable data storage. This approach contributes to more robust contract development, as it defines clear and meaningful ways to interact with the contract’s state.

Storage

Contract Storage in Soroban

Soroban offers three distinct types of storage, each with unique features:

  • Temporary Storage:
  • This type incurs the lowest fees and is automatically deleted once its Time-To-Live (TTL) expires. It’s best suited for short-lived or easily regenerable data, such as price oracles.
  • Persistent Storage:
  • While this option has higher fees, it allows for data to be archived and later restored through specific operations. It’s ideal for user-related information that needs to be retained over time.
  • Instance Storage:
  • A subtype of persistent storage, instance storage is linked to the lifespan of the contract itself, making it appropriate for data relevant to the contract, like metadata or administrative accounts. Both persistent and instance storage can be recovered after being archived, although they come with higher costs.

This structure allows developers to choose the most suitable storage type based on their data retention needs and cost considerations.

Instance storage in Soroban tends to be more expensive than persistent storage due to its unique characteristics. Here’s a quick comparison:

  • Cost: Instance storage usually incurs higher fees than persistent storage because it is tied to the contract’s lifespan, ensuring that the data remains accessible as long as the contract exists.
  • Duration: Instance storage lasts for the duration of the contract, while persistent storage can be archived and restored as needed, but may not have the same immediate accessibility as instance storage.

https://developers.stellar.org/docs/build/guides/storage

env.storage().temporary().set(&bid_key, bid_amount);
env.storage().persistent().set(&key, &new_points);
env.storage().instance().set(&key, &new_votes);

TimeBound

This code illustrates a “timelock” concept using a simplified version of the Claimable Balance pattern, similar to Stellar’s native implementation. The TimeBoundKind and TimeBound structures are crucial in implementing time-based conditions for claiming balances in the contract. Here’s an analysis of these structures in the context of the contract:

TimeBoundKind enum & TimeBound struct Explained:

  1. TimeBoundKind Enum:
   #[derive(Clone)]
   #[contracttype]
   pub enum TimeBoundKind {
       Before,
       After,
   }
  • TimeBoundKind defines two types of time conditions: Before and After.
  • Before: Specifies that the balance can only be claimed before a certain timestamp.
  • After: Specifies that the balance can only be claimed after a certain timestamp.
  • This enum is used to determine the relationship between the current ledger timestamp and the specified time condition.
  1. TimeBound Struct:
   #[derive(Clone)]
   #[contracttype]
   pub struct TimeBound {
       pub kind: TimeBoundKind,
       pub timestamp: u64,
   }
  • The TimeBound struct combines the TimeBoundKind with a specific timestamp (in seconds since the Unix epoch).
  • kind: Indicates the type of time condition (Before or After).
  • timestamp: The exact time that serves as the boundary for the timelock condition.
  • Together, these fields allow defining when a claim on the balance is permitted based on the current time.

Relation to the Contract:

The TimeBoundKind and TimeBound are central to the contract’s logic for managing claimable balances with a time restriction. Here’s how they function within the contract:

  1. Timelock Mechanism:
  • The contract allows depositors to specify a TimeBound when creating a ClaimableBalance. This ensures that claimants can only claim the balance either before or after the specified timestamp, depending on the kind.
  1. Deposit Function:
  • When a user deposits tokens into the contract, they must specify a TimeBound. This time constraint is stored alongside the balance information:
    rust env.storage().instance().set( &DataKey::Balance, &ClaimableBalance { token, amount, time_bound, claimants, }, );
  1. Claim Function:
  • The claim function checks the current ledger timestamp against the TimeBound before allowing any claimant to withdraw the tokens:
    rust if !check_time_bound(&env, &claimable_balance.time_bound) { panic!("time predicate is not fulfilled"); }
  • The check_time_bound function performs this validation: fn check_time_bound(env: &Env, time_bound: &TimeBound) -> bool { let ledger_timestamp = env.ledger().timestamp(); match time_bound.kind { TimeBoundKind::Before => ledger_timestamp <= time_bound.timestamp, TimeBoundKind::After => ledger_timestamp >= time_bound.timestamp, } }
  • This function checks if the current ledger time satisfies the condition specified by TimeBound. If not, the claim is rejected.

Significance:

  • Security: By enforcing a time constraint on when balances can be claimed, the contract ensures controlled and predictable access to funds.
  • Flexibility: The TimeBoundKind allows for versatile time-based conditions, giving users the ability to set specific claim windows.
  • Use Case Alignment: The implementation aligns with scenarios where delayed transfers or specific claim periods are required, such as vesting schedules, time-locked rewards, or conditional payments.

These components make the contract robust by ensuring that claims occur only under predefined time conditions, thus implementing a secure and programmable way to handle claimable balances.

ClaimableBalance

#[derive(Clone)]
#[contracttype]
pub struct ClaimableBalance {
    pub token: Address,
    pub amount: i128,
    pub claimants: Vec<Address>,
    pub time_bound: TimeBound,
}

“Claimable Balance” is the mechanism that incorporates the timelock feature. Let’s break it down:

Overview

The contract allows one user (the depositor) to deposit a specified amount of tokens, which can then be claimed by designated claimants either before or after a specified timestamp. It demonstrates the concept of timelocks, similar to claimable balances in the Stellar ecosystem.

Key Components

  1. Structs and Enums:
    • ClaimableBalance: This struct stores information about the claimable balance, including:
      • token: The address of the token being deposited.
      • amount: The amount of tokens deposited.
      • claimants: A list of addresses allowed to claim the tokens.
      • time_bound: A structure defining the time constraints for claiming the tokens.
    • DataKey: An enum that helps manage storage keys for contract state.
      • Init: Indicates if the contract has been initialized.
      • Balance: Stores the details of the claimable balance.
    • TimeBound: This struct defines the time conditions for claiming:
      • kind: Specifies whether the claim can occur before or after a certain timestamp.
      • timestamp: The specific time reference.
  2. check_time_bound Function:
    • This function checks if the current ledger timestamp meets the conditions specified in the time_bound. It returns true if the claim can be made based on whether it’s before or after the specified time.
  3. Contract Implementation:
    • deposit Function:
      • Allows the depositor to transfer tokens to the contract, creating a claimable balance.
      • Checks if the number of claimants exceeds a limit (10 in this case).
      • Ensures the contract is not already initialized to prevent duplicate deposits.
      • Requires the depositor to authorize the transaction.
      • Stores the claimable balance in the contract’s storage and marks it as initialized.
    • claim Function:
      • Allows a claimant to claim the deposited tokens.
      • Requires the claimant to authorize the claim.
      • Retrieves the claimable balance and checks the time conditions.
      • Verifies that the claimant is in the allowed list.
      • If all checks pass, transfers the tokens to the claimant and removes the balance from storage to prevent further claims.
  4. is_initialized Function:
    • A helper function that checks if the contract has been initialized by looking for the Init key in storage.

Claimants

pub claimants: Vec<Address> represents a public vector of addresses authorized to claim the deposited tokens. This allows multiple users to have rights to claim the balance. Each claimant must be included in this vector when the balance is created. During the claim process, the contract checks if the calling address is in this vector, ensuring only authorized users can access the funds. The vector can hold a maximum of 10 claimants, providing flexibility while maintaining a limit for simplicity and security.

Vec<Address> refers to a Soroban vector, which is a data structure optimized for the Soroban environment. Unlike a Rust heap vector, Soroban vectors are stored on the blockchain and are designed for efficient access and manipulation in smart contracts.

ClaimableBalanceContract

The line #[contract] pub struct ClaimableBalanceContract; serves a specific purpose in the Soroban smart contract framework:

  1. Contract Definition: This line defines a new smart contract called ClaimableBalanceContract. It acts as the main entry point for the contract’s functionality.
  2. Attribute Macro: The #[contract] attribute macro indicates that this struct should be treated as a contract by the Soroban SDK. It enables the framework to recognize and manage it as a deployable contract, allowing the associated implementation functions (like deposit and claim) to be called as contract methods.
  3. Separation of Logic: By defining a separate struct for the contract, it helps to organize the code and clearly delineate the contract’s state and behavior from other components like data types or utility functions.

TheClaimableBalanceContract struct is essential for the framework to identify and manage the contract, ensuring that it can be deployed and interacted with correctly on the Soroban blockchain.

fn check_time_bound

This part of the code defines a function called check_time_bound, which is responsible for validating whether the current ledger timestamp meets the conditions specified in a TimeBound structure. Here’s a breakdown of how it works:

  1. Function Signature:
   fn check_time_bound(env: &Env, time_bound: &TimeBound) -> bool
  • The function takes two parameters:
    • env: A reference to the environment (Env) that provides access to blockchain-related features.
    • time_bound: A reference to a TimeBound struct, which includes the kind of time condition (before or after) and the specific timestamp.
  1. Retrieve Current Timestamp:
   let ledger_timestamp = env.ledger().timestamp();
  • This line retrieves the current ledger timestamp from the blockchain environment. This timestamp is used to compare against the provided time_bound.
  1. Match Statement:
   match time_bound.kind {
       TimeBoundKind::Before => ledger_timestamp <= time_bound.timestamp,
       TimeBoundKind::After => ledger_timestamp >= time_bound.timestamp,
   }
  • The match statement checks the kind of the time bound:
    • If it’s TimeBoundKind::Before, the function checks if the current ledger timestamp is less than or equal to the specified timestamp. If true, the condition is fulfilled.
    • If it’s TimeBoundKind::After, it checks if the current ledger timestamp is greater than or equal to the specified timestamp. Again, if true, the condition is fulfilled.
  • The function returns a boolean value: true if the current timestamp satisfies the time condition, and false otherwise.

The purpose of this function is to enforce the timelock feature in the contract. By ensuring that claims can only be made within specified time constraints, it adds an important layer of security and control over when funds can be accessed.


Transfers token from from to the contract address.

Stores all the necessary info to allow one of the claimants to claim it.

Make sure claimant has authorized this call, which ensures their identity.

Transfer the stored amount of token to claimant after passing all the checks


Could you spoof the “from” address to send the funds into a black hole?

https://chatgpt.com/share/66f15421-5ec0-800b-b583-77948b4297f6

why is token of type Address?

https://chatgpt.com/c/66f1635e-c8e4-800b-8931-915bd78c6316

The address of XLM (Lumens) on the Stellar network is a special, predefined address that represents the native asset. It is often represented as the following public key:

CAS3J7GYLGXMF6TDJBBYYSE3HQ6BBSMLNUQ34T6TZMYMW2EVH34XOWMA

This address is used in transactions involving XLM, and when you specify it in your code or transactions, it indicates that you are working with the native currency of the Stellar network.

In general, for other issued tokens, you would obtain their specific addresses from the issuer or through Stellar network explorers.

https://api.v1.xlm.services/#get-/tokens/-tokenAddress-

Reference to &()

https://chatgpt.com/c/66f16bb9-6ec8-800b-8da6-87f2126bdc0a

In Rust, the unit type () is used to represent the absence of a value. In the context of your code, env.storage().instance().set(&DataKey::Init, &());, the &() is a reference to the unit value, which signifies that you’re storing a key in the storage without associating any meaningful data with it.

Here, DataKey::Init is likely a marker or flag indicating some initialization state, and by using &(), you’re effectively saying “this key exists, but there’s no additional data associated with it.” This pattern is often used in scenarios where the presence of the key itself is what matters, rather than any specific value.

In this specific case, env.storage().instance().set() expects a reference to align with its design principles, even if it might seem redundant for the unit type.

Full code

Invoke Smart Contract requiring struct argument


test invoke
~/rust/timelocked main*
❯ stellar contract invoke --id CDOQTMIY5QMXBOSFA4GFQD6L7QDMMYCJLU3GEXKUETPZVTH74FNH3QIG --source-account alice --network testnet \-- deposit --from GAUSBA6IHKK63ZPCLNQJQ332OFIETXOFSTK7AI7YTKR3M7EPTW7CLR5H --token CAS3J7GYLGXMF6TDJBBYYSE3HQ6BBSMLNUQ34T6TZMYMW2EVH34XOWMA --claimants '["GBH5BLUPQAF6YR2P5B6JOBWEB5JHL6HDOV44XGEETJEF2GE537YDFWLU"]' --amount "1" --time_bound '{"kind": "After", "timestamp": 1727110358}'

from key (alice) = GAUSBA6IHKK63ZPCLNQJQ332OFIETXOFSTK7AI7YTKR3M7EPTW7CLR5H

Python Code

Previous article

Stellar Cheatsheet
Bitcoin Programming

Next article

HTLC – Atomic Swaps