Skip to content

Smart contract portfolio ID collision: ledger sequence reuse causes silent data overwrite on concurrent creates #27

Description

@Uchechukwu-Ekezie

Summary

create_portfolio derives its ID from env.ledger().sequence() as u64, which is the current ledger sequence number — a value shared across the entire blockchain, not unique per transaction:

// contracts/src/lib.rs
let portfolio_id = env.ledger().sequence() as u64; // Convert u32 to u64

Two transactions landing in the same ledger will produce the same portfolio_id. The second persistent().set silently overwrites the first, destroying the earlier user's portfolio without any error.

Impact

  • Loss of funds/state: A user's portfolio is overwritten by another user's, breaking all future deposit and rebalance operations for the original owner.
  • No error is raised: Both transactions succeed on-chain; the first user has no signal that their portfolio was destroyed.
  • Authorization bypass: After overwrite, portfolio.user becomes the second user's address, so the first user can no longer call deposit or execute_rebalance (requires_auth fails), effectively locking them out.

Steps to Reproduce

  1. Submit two create_portfolio calls in transactions included in the same ledger.
  2. Query both resulting portfolio IDs — they are identical.
  3. Fetch the portfolio — it contains the second user's allocations.

Root Cause

Ledger sequence advances once per closed ledger, not once per transaction or per contract invocation. Relying on it as a unique entity key is fundamentally unsafe.

Suggested Fix

Use a monotonically incrementing counter stored in contract instance storage:

let counter: u64 = env.storage().instance().get(&DataKey::PortfolioCounter).unwrap_or(0);
let portfolio_id = counter + 1;
env.storage().instance().set(&DataKey::PortfolioCounter, &portfolio_id);

Alternatively, derive the ID from a hash of (user, ledger_sequence, tx_hash) if Soroban exposes transaction-level entropy.

References

  • contracts/src/lib.rscreate_portfolio function
  • contracts/src/types.rsDataKey::Portfolio(u64) key

Severity: Critical — silent data loss on concurrent portfolio creation

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions