Skip to content

all 4 fixed#562

Merged
OlufunbiIK merged 1 commit into
OlufunbiIK:mainfrom
MadakiElisha:eli
Apr 28, 2026
Merged

all 4 fixed#562
OlufunbiIK merged 1 commit into
OlufunbiIK:mainfrom
MadakiElisha:eli

Conversation

@MadakiElisha
Copy link
Copy Markdown
Contributor

@MadakiElisha MadakiElisha commented Apr 28, 2026

close #513
close #518
close #519
close #520

Summary by CodeRabbit

  • New Features

    • Manager-based authorization for artist allowlist operations
    • Holder query endpoints: list holders by page and rank top fans by balance
    • Structured event tracking for tip creation, claims, and refunds
  • Improvements

    • Enhanced data persistence with automatic TTL management
    • Storage consistency tracking for token holder state

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 28, 2026

@MadakiElisha is attempting to deploy a commit to the olufunbiik's projects Team on Vercel.

A member of the Team first needs to authorize it.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 28, 2026

📝 Walkthrough

Walkthrough

This PR implements delegated manager roles for artist-allowlist with TTL hardening, adds holder indexing and top-fan query capabilities to fan-token, and centralizes event publishing in tip-time-lock. Changes include new storage abstractions, authorization layers, and index maintenance logic across multiple contracts.

Changes

Cohort / File(s) Summary
Artist Allowlist Access Control
contracts/artist-allowlist/src/access.rs
New require_artist_or_manager helper enforcing caller authentication and delegated manager checks via storage lookup.
Artist Allowlist Storage & TTL Management
contracts/artist-allowlist/src/storage.rs, contracts/artist-allowlist/src/indexes.rs
New storage module centralizing persistent accessors for config, entries, token gates, and manager state with TTL bumping on read/write paths. Index module adds TTL extension for ArtistEntries keys during mutations and non-empty reads.
Artist Allowlist Core Logic
contracts/artist-allowlist/src/lib.rs
Adds set_manager and is_manager contract methods; updates all mutating functions to accept caller parameter and gate execution via require_artist_or_manager; shifts all persistent storage access to new storage::* helpers; includes caller in emitted event payloads.
Artist Allowlist Tests
contracts/artist-allowlist/src/test.rs
Updates all allowlist/admin function calls to pass caller address; adds new tests validating delegated manager authorization, revocation, and unauthorized access rejection.
Fan Token Holder Indexing
contracts/fan-token/src/storage.rs
Adds HolderIndex storage key and get_holders/sync_holder functions to maintain enumerable holder lists and update membership on balance changes with TTL extension.
Fan Token Queries & Integration
contracts/fan-token/src/queries.rs, contracts/fan-token/src/lib.rs
New queries module providing list_holders (paginated enumeration) and top_fans (rank-sorted by balance). Main contract wires new query entry points and calls storage::sync_holder after all balance mutations.
Fan Token Tests
contracts/fan-token/src/test.rs
Adds pagination and ranking tests for holder enumeration and top-fans queries; validates consistency of holder removal on zero balance and ranking updates post-transfer.
Tip Time Lock Events Module
contracts/tip-time-lock/src/events.rs
New dedicated events module defining canonical topic symbols, TipActionEvent contract type, and emit helpers for create/claim/refund lifecycle with consistent payload structure.
Tip Time Lock Refactoring
contracts/tip-time-lock/src/lib.rs
Moves inline event publishing to new dedicated events module; calls emit_tip_created, emit_tip_claimed, emit_tip_refunded helpers instead of constructing/publishing events directly.
Tip Time Lock Tests
contracts/tip-time-lock/src/test.rs
Adds event assertion helper converting event log to debug string; lifecycle tests now validate presence of expected event types and key fields in emitted snapshots.

Sequence Diagrams

