Skip to content
Open
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
155 changes: 154 additions & 1 deletion contracts/invoice-token/src/storage.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,52 @@
//! Storage helpers for balances, metadata, allowances, fees, roles, nonces, and history.
//! Storage module for the invoice-token contract.
//!
//! This module manages the lifecycle and access patterns for token state, including balances,
//! allowances, metadata, fees, nonces, and access control roles.
//!
//! # Storage Architecture
//! The storage architecture leverages two types of Soroban storage:
//! - **Instance Storage:** Used for global contract state (e.g., `Metadata`, `TotalSupply`, `FeeBps`, `RoleAdmin`, `RoleGrant`).
//! Instance storage shares the TTL of the contract instance. Whenever the contract is invoked,
//! instance storage is implicitly bumped based on the instance's bump policy.
//! - **Persistent Storage:** Used for user-specific state that outlives the current instance TTL and must be explicitly
//! bumped (e.g., `Balance`, `Allowance`, `Nonce`, `History`, `Frozen`). This ensures that user funds and approvals
//! are not lost even if the contract instance itself is archived.
//!
//! # TTL Bump Policies
//! When modifying persistent storage, developers should ensure TTLs are sufficiently bumped
//! (typically done in the top-level token interface functions invoking this module, or automatically by the host).

use soroban_sdk::{Address, Symbol, Vec};

use crate::types::{AllowanceData, OwnershipHistoryRecord, StorageKey, TokenMetadata};

/// Load token metadata from instance storage.
///
/// # Parameters
/// - `env`: The environment context.
///
/// # Returns
/// Returns `Some(TokenMetadata)` if initialized, otherwise `None`.
pub fn get_metadata(env: &soroban_sdk::Env) -> Option<TokenMetadata> {
env.storage().instance().get(&StorageKey::Metadata)
}

/// Save token metadata to instance storage.
///
/// # Parameters
/// - `env`: The environment context.
/// - `meta`: The metadata structure to store.
pub fn set_metadata(env: &soroban_sdk::Env, meta: &TokenMetadata) {
env.storage().instance().set(&StorageKey::Metadata, meta);
}

