OpenZeppelin / OpenZeppelin/stellar-contracts
🐞 [Bug]: Role enumeration counter-derived storage keys cause footprint invalidation under concurrency
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 95
- Forks
- 68
- Avg merge
- 4d 42m
- Merged PRs (30d)
- 20
Description
What happened?
add_to_role_enumeration in packages/access/src/access_control/storage.rs derives write keys from a shared mutable counter:
let count = e.storage().persistent().get(&count_key).unwrap_or(0);
let new_key = AccessControlStorageKey::RoleAccounts(RoleAccountKey { role, index: count });
e.storage().persistent().set(&new_key, account);
On Soroban, transaction footprints must declare exact storage keys at simulation time. If RoleAccountsCount advances between simulation and execution (another grant_role lands first), the contract writes to a key not in the footprint and the transaction traps:
"trying to access contract data key outside of the footprint"
["RoleAccounts", {"index": 7044, "role": "WLISTED"}]
Any two concurrent grant_role calls on the same role will race on the counter. Observed on Mainnet with a role containing ~7000 members.
Reproduction: two concurrent grant_role(env, caller, user_a, role) and grant_role(env, caller, user_b, role) (sent with channel accounts to have concurrency) — both simulate with count = N and both declare RoleAccounts({role, N}) in their write footprint. First lands and increments the counter to N+1. Second executes, reads the now-updated count = N+1, and tries to write RoleAccounts({role, N+1}) which is not in its declared footprint.
Expected behavior
grant_role should be safe to call concurrently on the same role. Storage keys in the write path should be deterministic from the function inputs.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in packages/access/src/access_control/storage.rs at add_to_role_enumeration, then trace its callers from grant_role. Reproduce the race with two concurrent grant_role calls using channel accounts and inspect the simulation and execution footprints. Done means concurrent grants on the same role no longer trap because the write keys are deterministic from the function inputs.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- blockchain
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100