sequenceDiagram
    actor Caller
    participant Contract
    participant Storage
    
    Caller->>Contract: set_allowlist_mode(artist, caller, mode)
    Contract->>Contract: require_artist_or_manager(artist, caller)
    Contract->>Storage: is_manager(artist, caller)
    Storage-->>Contract: bool (manager check result)
    alt Caller is artist or authorized manager
        Contract->>Storage: set_config(artist, config)
        Storage->>Storage: bump TTL
        Contract->>Contract: emit event with caller
        Contract-->>Caller: Ok(())
    else Unauthorized
        Contract-->>Caller: Error::Unauthorized
    end
Loading
sequenceDiagram
    actor Caller
    participant Contract
    participant Storage
    
    Caller->>Contract: mint_for_tip(artist, holder, amount)
    Contract->>Contract: update balance
    Contract->>Storage: sync_holder(artist, holder, new_balance)
    alt Balance > 0 and holder not in list
        Storage->>Storage: append holder to list
    else Balance <= 0 and holder in list
        Storage->>Storage: remove holder (swap & pop)
    end
    Storage->>Storage: persist updated list
    Storage->>Storage: bump HolderIndex TTL
    Contract-->>Caller: Ok(())
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 Manager roles now bloom, TTL extends so bright,
Holders lined up for ranking, queries taking flight!
Events find their module, organized with care,
Authorization layers make the contracts fair!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title "all 4 fixed" is vague and does not clearly convey what the pull request accomplishes, using a non-descriptive placeholder term. Consider a more descriptive title that summarizes the primary changes, such as "Add manager roles, TTL hardening, holder queries, and event module normalization" or similar.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Linked Issues check ✅ Passed All code requirements from issues #513, #518, #519, and #520 are fully addressed across files modified and created.
Out of Scope Changes check ✅ Passed All changes are scoped to the four linked issues; no unrelated modifications were introduced outside the specified requirements.
Docstring Coverage ✅ Passed Docstring coverage is 95.06% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
contracts/tip-time-lock/src/test.rs (1)

484-485: ⚠️ Potential issue | 🟡 Minor

Reuse the returned lock_id instead of hard-coding "1".

These tests assume the first generated id is always 1, which makes them fail for the wrong reason if the id scheme ever changes. Capture the value returned from create_time_lock_tip and use it in the later claim/refund calls.

Also applies to: 530-530, 587-587

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/tip-time-lock/src/test.rs` around lines 484 - 485, The test is
hard-coding "1" for the lock id; change it to capture and reuse the id returned
by create_time_lock_tip (e.g., let lock_id =
client.create_time_lock_tip(...).unwrap() or the actual return binding used in
tests) and pass that variable to client.claim_tip and client.refund_tip instead
of String::from_str("1"); update all occurrences (the claim/refund calls) to use
the captured lock_id (converting to the expected type/string only if necessary)
so tests no longer assume the id scheme.
contracts/artist-allowlist/src/lib.rs (1)

132-145: ⚠️ Potential issue | 🟠 Major

Move token validation to set_token_gate with proper error handling.

The Client::new() call on line 134 does not validate that token_address is a real token contract—it only creates a client wrapper. The misleading comment on lines 132–133 should be removed. When an invalid token address is set, set_token_gate returns success, but check_can_tip will panic later when it calls client.balance() because check_can_tip returns a bool with no error handling.

Validate the token address during setup by calling an actual token method (e.g., balance() or transfer_from()) and map any failures to return InvalidTokenConfig. This prevents silent acceptance of invalid tokens and provides proper error feedback to the caller.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/artist-allowlist/src/lib.rs` around lines 132 - 145, Remove the
misleading comment and the pointless token::Client::new() wrapper call from the
setup path and instead perform real validation inside storage::set_token_gate
(or the public function that calls it) by creating token::Client and invoking a
token method such as client.balance(artist) (or another read method) and map any
client errors into the InvalidTokenConfig error; update set_token_gate's
signature to return a Result (propagate that Result to the public setter) so
failures are returned to the caller and ensure check_can_tip continues to use
token::Client but no longer needs to guard against an unvalidated token address
because invalid tokens will be rejected at setup.
🧹 Nitpick comments (1)
contracts/tip-time-lock/src/test.rs (1)

