Skip to content

Implement Base Reserve Storage Layer #35#36

Merged
phertyameen merged 4 commits intobridgelet-org:mainfrom
Joaco2603:main
Feb 25, 2026
Merged

Implement Base Reserve Storage Layer #35#36
phertyameen merged 4 commits intobridgelet-org:mainfrom
Joaco2603:main

Conversation

@Joaco2603
Copy link
Copy Markdown
Contributor

@Joaco2603 Joaco2603 commented Feb 23, 2026

Implement Base Reserve Storage Layer #35

Closes #35

Purpose

reserve_contract is a focused Soroban smart contract that stores and exposes the base reserve configuration for the Bridgelet system.

In the Stellar network, every account must keep a minimum XLM balance (the base reserve) to remain open. Bridgelet's ephemeral accounts query this value to know how much XLM is network overhead (that must be returned to the creator on close) versus how much belongs to the actual user payment.

This contract answers one question: "what is the configured base reserve, in stroops?"

1 XLM = 10,000,000 stroops. All values are stored as integers to avoid floating-point arithmetic inside the contract.


File Structure

contracts/reserve_contract/
├── Cargo.toml
└── src/
    ├── lib.rs       ← public contract interface & entry points
    ├── storage.rs   ← all read/write helpers and TTL management
    ├── errors.rs    ← error enum with every possible failure code
    ├── events.rs    ← event structs and emit helpers
    └── test.rs      ← unit + property tests (20 cases)

Public API

All entry points are defined in src/lib.rs on ReserveContract.

initialize(admin: Address) → Result<(), Error>

