Skip to content

Commit 0c32ac8

Browse files
Fix contract restore panic when restoring a missing entry (#2660)
### What Guard `stellar contract restore` against the same "index out of bounds" panic that #2657 fixed in `extend`: the no-op path unconditionally indexed `entry.entries[0]`, which panics if the post-transaction fetch returns no entries. It now errors with "Ledger entry not found" instead. ### Why `restore` carried the identical unguarded indexing as `extend` (see #2599), so this applies the same `.first().ok_or(Error::LedgerEntryNotFound)?` fix. Unlike extend, restoring a non-existent entry fails cleanly at simulation ("Missing entry to restore") before reaching the no-op path, so this guard is defensive hardening for the narrower case where the fetch after a no-op comes back empty (e.g. the entry was evicted in the meantime). The regression test asserts the non-existent-entry case fails cleanly at simulation without panicking. ### Known limitations The empty-fetch-after-no-op case itself isn't covered by an integration test since simulation catches the straightforward missing-entry scenario first.
1 parent 380f7c1 commit 0c32ac8

2 files changed

Lines changed: 30 additions & 1 deletion

File tree

‎cmd/crates/soroban-test/tests/it/integration/hello_world.rs‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,27 @@ async fn extend_nonexistent_entry_errors_without_panic() {
365365
.stderr(predicates::str::contains("panicked").not());
366366
}
367367

368+
#[tokio::test]
369+
async fn restore_nonexistent_entry_errors_without_panic() {
370+
// Restoring a well-formed but non-existent contract id fails at
371+
// simulation with "Missing entry to restore". The CLI must surface that
372+
// as a clean error and never panic with "index out of bounds" while
373+
// inspecting empty ledger entries. See issue #2599.
374+
const NONEXISTENT_ID: &str = "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4";
375+
let sandbox = &TestEnv::new();
376+
377+
sandbox
378+
.new_assert_cmd("contract")
379+
.arg("restore")
380+
.arg("--id")
381+
.arg(NONEXISTENT_ID)
382+
.assert()
383+
.failure()
384+
.stderr(predicates::str::contains("Missing entry to restore"))
385+
.stderr(predicates::str::contains("index out of bounds").not())
386+
.stderr(predicates::str::contains("panicked").not());
387+
}
388+
368389
async fn invoke_with_seed(sandbox: &TestEnv, id: &str, seed_phrase: &str) {
369390
invoke_with_source(sandbox, seed_phrase, id).await;
370391
}

‎cmd/soroban-cli/src/commands/contract/restore.rs‎

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -262,7 +262,15 @@ impl Cmd {
262262
if changes.is_empty() {
263263
print.infoln("No changes detected, transaction was a no-op.");
264264
let entry = client.get_full_ledger_entries(&entry_keys).await?;
265-
let extension = entry.entries[0].live_until_ledger_seq.unwrap_or_default();
265+
// The fetch after a no-op can return no entries (e.g. the entry
266+
// was evicted in the meantime), so avoid indexing into an empty
267+
// vec (which would panic).
268+
let extension = entry
269+
.entries
270+
.first()
271+
.ok_or(Error::LedgerEntryNotFound)?
272+
.live_until_ledger_seq
273+
.unwrap_or_default();
266274

267275
return Ok(TxnResult::Res(extension));
268276
}

0 commit comments

Comments
 (0)