30-38: Tighten the event assertion helper.

Searching the Debug output of the full event log is brittle: unrelated events can satisfy these substring checks, and any change in the SDK's formatting will break the tests. Prefer asserting the latest contract event and decoding its payload directly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/tip-time-lock/src/test.rs` around lines 30 - 38, The helper
assert_event_snapshot_contains currently searches the Debug string of
env.events().all(), which is brittle; change assert_event_snapshot_contains to
fetch the most recent event (e.g., let last =
env.events().all().last().expect(...)) and assert against its decoded
payload/fields instead of substring matching the Debug output. Decode the event
payload (from the event's data/topics as provided by the SDK) and compare
specific fields or expected strings directly so the test inspects the latest
contract event value rather than scanning the entire debug dump.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@contracts/artist-allowlist/src/indexes.rs`:
- Line 27: The code is extending IndexKey::ArtistEntries TTL via bump(env, &key)
without touching the underlying DataKey::Entry TTLs, causing
list_allowlist/get_allowlist_count to become stale; either remove the index-only
bump calls (references: bump, IndexKey::ArtistEntries) from the affected
functions or, if you must refresh the index, also iterate the referenced
DataKey::Entry items and call storage::get_entry/has_entry (or explicitly
refresh/prune those entry keys) so entry TTLs stay in sync; update the logic in
the functions that call bump (and corresponding blocks at the other locations
mentioned) to use one consistent approach.

In `@contracts/artist-allowlist/src/lib.rs`:
- Around line 169-171: The allowlist mutation paths (e.g., after
storage::set_entry and indexes::add_to_index) must refresh the config TTL so
DataKey::Config does not expire and cause check_can_tip to silently fall back to
true; add a call to a new helper (e.g., config::refresh_config_ttl(&env) or
config::touch_config(&env)) immediately after each mutation to update the
DataKey::Config entry's expiration while preserving its contents, implement that
helper to read the existing DataKey::Config, re-save it with the same payload
and a renewed TTL, and apply this change at all mutation sites mentioned
(including the other occurrences around 192-194, 206-209, 278-280, 314-316).

In `@contracts/fan-token/src/queries.rs`:
- Around line 21-61: The top_fans function currently builds and fully
selection-sorts all holder balances (ranked) with O(n²); change it to compute
only the top `limit` items by maintaining a bounded min-heap or a fixed-size
top-k buffer while iterating holders from storage::get_holders: for each
FanBalance (from storage::get_balance) insert into a min-heap of size at most
`limit` (evict the smallest when capacity exceeded) or keep a sorted Vec of up
to `limit` and insert/evict appropriately, then drain the heap/buffer into `top`
in descending order; update references to ranked, len, and the final take logic
so you never perform the full selection sort and only track up to `limit`
FanBalance entries.
- Around line 11-12: The computation of end uses plain addition which can
overflow even though start uses saturating_mul; change the end calculation to
use a saturating add (e.g., replace `start + page_size` with
`start.saturating_add(page_size)`) so the expression in the function that
computes pagination (the `start`/`end` logic in queries.rs) becomes `let end =
start.saturating_add(page_size).min(holders.len());` ensuring extreme `page`
values cannot overflow or wrap.

In `@contracts/fan-token/src/storage.rs`:
- Around line 84-115: sync_holder currently writes an empty holder Vec back to
storage and extends its TTL when the last holder is removed, leaving a dead
DataKey::HolderIndex entry; update sync_holder so that after removing the last
holder (when holders.is_empty()), you remove the key from storage instead of
calling env.storage().persistent().set and do not call extend_ttl for that
key—use env.storage().persistent().remove(&DataKey::HolderIndex(artist.clone()))
and only call extend_ttl when you set a non-empty holders Vec.

---

Outside diff comments:
In `@contracts/artist-allowlist/src/lib.rs`:
- Around line 132-145: Remove the misleading comment and the pointless
token::Client::new() wrapper call from the setup path and instead perform real
validation inside storage::set_token_gate (or the public function that calls it)
by creating token::Client and invoking a token method such as
client.balance(artist) (or another read method) and map any client errors into
the InvalidTokenConfig error; update set_token_gate's signature to return a
Result (propagate that Result to the public setter) so failures are returned to
the caller and ensure check_can_tip continues to use token::Client but no longer
needs to guard against an unvalidated token address because invalid tokens will
be rejected at setup.

In `@contracts/tip-time-lock/src/test.rs`:
- Around line 484-485: The test is hard-coding "1" for the lock id; change it to
capture and reuse the id returned by create_time_lock_tip (e.g., let lock_id =
client.create_time_lock_tip(...).unwrap() or the actual return binding used in
tests) and pass that variable to client.claim_tip and client.refund_tip instead
of String::from_str("1"); update all occurrences (the claim/refund calls) to use
the captured lock_id (converting to the expected type/string only if necessary)
so tests no longer assume the id scheme.

---

Nitpick comments:
In `@contracts/tip-time-lock/src/test.rs`:
- Around line 30-38: The helper assert_event_snapshot_contains currently
searches the Debug string of env.events().all(), which is brittle; change
assert_event_snapshot_contains to fetch the most recent event (e.g., let last =
env.events().all().last().expect(...)) and assert against its decoded
payload/fields instead of substring matching the Debug output. Decode the event
payload (from the event's data/topics as provided by the SDK) and compare
specific fields or expected strings directly so the test inspects the latest
contract event value rather than scanning the entire debug dump.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1184f7dc-0eeb-4fc6-b454-710f00ed4a40

📥 Commits

Reviewing files that changed from the base of the PR and between 220f861 and 687ae41.

📒 Files selected for processing (12)
  • contracts/artist-allowlist/src/access.rs
  • contracts/artist-allowlist/src/indexes.rs
  • contracts/artist-allowlist/src/lib.rs
  • contracts/artist-allowlist/src/storage.rs
  • contracts/artist-allowlist/src/test.rs
  • contracts/fan-token/src/lib.rs
  • contracts/fan-token/src/queries.rs
  • contracts/fan-token/src/storage.rs
  • contracts/fan-token/src/test.rs
  • contracts/tip-time-lock/src/events.rs
  • contracts/tip-time-lock/src/lib.rs
  • contracts/tip-time-lock/src/test.rs

.unwrap_or_else(|| Vec::new(env));
entries.push_back(address.clone());
env.storage().persistent().set(&key, &entries);
bump(env, &key);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don't extend the index TTL independently from the entry TTLs.

These paths now keep IndexKey::ArtistEntries alive, but the underlying DataKey::Entry records are only refreshed through storage::get_entry/has_entry. That means list_allowlist and get_allowlist_count can drift stale after entry keys expire, while is_on_allowlist correctly returns false. Please either refresh/prune the entry keys here as well, or stop bumping the index on its own.

Also applies to: 48-48, 62-64, 77-84

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/artist-allowlist/src/indexes.rs` at line 27, The code is extending
IndexKey::ArtistEntries TTL via bump(env, &key) without touching the underlying
DataKey::Entry TTLs, causing list_allowlist/get_allowlist_count to become stale;
either remove the index-only bump calls (references: bump,
IndexKey::ArtistEntries) from the affected functions or, if you must refresh the
index, also iterate the referenced DataKey::Entry items and call
storage::get_entry/has_entry (or explicitly refresh/prune those entry keys) so
entry TTLs stay in sync; update the logic in the functions that call bump (and
corresponding blocks at the other locations mentioned) to use one consistent
approach.

Comment on lines +169 to 171
storage::set_entry(&env, &artist, &address, &entry);

indexes::add_to_index(&env, &artist, &address);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Refresh the config TTL when the allowlist is actively mutated.

These write paths only touch entry/index state. Since check_can_tip falls back to true when DataKey::Config is missing at Lines 206-209, an artist can keep actively managing an AllowlistOnly setup and still silently fail open once the config key expires. Please bump the config key alongside these mutations, or centralize a helper that touches all state needed to enforce the current mode.

Also applies to: 192-194, 206-209, 278-280, 314-316

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/artist-allowlist/src/lib.rs` around lines 169 - 171, The allowlist
mutation paths (e.g., after storage::set_entry and indexes::add_to_index) must
refresh the config TTL so DataKey::Config does not expire and cause
check_can_tip to silently fall back to true; add a call to a new helper (e.g.,
config::refresh_config_ttl(&env) or config::touch_config(&env)) immediately
after each mutation to update the DataKey::Config entry's expiration while
preserving its contents, implement that helper to read the existing
DataKey::Config, re-save it with the same payload and a renewed TTL, and apply
this change at all mutation sites mentioned (including the other occurrences
around 192-194, 206-209, 278-280, 314-316).

Comment on lines +11 to +12
let start = page.saturating_mul(page_size);
let end = (start + page_size).min(holders.len());
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Guard the end-index arithmetic.

start + page_size can overflow for large page values even though the start offset is saturated. Use a saturating add here so extreme inputs don't turn the query into a panic or wraparound.

♻️ Suggested fix
-    let end = (start + page_size).min(holders.len());
+    let end = start.saturating_add(page_size).min(holders.len());
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let start = page.saturating_mul(page_size);
let end = (start + page_size).min(holders.len());
let start = page.saturating_mul(page_size);
let end = start.saturating_add(page_size).min(holders.len());
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/fan-token/src/queries.rs` around lines 11 - 12, The computation of
end uses plain addition which can overflow even though start uses
saturating_mul; change the end calculation to use a saturating add (e.g.,
replace `start + page_size` with `start.saturating_add(page_size)`) so the
expression in the function that computes pagination (the `start`/`end` logic in
queries.rs) becomes `let end =
start.saturating_add(page_size).min(holders.len());` ensuring extreme `page`
values cannot overflow or wrap.

Comment on lines +21 to +61
pub fn top_fans(env: &Env, artist: &Address, limit: u32) -> Vec<FanBalance> {
if limit == 0 {
return Vec::new(env);
}

let holders = storage::get_holders(env, artist);
let mut ranked = Vec::new(env);

for holder in holders.iter() {
if let Some(balance) = storage::get_balance(env, artist, &holder) {
if balance.balance > 0 {
ranked.push_back(balance);
}
}
}

let len = ranked.len();
for i in 0..len {
let mut max_idx = i;
for j in (i + 1)..len {
let left = ranked.get(max_idx).unwrap();
let right = ranked.get(j).unwrap();
if right.balance > left.balance {
max_idx = j;
}
}
if max_idx != i {
let vi = ranked.get(i).unwrap();
let vmax = ranked.get(max_idx).unwrap();
ranked.set(i, vmax);
ranked.set(max_idx, vi);
}
}

let take = limit.min(ranked.len());
let mut top = Vec::new(env);
for i in 0..take {
top.push_back(ranked.get(i).unwrap());
}
top
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Avoid ranking all holders with selection sort.

This walks the entire holder set with O(n²) work even though callers only need the top limit. That will get expensive quickly as the holder index grows; a bounded top-k pass would scale much better.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/fan-token/src/queries.rs` around lines 21 - 61, The top_fans
function currently builds and fully selection-sorts all holder balances (ranked)
with O(n²); change it to compute only the top `limit` items by maintaining a
bounded min-heap or a fixed-size top-k buffer while iterating holders from
storage::get_holders: for each FanBalance (from storage::get_balance) insert
into a min-heap of size at most `limit` (evict the smallest when capacity
exceeded) or keep a sorted Vec of up to `limit` and insert/evict appropriately,
then drain the heap/buffer into `top` in descending order; update references to
ranked, len, and the final take logic so you never perform the full selection
sort and only track up to `limit` FanBalance entries.

Comment on lines +84 to +115
pub fn sync_holder(env: &Env, artist: &Address, holder: &Address, balance: i128) {
let key = DataKey::HolderIndex(artist.clone());
let mut holders: soroban_sdk::Vec<Address> = env
.storage()
.persistent()
.get(&key)
.unwrap_or_else(|| soroban_sdk::Vec::new(env));

let mut idx: Option<u32> = None;
for i in 0..holders.len() {
if holders.get(i).unwrap() == *holder {
idx = Some(i);
break;
}
}

if balance > 0 {
if idx.is_none() {
holders.push_back(holder.clone());
}
} else if let Some(i) = idx {
let last = holders.pop_back().unwrap();
if i < holders.len() {
holders.set(i, last);
}
}

env.storage().persistent().set(&key, &holders);
env.storage()
.persistent()
.extend_ttl(&key, LIFETIME_THRESHOLD, EXTEND_TO);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Drop the holder-index key once it becomes empty.

sync_holder still writes an empty vector and renews its TTL after the last holder is removed. That leaves a dead index entry around and works against the rent-hardening goal for artists whose holder list is empty.

🧹 Suggested fix
-    env.storage().persistent().set(&key, &holders);
-    env.storage().persistent().extend_ttl(&key, LIFETIME_THRESHOLD, EXTEND_TO);
+    if holders.is_empty() {
+        env.storage().persistent().remove(&key);
+    } else {
+        env.storage().persistent().set(&key, &holders);
+        env.storage().persistent().extend_ttl(&key, LIFETIME_THRESHOLD, EXTEND_TO);
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
pub fn sync_holder(env: &Env, artist: &Address, holder: &Address, balance: i128) {
let key = DataKey::HolderIndex(artist.clone());
let mut holders: soroban_sdk::Vec<Address> = env
.storage()
.persistent()
.get(&key)
.unwrap_or_else(|| soroban_sdk::Vec::new(env));
let mut idx: Option<u32> = None;
for i in 0..holders.len() {
if holders.get(i).unwrap() == *holder {
idx = Some(i);
break;
}
}
if balance > 0 {
if idx.is_none() {
holders.push_back(holder.clone());
}
} else if let Some(i) = idx {
let last = holders.pop_back().unwrap();
if i < holders.len() {
holders.set(i, last);
}
}
env.storage().persistent().set(&key, &holders);
env.storage()
.persistent()
.extend_ttl(&key, LIFETIME_THRESHOLD, EXTEND_TO);
}
pub fn sync_holder(env: &Env, artist: &Address, holder: &Address, balance: i128) {
let key = DataKey::HolderIndex(artist.clone());
let mut holders: soroban_sdk::Vec<Address> = env
.storage()
.persistent()
.get(&key)
.unwrap_or_else(|| soroban_sdk::Vec::new(env));
let mut idx: Option<u32> = None;
for i in 0..holders.len() {
if holders.get(i).unwrap() == *holder {
idx = Some(i);
break;
}
}
if balance > 0 {
if idx.is_none() {
holders.push_back(holder.clone());
}
} else if let Some(i) = idx {
let last = holders.pop_back().unwrap();
if i < holders.len() {
holders.set(i, last);
}
}
if holders.is_empty() {
env.storage().persistent().remove(&key);
} else {
env.storage().persistent().set(&key, &holders);
env.storage()
.persistent()
.extend_ttl(&key, LIFETIME_THRESHOLD, EXTEND_TO);
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@contracts/fan-token/src/storage.rs` around lines 84 - 115, sync_holder
currently writes an empty holder Vec back to storage and extends its TTL when
the last holder is removed, leaving a dead DataKey::HolderIndex entry; update
sync_holder so that after removing the last holder (when holders.is_empty()),
you remove the key from storage instead of calling
env.storage().persistent().set and do not call extend_ttl for that key—use
env.storage().persistent().remove(&DataKey::HolderIndex(artist.clone())) and
only call extend_ttl when you set a non-empty holders Vec.

@OlufunbiIK OlufunbiIK merged commit cc37362 into OlufunbiIK:main Apr 28, 2026
1 of 3 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

2 participants