One-time setup. Stores the admin address that will be allowed to set the reserve. Calling it a second time returns Error::AlreadyInitialized (code #4).

initialize(admin)
  ├── extend_instance_ttl()
  ├── has_admin? → AlreadyInitialized (#4)
  ├── admin.require_auth()
  ├── set_admin(admin)
  └── emit "init" event

set_base_reserve(amount: i128) → Result<(), Error>

Stores a new base reserve value (in stroops). Only the admin may call this. Overwrites any previous value and emits a BaseReserveUpdated event for off-chain auditability.

Safety ceiling: MAX_RESERVE_STROOPS = 100_000_000_000 (10,000 XLM). This prevents accidental misconfiguration (e.g. passing a value in XLM instead of stroops).

set_base_reserve(amount)
  ├── extend_instance_ttl()
  ├── get_admin()    → None  → NotInitialized (#5)
  ├── admin.require_auth()  → Unauthorized (#3)
  ├── amount ≤ 0            → InvalidAmount (#1)
  ├── amount > 100_000_000_000 → AmountTooLarge (#6)
  ├── set_base_reserve(amount)
  └── emit "reserve" event  { old_value, new_value, admin }

get_base_reserve() → Option<i128>

Returns Some(amount) if configured, None otherwise. Safe default — callers must handle the unset case explicitly.

require_base_reserve() → Result<i128, Error>

Returns the reserve or Error::ReserveNotSet (#2) if not configured. Use this when the consumer requires the value to exist before proceeding (e.g. during a sweep flow).

has_base_reserve() → bool

Cheaper presence check — returns true/false without reading the actual value.

get_admin() → Option<Address>

Returns the admin address if the contract has been initialized.


Storage

All data lives in instance storage, meaning it is tied to the contract instance's lifecycle and shares one TTL with the contract itself.

Key (DataKey) Type Description
BaseReserve i128 Reserve amount in stroops
Admin Address Admin set at initialization

TTL is proactively extended on every public entry-point call (reads included) to prevent the contract from being archived during inactivity.

// src/storage.rs
const INSTANCE_TTL_THRESHOLD: u32 = 100;     // ~8 minutes
const INSTANCE_TTL_EXTEND_TO: u32 = 518_400; // ~30 days

Errors

Defined in src/errors.rs as a #[contracterror] enum:

Code Variant When it is returned
#1 InvalidAmount Amount is zero or negative
#2 ReserveNotSet require_base_reserve called before any value is stored
#3 Unauthorized Caller is not the admin
#4 AlreadyInitialized initialize called more than once
#5 NotInitialized set_base_reserve called before initialize
#6 AmountTooLarge Amount exceeds 100,000,000,000 stroops (10,000 XLM)

Events

Defined in src/events.rs:

Symbol Struct Fields
"init" ContractInitialized admin: Address
"reserve" BaseReserveUpdated old_value: i128, new_value: i128, admin: Address

old_value is 0 when no prior reserve existed, making event history self-contained for off-chain indexers.


Access Control Flow

         ┌────────────────────────────────┐
         │          Anyone               │
         │  get_base_reserve()           │
         │  has_base_reserve()           │
         │  require_base_reserve()       │
         │  get_admin()                  │
         └────────────────────────────────┘
                        │
         ┌──────────────▼──────────────┐
         │     Admin only              │
         │  set_base_reserve(amount)   │
         └──────────────┬──────────────┘
                        │
         ┌──────────────▼──────────────┐
         │   Uncalled (one-time only)  │
         │  initialize(admin)          │
         └─────────────────────────────┘

Test Coverage

20 tests in src/test.rs, grouped by concern:

Group Tests
Initialization test_initialize_stores_admin, test_initialize_twice_panics
Not-initialized guard test_set_base_reserve_before_initialize_panics
Safe-default reads test_get_base_reserve_returns_none_when_not_set, test_has_base_reserve_returns_false_when_not_set, test_require_base_reserve_panics_when_not_set
Set/get round-trip test_set_and_get_base_reserve, test_set_base_reserve_minimum_valid_value
Overwrite behaviour test_set_base_reserve_overwrites_previous_value
Input validation (lower) test_set_base_reserve_zero_is_rejected, test_set_base_reserve_negative_is_rejected, test_set_base_reserve_min_i128_is_rejected
Input validation (upper) test_set_base_reserve_at_max_is_accepted, test_set_base_reserve_above_max_is_rejected, test_set_base_reserve_huge_value_is_rejected
State isolation test_two_contracts_are_independent
Admin accessor test_get_admin_returns_none_before_init, test_get_admin_returns_admin_after_init
TTL management test_ttl_extended_after_read, test_ttl_extended_after_write

Test helpers

  • create_env() — builds a Env with min_persistent_entry_ttl = 50 (below TTL threshold) and max_entry_ttl = 600_000 (above extend target) so TTL assertions are reliable.
  • setup() — deploys and initializes a contract; returns (env, client, admin, contract_id).
  • setup_uninitialized() — deploys only, no initialize call.
  • assert_ttl_extended(env, contract_id) — reads the instance TTL inside the contract context and asserts it is ≥ 518,400 ledgers.

Running the Tests

cargo test -p reserve_contract

All 20 tests should pass. Snapshots live in test_snapshots/test/test/ and are updated automatically when the contract's observable behaviour changes.

🐛 Bug Report: sweep_controller — Integration Tests Failing

Date: 2026-02-23
Contract: contracts/sweep_controller
Files affected:

  • contracts/sweep_controller/tests/integration.rs
  • contracts/sweep_controller/src/authorization.rs

Description

The integration tests in contracts/sweep_controller/tests/integration.rs do not pass. Multiple tests fail or produce misleading results because they use hardcoded dummy Ed25519 signatures (e.g., [0u8; 64], [1u8; 64], [2u8; 64]) while the contract performs real cryptographic Ed25519 verification via env.crypto().ed25519_verify().

In the Soroban SDK, ed25519_verify does not return a Result — it panics when the signature is invalid. This causes tests that expect a successful sweep to panic unexpectedly, and tests that attempt to catch errors via std::panic::catch_unwind to catch the wrong kind of failure.

Copy link
Copy Markdown
Contributor

@phertyameen phertyameen left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Joaco2603
This is awesome. Learnt alot from the well explained pr message. I will create the issue to handle the sweep controller failled tests too. Could really use your expertise soon. Could you drop your telegram username as a comment so I can add you to the bridglet group?

@phertyameen phertyameen merged commit 5b8c44c into bridgelet-org:main Feb 25, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Implement Base Reserve Storage Layer

2 participants