/// Load total supply from instance storage.
///
/// # Parameters
/// - `env`: The environment context.
///
/// # Returns
/// Returns the total token supply as `i128`. Defaults to `0` if not set.
pub fn get_total_supply(env: &soroban_sdk::Env) -> i128 {
env.storage()
.instance()
Expand All @@ -23,13 +55,24 @@ pub fn get_total_supply(env: &soroban_sdk::Env) -> i128 {
}

/// Save total supply to instance storage.
///
/// # Parameters
/// - `env`: The environment context.
/// - `amount`: The new total supply amount.
pub fn set_total_supply(env: &soroban_sdk::Env, amount: i128) {
env.storage()
.instance()
.set(&StorageKey::TotalSupply, &amount);
}

/// Get balance for an address (persistent storage).
///
/// # Parameters
/// - `env`: The environment context.
/// - `addr`: The address to query the balance for.
///
/// # Returns
/// Returns the balance as `i128`. Defaults to `0` if the account does not exist or has no balance.
pub fn get_balance(env: &soroban_sdk::Env, addr: &Address) -> i128 {
env.storage()
.persistent()
Expand All @@ -38,6 +81,12 @@ pub fn get_balance(env: &soroban_sdk::Env, addr: &Address) -> i128 {
}

/// Set balance for an address (persistent storage).
/// If the amount is 0, the entry is removed from storage to save space.
///
/// # Parameters
/// - `env`: The environment context.
/// - `addr`: The address whose balance will be set.
/// - `amount`: The new balance.
pub fn set_balance(env: &soroban_sdk::Env, addr: &Address, amount: i128) {
if amount == 0 {
env.storage()
Expand All @@ -51,6 +100,13 @@ pub fn set_balance(env: &soroban_sdk::Env, addr: &Address, amount: i128) {
}

/// Check whether an account is frozen.
///
/// # Parameters
/// - `env`: The environment context.
/// - `account`: The address to check.
///
/// # Returns
/// Returns `true` if the account is frozen, otherwise `false`.
pub fn is_account_frozen(env: &soroban_sdk::Env, account: &Address) -> bool {
env.storage()
.persistent()
Expand All @@ -59,6 +115,11 @@ pub fn is_account_frozen(env: &soroban_sdk::Env, account: &Address) -> bool {
}

/// Update an account's frozen state, removing unrestricted entries from storage.
///
/// # Parameters
/// - `env`: The environment context.
/// - `account`: The address to update.
/// - `frozen`: The new frozen state.
pub fn set_account_frozen(env: &soroban_sdk::Env, account: &Address, frozen: bool) {
let key = StorageKey::Frozen(account.clone());
if frozen {
Expand All @@ -69,6 +130,15 @@ pub fn set_account_frozen(env: &soroban_sdk::Env, account: &Address, frozen: boo
}

/// Get allowance (from, spender). Returns 0 if expired or not set.
///
/// # Parameters
/// - `env`: The environment context.
/// - `from`: The owner of the funds.
/// - `spender`: The authorized spender.
/// - `current_ledger`: The current ledger sequence number.
///
/// # Returns
/// Returns the available allowance as `i128`. Returns `0` if expired or not found.
pub fn get_allowance(
env: &soroban_sdk::Env,
from: &Address,
Expand All @@ -85,6 +155,13 @@ pub fn get_allowance(

/// Set allowance (from, spender) -> (amount, expiration_ledger).
/// Removes the key when amount is 0 to save persistent storage.
///
/// # Parameters
/// - `env`: The environment context.
/// - `from`: The owner of the funds.
/// - `spender`: The authorized spender.
/// - `amount`: The amount to authorize.
/// - `expiration_ledger`: The ledger sequence when this allowance expires.
pub fn set_allowance(
env: &soroban_sdk::Env,
from: &Address,
Expand All @@ -107,6 +184,14 @@ pub fn set_allowance(
}

/// Get raw allowance data (for decreasing allowance on transfer_from/burn_from).
///
/// # Parameters
/// - `env`: The environment context.
/// - `from`: The owner of the funds.
/// - `spender`: The authorized spender.
///
/// # Returns
/// Returns `Some(AllowanceData)` if found, otherwise `None`.
pub fn get_allowance_data(
env: &soroban_sdk::Env,
from: &Address,
Expand All @@ -120,6 +205,12 @@ pub fn get_allowance_data(
// ==================== Fee (Issue #113) ====================

/// Get fee basis points from instance storage. Returns 0 if not set.
///
/// # Parameters
/// - `env`: The environment context.
///
/// # Returns
/// Returns the fee basis points as `i128`.
pub fn get_fee_bps(env: &soroban_sdk::Env) -> i128 {
env.storage()
.instance()
Expand All @@ -128,27 +219,51 @@ pub fn get_fee_bps(env: &soroban_sdk::Env) -> i128 {
}

/// Save fee basis points to instance storage.
///
/// # Parameters
/// - `env`: The environment context.
/// - `bps`: The fee basis points to set.
pub fn set_fee_bps(env: &soroban_sdk::Env, bps: i128) {
env.storage().instance().set(&StorageKey::FeeBps, &bps);
}

// ==================== Role Admin (Issue #108) ====================

/// Get the admin address for a specific role. Returns None if unset.
///
/// # Parameters
/// - `env`: The environment context.
/// - `role`: The role symbol.
///
/// # Returns
/// Returns the admin address `Some(Address)` if set, otherwise `None`.
pub fn get_role_admin(env: &soroban_sdk::Env, role: &Symbol) -> Option<Address> {
env.storage()
.instance()
.get(&StorageKey::RoleAdmin(role.clone()))
}

/// Set the admin address for a specific role.
///
/// # Parameters
/// - `env`: The environment context.
/// - `role`: The role symbol.
/// - `admin`: The admin address.
pub fn set_role_admin(env: &soroban_sdk::Env, role: &Symbol, admin: &Address) {
env.storage()
.instance()
.set(&StorageKey::RoleAdmin(role.clone()), admin);
}

/// Check whether `account` has been granted `role`.
///
/// # Parameters
/// - `env`: The environment context.
/// - `role`: The role symbol to check.
/// - `account`: The account address to verify.
///
/// # Returns
/// Returns `true` if the account has the role, otherwise `false`.
pub fn has_role(env: &soroban_sdk::Env, role: &Symbol, account: &Address) -> bool {
env.storage()
.instance()
Expand All @@ -157,6 +272,12 @@ pub fn has_role(env: &soroban_sdk::Env, role: &Symbol, account: &Address) -> boo
}

/// Grant or revoke `role` for `account`.
///
/// # Parameters
/// - `env`: The environment context.
/// - `role`: The role symbol.
/// - `account`: The account address.
/// - `granted`: Boolean indicating whether to grant (`true`) or revoke (`false`).
pub fn set_role_grant(env: &soroban_sdk::Env, role: &Symbol, account: &Address, granted: bool) {
let key = StorageKey::RoleGrant(role.clone(), account.clone());
if granted {
Expand All @@ -169,6 +290,13 @@ pub fn set_role_grant(env: &soroban_sdk::Env, role: &Symbol, account: &Address,
// ==================== Nonce (Issue #106) ====================

/// Get the current nonce for an address. Starts at 0.
///
/// # Parameters
/// - `env`: The environment context.
/// - `addr`: The account address.
///
/// # Returns
/// Returns the current nonce as `u64`.
pub fn get_nonce(env: &soroban_sdk::Env, addr: &Address) -> u64 {
env.storage()
.persistent()
Expand All @@ -177,6 +305,13 @@ pub fn get_nonce(env: &soroban_sdk::Env, addr: &Address) -> u64 {
}

/// Increment and return the new nonce for an address.
///
/// # Parameters
/// - `env`: The environment context.
/// - `addr`: The account address.
///
/// # Returns
/// Returns the newly incremented nonce as `u64`.
#[allow(dead_code)]
pub fn increment_nonce(env: &soroban_sdk::Env, addr: &Address) -> u64 {
let current = get_nonce(env, addr);
Expand All @@ -190,6 +325,13 @@ pub fn increment_nonce(env: &soroban_sdk::Env, addr: &Address) -> u64 {
// ==================== Ownership History (Issue #111) ====================

/// Get the full ownership history for an address. Returns empty Vec if none.
///
/// # Parameters
/// - `env`: The environment context.
/// - `addr`: The account address.
///
/// # Returns
/// Returns a `Vec<OwnershipHistoryRecord>`.
pub fn get_token_history(env: &soroban_sdk::Env, addr: &Address) -> Vec<OwnershipHistoryRecord> {
env.storage()
.persistent()
Expand All @@ -198,6 +340,11 @@ pub fn get_token_history(env: &soroban_sdk::Env, addr: &Address) -> Vec<Ownershi
}

/// Append an ownership history record for an address.
///
/// # Parameters
/// - `env`: The environment context.
/// - `addr`: The account address.
/// - `record`: The history record to append.
pub fn append_token_history(
env: &soroban_sdk::Env,
addr: &Address,
Expand All @@ -216,6 +363,12 @@ pub fn append_token_history(
// ==================== Allowance Expiration Extension ====================

/// Extend the expiration ledger of an existing allowance in place, leaving its amount untouched.
///
/// # Parameters
/// - `env`: The environment context.
/// - `from`: The owner of the funds.
/// - `spender`: The authorized spender.
/// - `new_expiration_ledger`: The new expiration ledger sequence.
pub fn extend_allowance_expiration(
env: &soroban_sdk::Env,
from: &Address,
Expand Down
59 changes: 49 additions & 10 deletions contracts/invoice-token/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,25 +24,64 @@ pub fn is_zero_address(env: &Env, address: &Address) -> bool {
#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum StorageKey {
/// Instance: token metadata and config.
/// Purpose: Stores global token configuration and metadata (admin, symbol, decimals, etc.).
/// Storage Type: Instance storage.
/// Access Pattern: Read frequently on metadata queries; written during initialization or by admin.
/// TTL Policy: Bumped automatically with the contract instance.
Metadata,
/// Instance: total supply.

/// Purpose: Tracks the total circulating supply of the token.
/// Storage Type: Instance storage.
/// Access Pattern: Read on `total_supply` queries; written during minting or burning operations.
/// TTL Policy: Bumped automatically with the contract instance.
TotalSupply,
/// Persistent: balance by holder address.

/// Purpose: Tracks the token balance for a specific holder address.
/// Storage Type: Persistent storage.
/// Access Pattern: Read on balance queries and transfers; written during transfers, minting, or burning.
/// TTL Policy: Must be explicitly bumped to prevent loss of user funds.
Balance(soroban_sdk::Address),
/// Persistent: allowance (from, spender) -> AllowanceData.

/// Purpose: Tracks the delegated spending allowance (`AllowanceData`) from an owner to a spender.
/// Storage Type: Persistent storage.
/// Access Pattern: Read on `allowance` queries and `transfer_from`; written during `approve`.
/// TTL Policy: Explicitly bumped; expires based on the `expiration_ledger` in `AllowanceData`.
Allowance(soroban_sdk::Address, soroban_sdk::Address),
/// Instance: fee basis points.

/// Purpose: Stores the fee basis points applied to transfers (if applicable).
/// Storage Type: Instance storage.
/// Access Pattern: Read during fee-enabled transfers; written by admin.
/// TTL Policy: Bumped automatically with the contract instance.
FeeBps,
/// Instance: role admin mapping (role -> admin address).

/// Purpose: Maps a specific role (e.g., minter) to its administrator address.
/// Storage Type: Instance storage.
/// Access Pattern: Read when checking administrative rights; written by super admin.
/// TTL Policy: Bumped automatically with the contract instance.
RoleAdmin(soroban_sdk::Symbol),
/// Instance: role grant mapping (role, account) -> bool.

/// Purpose: Tracks whether a specific account has been granted a particular role.
/// Storage Type: Instance storage.
/// Access Pattern: Read during role-restricted operations; written when granting/revoking roles.
/// TTL Policy: Bumped automatically with the contract instance.
RoleGrant(soroban_sdk::Symbol, soroban_sdk::Address),
/// Persistent: nonce per address for permit-style transfers.

/// Purpose: Tracks the current nonce for permit-style operations or replay protection.
/// Storage Type: Persistent storage.
/// Access Pattern: Read during signature verification; incremented on successful operation.
/// TTL Policy: Explicitly bumped to maintain sequence history.
Nonce(soroban_sdk::Address),
/// Persistent: ownership history records for a token holder.

/// Purpose: Stores a list of ownership history records (`OwnershipHistoryRecord`) for an address.
/// Storage Type: Persistent storage.
/// Access Pattern: Read on history queries; appended to during transfers.
/// TTL Policy: Explicitly bumped to preserve historical data.
History(soroban_sdk::Address),
/// Persistent: whether an account is restricted from token operations.

/// Purpose: Tracks whether an account is restricted/frozen from token operations.
/// Storage Type: Persistent storage.
/// Access Pattern: Read before transfers; written by admin to freeze/unfreeze accounts.
/// TTL Policy: Explicitly bumped to enforce compliance rules.
Frozen(soroban_sdk::Address),
}

Expand Down