Skip to content

stellar-contracts: make access-grant index creation provably idempotent - #1282

Merged
llinsss merged 2 commits into
DogStark:mainfrom
samuel2926i39-art:fix/1160-access-grant-index-dedup
Aug 31, 2026
Merged

stellar-contracts: make access-grant index creation provably idempotent#1282
llinsss merged 2 commits into
DogStark:mainfrom
samuel2926i39-art:fix/1160-access-grant-index-dedup

Conversation

@samuel2926i39-art

Copy link
Copy Markdown
Contributor

Summary

grant_access read instance storage twice to answer two related questions: "what was the grant's prior state" (used to build the updated AccessGrant record) and "is this grantee new to the index" (is_new_grant, used to decide whether to push a new AccessGrantIndex entry / bump AccessGrantCount). Both reads happened before any write to that key, so they always agreed in practice — but that correctness depended on nobody ever inserting a write between them, not on the code structurally guaranteeing it. That's a fragile invariant to rely on in a function whose whole job is "don't create a second index entry for the same grantee."

What changed

  • grant_access now performs one read of the existing AccessGrant (if any), and derives both the updated grant record and is_new_grant from that single Option. "One canonical index entry per grantee" is now true by construction, not by the absence of an intervening write.
  • No behavior change: the function still updates access level/expiry/active-state on an existing grant, and still only pushes a new AccessGrantIndex/AccessGrantCount entry the first time a given grantee is granted access for a pet. No public ABI, storage layout, or ContractError discriminant changed.

Test added

test_grant_access_repeated_calls_keep_one_canonical_index_entry (in test_access_control.rs): calls grant_access three times for the same (pet_id, grantee) — varying access level and expiry each time, using get_caller_nonce for correct nonce sequencing — then peeks at internal storage via env.as_contract (there's no public getter for AccessGrantCount/AccessGrantIndex) to assert the count stays at exactly 1 and index slot 1 still holds the grantee, and confirms check_access reflects the latest call's values (proving this was an update, not two independent records).

⚠️ Verification limitation — please read before reviewing

Same caveat as my companion PR for #1159 on this same file: stellar-contracts fails to compile on a clean upstream/main checkout with 346 pre-existing errors, entirely unrelated to access grants (duplicate const definitions, a panicking #[contracterror] macro, ContractError not resolving in some impl blocks, tests calling contract methods that don't exist). I could not run cargo build, clippy, or cargo test to a passing state here.

What I could do: I ran cargo build --lib after this change and confirmed none of the 346 errors are new or located at any line this PR touches (checked by line number and by searching the full error log for is_new_grant / the new test's name). While investigating, I also noticed the existing test_access_expiry test in this same file calls grant_access with only 4 arguments, but the real function signature takes 5 (pet_id, grantee, access_level, expires_at, nonce) — one more symptom of the broader pre-existing breakage. My new test uses the correct 5-argument signature.

I'd recommend a maintainer build/test this specific diff against a working local checkout before merging, or fix the underlying crate-wide breakage first — I don't want to claim verified-green work that isn't.

Threat-model note

This is a robustness/structural-correctness change, not a new authorization boundary: grant_access is still gated by owner.require_auth() and nonce-based replay protection, unchanged. The fix only affects how the function decides internally whether to grow the index — it doesn't change who can call it or what they're authorized to do. The risk this closes is a maintenance/drift risk (a future refactor inserting a write between the two old reads could silently reintroduce duplicate index entries) rather than a live exploit today.

Closes #1160

grant_access read storage twice to answer two related questions: what
was the prior grant state (to build the updated record), and whether
this grantee is new to the index (to decide whether to push a new
AccessGrantIndex entry). Both reads always agreed in practice since
nothing writes to the key between them, but that safety depended on the
absence of an intervening write rather than being structural.

Derives is_new_grant from the same single read used to build the
updated grant record, so "one canonical AccessGrantIndex/AccessGrantCount
entry per grantee" holds by construction. No behavior change, no public
ABI/storage layout/error discriminant change.

Adds a test proving repeated grant_access calls for the same (pet_id,
grantee) - across different access levels and expiry values - never
grow AccessGrantCount past 1 and never create a duplicate index slot.

Closes DogStark#1160
@drips-wave

drips-wave Bot commented Aug 29, 2026

Copy link
Copy Markdown

@samuel2926i39-art Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@llinsss
llinsss merged commit 632eb0a into DogStark:main Aug 31, 2026
0 of 20 checks 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.

[stellar-contracts] Prevent duplicate access-grant index entries

2 participants