Skip to main content

Program Escrow — Lifecycle Dwell-Time Metrics

Last updated: 16 August 2026

Documents a contract that does not exist

The only Soroban contract in this project is grainhack-escrow (initialise, fund, publish_root, claim, is_claimed, sweep), and it has never been deployed. The program-escrow and bounty-escrow surfaces this page describes — publish_program, lock_program_funds, single_payout, batch_payout, create_bounty and the rest — have no implementation in any repository, verified 16 August 2026.

This page is kept as a design record. Do not read it as a description of working software, and do not implement against it without checking what the contract actually exposes. See Payout path — what exists today.

Overview​

This feature extends the on-chain analytics of the Program Escrow contract to record per-status transition timestamps for every program. Ecosystem operators can query get_program_lifecycle_timeline to see how long each program spent in Draft or Active status before transitioning, enabling detection of stalled or abandoned programs.

Data Model​

StatusTransition​

pub struct StatusTransition {
pub from_status: ProgramStatus,
pub to_status: ProgramStatus,
pub timestamp: u64,
}

Each transition records:

  • The status being transitioned from.
  • The status being transitioned to.
  • The ledger timestamp (seconds since Unix epoch) when the transition was recorded.

ProgramLifecycleTimeline​

pub struct ProgramLifecycleTimeline {
pub transitions: Vec<StatusTransition>,
}

Stored under the separate storage key DataKey::LifecycleTimeline(program_id), a timeline is an ordered list of transitions (oldest first). Because the timeline lives under its own storage key, adding it does not alter the field ordering of ProgramData or Analytics, preserving full storage compatibility.

Transitions Recorded​

TriggerTransition
init_program / initialize_programDraft → Draft ¹
publish_programDraft → Active
batch_initialize_programsDraft → Draft ¹

¹ The initial Draft→Draft entry records the moment the program was created so that dwell time in Draft can be computed even if the program is never published.

Query Function​

get_program_lifecycle_timeline(env, program_id)​

pub fn get_program_lifecycle_timeline(
env: Env,
program_id: String,
) -> Vec<StatusTransition>

Returns the ordered list of status transitions for the given program. Returns an empty Vec when:

  • The program ID does not exist.
  • The program was created before this feature was deployed (legacy program with no timeline stored).

Computing Dwell Times​

Clients compute dwell time by subtracting consecutive timestamps:

dwell_in_draft = transitions[1].timestamp - transitions[0].timestamp

Example:

StatusTransition { from: Draft, to: Draft, timestamp: 1000 } ← created
StatusTransition { from: Draft, to: Active, timestamp: 172_800 } ← published

Dwell in Draft = 172_800 - 1000 = 171_800 seconds (~2 days)

Security Considerations​

Append-Only Semantics​

New transitions are appended to the end of the vec. No existing entry is ever modified or removed. This ensures that the timeline is an immutable audit trail.

No Replay Risk​

The record_status_transition helper is called after the status change has been persisted to ProgramData. If the transition guard fails (e.g. publishing an already‑published program), the function panics before reaching the record_status_transition call, so no spurious transition is recorded.

Deterministic Timestamps​

Timestamps come from env.ledger().timestamp(), which is part of the deterministic Soroban host interface. All validators agree on the same value, so dwell-time computations are consensus-safe.

Upgrade Safety​

  • ProgramData field ordering is unchanged.
  • Analytics field ordering is unchanged.
  • The timeline is stored under a new DataKey::LifecycleTimeline variant.
  • Legacy programs that predate this feature return an empty vec — they do not panic.

Test Coverage​

The test suite in test_lifecycle_dwell_time.rs covers:

ScenarioAssertion
init_program records initial Draft→Draft entryTimeline length = 1, timestamp matches ledger
publish_program appends Draft→Active entryTimeline length = 2, dwell > 0
Full lifecycle with non‑zero dwell timedwell_seconds == expected delta
batch_initialize_programs (via individual init)Each program has its own timeline
Non‑existent / legacy programEmpty vec returned
Double‑publish panics without spurious transitionTimeline unchanged after panic
Independent timelines for multiple programsTimelines do not interfere
StatusTransition struct field correctnessField getters return expected values

Gas / Storage Impact​

Each transition appends one StatusTransition (2 × enum discriminant + 1 × u64 ~ 50 bytes XDR) to the timeline vec. For the common case (Draft → Active → terminal), this adds ~150 bytes to persistent storage per program — negligible compared to the existing ProgramData payload.

Future Extensions​

  • Terminal transitions: Completed / Cancelled states can be added to ProgramStatus and wired into record_status_transition when those code paths are implemented.
  • Caller attribution: An optional triggered_by: Address field could be added to StatusTransition in a future storage schema version.
  • Time-weighted metrics: Off-chain indexers can compute average dwell times across all programs to surface stalled programs.