Skip to content

fix: stale ERC20 total supply#45

Open
pthmas wants to merge 3 commits intomainfrom
pthmas/debug-token-supply
Open

fix: stale ERC20 total supply#45
pthmas wants to merge 3 commits intomainfrom
pthmas/debug-token-supply

Conversation

@pthmas
Copy link
Copy Markdown
Collaborator

@pthmas pthmas commented Mar 31, 2026

Summary

  • stop treating one-time metadata fetches as the source of truth for ERC-20 total supply
  • update total supply from indexed mint and burn transfers during indexing
  • prefer indexed balance-derived supply in token and address detail responses

Reindexing is enough to apply this during development, so this PR does not include a backfill migration.

Summary by CodeRabbit

  • Bug Fixes
    • ERC-20 total supply now calculated from indexed balance transaction history rather than potentially stale contract metadata.
    • API endpoints for tokens and addresses automatically use dynamically-computed supply values when balance history is complete and verified.
    • Eliminates discrepancies caused by incomplete or outdated stored supply data.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 31, 2026

📝 Walkthrough

Walkthrough

Implemented ERC-20 supply history tracking by accumulating supply deltas during transaction indexing and conditionally computing indexed total supply in API responses when supply history is marked complete. Removes on-chain totalSupply() calls from metadata fetching and replaces stored supply values with indexed calculations when available.

Changes

Cohort / File(s) Summary
Supply delta accumulation
backend/crates/atlas-server/src/indexer/batch.rs
Added supply_map field to track per-contract supply deltas and apply_supply_delta() method to aggregate mint/burn deltas by contract address.
Indexer supply history management
backend/crates/atlas-server/src/indexer/indexer.rs, backend/crates/atlas-server/src/indexer/metadata.rs
Modified Transfer event handling to record mint/burn supply deltas in supply_map instead of skipping them; bulk-update erc20_contracts.total_supply from accumulated deltas; added supply history completeness state tracking and transitions to true when reaching chain head. Removed totalSupply() interface method and all on-chain supply fetching logic from metadata.
State management
backend/crates/atlas-server/src/state_keys.rs, backend/crates/atlas-server/src/lib.rs, backend/crates/atlas-server/src/main.rs, backend/crates/atlas-server/src/api/handlers/mod.rs
Added new ERC20_SUPPLY_HISTORY_COMPLETE_KEY constant; exported new module; introduced has_complete_erc20_supply_history() handler utility to check supply history completeness flag from indexer state.
API supply computation
backend/crates/atlas-server/src/api/handlers/addresses.rs, backend/crates/atlas-server/src/api/handlers/tokens.rs
Added get_indexed_erc20_total_supply() and get_indexed_total_supply() helpers that sum positive erc20_balances by contract. Modified API responses to prefer indexed supply over stored values when supply history is complete; otherwise fall back to original stored supply.
Integration tests
backend/crates/atlas-server/tests/integration/addresses.rs, backend/crates/atlas-server/tests/integration/tokens.rs
Added test helpers seed_erc20_address_data() and set_erc20_supply_history_complete() for ERC-20 test setup. Added integration tests verifying API returns indexed supply when history is complete and validating stale stored values are overridden.

Sequence Diagram(s)

sequenceDiagram
    participant Indexer
    participant BlockBatch
    participant Database
    participant APIHandler
    participant State

    Indexer->>BlockBatch: Detect Transfer (mint/burn)
    BlockBatch->>BlockBatch: apply_supply_delta(contract, delta)
    Indexer->>BlockBatch: Detect Transfer (transfer between addresses)
    BlockBatch->>BlockBatch: Record balance_delta per address
    Indexer->>Database: write_batch() with supply_map
    Database->>Database: UPDATE erc20_contracts.total_supply from unnest(deltas)
    note over Indexer: Reaches chain head
    Indexer->>State: Mark ERC20_SUPPLY_HISTORY_COMPLETE = "true"
    State->>Database: UPDATE indexer_state
Loading
sequenceDiagram
    participant Client
    participant APIHandler
    participant State
    participant Database as DB: erc20_balances

    Client->>APIHandler: GET /api/tokens/{address}
    APIHandler->>State: has_complete_erc20_supply_history()?
    State->>Database: SELECT value FROM indexer_state WHERE key=?
    Database-->>State: "true"
    State-->>APIHandler: true
    APIHandler->>Database: SELECT SUM(balance) FROM erc20_balances WHERE balance > 0
    Database-->>APIHandler: indexed_supply
    APIHandler-->>Client: {total_supply: indexed_supply}
    
    note over APIHandler: If supply_history incomplete
    APIHandler-->>Client: {total_supply: stored_value}
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested reviewers

  • tac0turtle

Poem

🐰 Supply deltas hop through batch,
Mints and burns in delta-map stash,
When history's complete at chain's crest,
Indexed sums beat stored values best—
hippity-hop, no more on-chain call!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 61.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'fix: stale ERC20 total supply' directly and concisely describes the main objective of the PR: fixing the problem of stale ERC-20 total supply values by switching from one-time metadata fetches to indexed supply updates.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch pthmas/debug-token-supply

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: 2

Caution

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

⚠️ Outside diff range comments (2)
backend/crates/atlas-server/src/indexer/metadata.rs (1)

284-315: ⚠️ Potential issue | 🟠 Major

Keep a bootstrap supply for tokens first seen mid-history.

erc20_balances and supply_map are only built from transfers that Atlas indexes from config.start_block onward. For a token that already had circulating supply before the indexer first sees it, removing the one-time totalSupply() read leaves no baseline to fall back to, so later mint/burn deltas can never reconstruct the real supply. Please keep a bootstrap snapshot here, or persist a separate baseline column that the indexed deltas can build on.

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

In `@backend/crates/atlas-server/src/indexer/metadata.rs` around lines 284 - 315,
fetch_erc20_contract_metadata no longer captures a token's pre-index history
supply; call IERC20Metadata::totalSupply().call().await (or equivalent
totalSupply() on the token contract) when fetching metadata and persist that
value as a bootstrap baseline so later indexed transfer deltas can reconstruct
real supply. Update the DB write in fetch_erc20_contract_metadata to bind and
store the returned total_supply into a new column (e.g., bootstrap_supply or
initial_supply) on the erc20_contracts row (and optionally a bootstrap_block
column) instead of relying solely on deltas, and ensure the function still sets
metadata_fetched = true after saving the bootstrap value.
backend/crates/atlas-server/src/api/handlers/tokens.rs (1)

100-145: ⚠️ Potential issue | 🟠 Major

Don't treat “some indexed rows exist” as “supply is complete.”

transfer_count > 0 and total.0 > 0 only mean we've indexed some activity, not that erc20_balances contains the full preexisting holder set. On a deployment that starts mid-chain, these branches can replace a valid stored supply with the sum of post-start deltas and skew holder percentages. Please gate the balance-derived override on an explicit backfill/completeness signal instead of COUNT(*) > 0.

🧹 Nitpick comments (1)
backend/crates/atlas-server/tests/integration/tokens.rs (1)

222-248: Please cover the changed /holders supply path too.

This PR also changes get_token_holders, but the current suite still exercises that endpoint with matching stored and indexed supply, so a regression there would pass unnoticed. Adding a stale erc20_contracts.total_supply case for /api/tokens/:address/holders would pin the new percentage behavior.

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

In `@backend/crates/atlas-server/tests/integration/tokens.rs` around lines 222 -
248, Add a parallel test to cover the /api/tokens/:address/holders path: copy
the pattern from get_token_detail_prefers_indexed_supply_over_stale_stored_value
(seed_token_data, mutate erc20_contracts.total_supply to a stale value via
sqlx::query), then call the holders endpoint
(Request::builder().uri(format!("/api/tokens/{}/holders", TOKEN_A))) and assert
the response.status is OK and that the response's total_supply field and any
percentage fields for holders are computed using the indexed supply (e.g., still
"1000000") rather than the stale stored value; reference the existing test name
and the get_token_holders endpoint to locate where to add this new assertion.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/crates/atlas-server/src/api/handlers/addresses.rs`:
- Around line 251-260: The erc20 total_supply override currently triggers based
on has_erc20_transfers and will return partial sums when the indexer started
late; change the logic in the erc20_contract handling so you only call
get_indexed_erc20_total_supply and override erc20.total_supply when the explicit
completeness/backfill signal used by the token handler is true (i.e., the same
"index complete" check the token handler uses), and do not rely solely on
has_erc20_transfers; keep the branch returning Some(erc20) otherwise and leave
total_supply as None when the completeness flag is not set.

In `@backend/migrations/20240109000001_recompute_erc20_supply.sql`:
- Around line 4-15: The current UPDATE can overwrite correct total_supply with
incomplete sums; change the WHERE clause logic so you only overwrite when the
computed b.total_supply is nonzero or when the existing c.total_supply is NULL
(i.e., preserve positive existing totals). In practice, modify the UPDATE on
erc20_contracts to set total_supply from the subquery only if b.total_supply > 0
OR c.total_supply IS NULL, referencing erc20_contracts.total_supply and the
computed b.total_supply from the erc20_balances aggregation to avoid zeroing out
known-good snapshots.

---

Outside diff comments:
In `@backend/crates/atlas-server/src/indexer/metadata.rs`:
- Around line 284-315: fetch_erc20_contract_metadata no longer captures a
token's pre-index history supply; call
IERC20Metadata::totalSupply().call().await (or equivalent totalSupply() on the
token contract) when fetching metadata and persist that value as a bootstrap
baseline so later indexed transfer deltas can reconstruct real supply. Update
the DB write in fetch_erc20_contract_metadata to bind and store the returned
total_supply into a new column (e.g., bootstrap_supply or initial_supply) on the
erc20_contracts row (and optionally a bootstrap_block column) instead of relying
solely on deltas, and ensure the function still sets metadata_fetched = true
after saving the bootstrap value.

---

Nitpick comments:
In `@backend/crates/atlas-server/tests/integration/tokens.rs`:
- Around line 222-248: Add a parallel test to cover the
/api/tokens/:address/holders path: copy the pattern from
get_token_detail_prefers_indexed_supply_over_stale_stored_value
(seed_token_data, mutate erc20_contracts.total_supply to a stale value via
sqlx::query), then call the holders endpoint
(Request::builder().uri(format!("/api/tokens/{}/holders", TOKEN_A))) and assert
the response.status is OK and that the response's total_supply field and any
percentage fields for holders are computed using the indexed supply (e.g., still
"1000000") rather than the stale stored value; reference the existing test name
and the get_token_holders endpoint to locate where to add this new assertion.
🪄 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: 1074d95b-09b1-4216-a69b-51e3b41f3034

📥 Commits

Reviewing files that changed from the base of the PR and between 1f86eb2 and 7001f55.

📒 Files selected for processing (8)
  • backend/crates/atlas-server/src/api/handlers/addresses.rs
  • backend/crates/atlas-server/src/api/handlers/tokens.rs
  • backend/crates/atlas-server/src/indexer/batch.rs
  • backend/crates/atlas-server/src/indexer/indexer.rs
  • backend/crates/atlas-server/src/indexer/metadata.rs
  • backend/crates/atlas-server/tests/integration/addresses.rs
  • backend/crates/atlas-server/tests/integration/tokens.rs
  • backend/migrations/20240109000001_recompute_erc20_supply.sql

@pthmas pthmas changed the title Fix stale ERC20 total supply fix: stale ERC20 total supply Mar 31, 2026
@pthmas
Copy link
Copy Markdown
Collaborator Author

pthmas commented Mar 31, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 31, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@pthmas
Copy link
Copy Markdown
Collaborator Author

pthmas commented Apr 1, 2026

@coderabbitai review

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 1, 2026

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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: 2

🧹 Nitpick comments (1)
backend/crates/atlas-server/src/api/handlers/mod.rs (1)

30-41: Extract the completion check into a pure helper and cover it in this file’s tests.

The new indexer_state branch is only exercised through the DB path here. Pulling the Some("true") check into a tiny pure function would let the existing #[cfg(test)] module cover missing-row / "false" / malformed-value cases without a database fixture.

As per coding guidelines, "Add unit tests for new logic in a #[cfg(test)] mod tests block in the same file".

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

In `@backend/crates/atlas-server/src/api/handlers/mod.rs` around lines 30 - 41,
Extract the "Some(\"true\")" check into a tiny pure helper (e.g., fn
parse_erc20_supply_history_complete(value: Option<&str>) -> bool) and have
has_complete_erc20_supply_history call that helper; then add a #[cfg(test)] mod
tests in the same file that unit-tests the helper for None (missing row),
Some("false"), Some("true"), and malformed values to cover all cases without
needing the DB fixture. Ensure the helper name and
has_complete_erc20_supply_history are referenced in tests so the DB path remains
unchanged but logic is fully exercised by unit tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@backend/crates/atlas-server/src/api/handlers/tokens.rs`:
- Around line 101-102: The current logic lets indexed supply leak when history
is incomplete: when has_complete_erc20_supply_history(&state.pool) is false you
must not surface erc20_contracts.total_supply (or any value derived from
get_indexed_total_supply) to callers of get_token or get_token_holders; change
the false-branch so that get_token clears/suppresses contract.total_supply (set
to None) and ensure get_token_holders does not fall back to the stored
erc20_contracts.total_supply value—either query a trusted on-chain source
instead or return no total_supply until has_complete_erc20_supply_history
returns true; apply the same change to the other occurrence around lines 137-145
where total_supply is currently used.

In `@backend/crates/atlas-server/src/indexer/indexer.rs`:
- Around line 122-127: The logic that sets ERC20 supply-history completion (via
get_erc20_supply_history_status and set_erc20_supply_history_complete) must only
mark complete when there are no outstanding gaps or permanently failed blocks;
update the code so it checks the failed_blocks set (or parked failed count) and
any deferred/failed retry state before setting the flag, and ensure any code
paths that move a block into failed_blocks immediately call
set_erc20_supply_history_complete(false) to reset the flag; apply this change to
the same pattern found around the other similar blocks (the other
get_/set_erc20_supply_history_status usages) so the flag represents a gap-free
condition and is cleared when a block is deferred to failed_blocks.

---

Nitpick comments:
In `@backend/crates/atlas-server/src/api/handlers/mod.rs`:
- Around line 30-41: Extract the "Some(\"true\")" check into a tiny pure helper
(e.g., fn parse_erc20_supply_history_complete(value: Option<&str>) -> bool) and
have has_complete_erc20_supply_history call that helper; then add a #[cfg(test)]
mod tests in the same file that unit-tests the helper for None (missing row),
Some("false"), Some("true"), and malformed values to cover all cases without
needing the DB fixture. Ensure the helper name and
has_complete_erc20_supply_history are referenced in tests so the DB path remains
unchanged but logic is fully exercised by unit tests.
🪄 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: 59fbd274-7d3d-4133-ac21-ffcb5ad929fd

📥 Commits

Reviewing files that changed from the base of the PR and between 7001f55 and 35b1c28.

📒 Files selected for processing (9)
  • backend/crates/atlas-server/src/api/handlers/addresses.rs
  • backend/crates/atlas-server/src/api/handlers/mod.rs
  • backend/crates/atlas-server/src/api/handlers/tokens.rs
  • backend/crates/atlas-server/src/indexer/indexer.rs
  • backend/crates/atlas-server/src/lib.rs
  • backend/crates/atlas-server/src/main.rs
  • backend/crates/atlas-server/src/state_keys.rs
  • backend/crates/atlas-server/tests/integration/addresses.rs
  • backend/crates/atlas-server/tests/integration/tokens.rs
✅ Files skipped from review due to trivial changes (3)
  • backend/crates/atlas-server/src/lib.rs
  • backend/crates/atlas-server/src/main.rs
  • backend/crates/atlas-server/src/api/handlers/addresses.rs
🚧 Files skipped from review as they are similar to previous changes (2)
  • backend/crates/atlas-server/tests/integration/tokens.rs
  • backend/crates/atlas-server/tests/integration/addresses.rs

Comment on lines +101 to +102
if has_complete_erc20_supply_history(&state.pool).await? {
contract.total_supply = Some(get_indexed_total_supply(&state.pool, &address).await?);
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

The incomplete-history path still exposes indexed supply.

backend/crates/atlas-server/src/indexer/indexer.rs now advances erc20_contracts.total_supply from indexed mint/burn deltas. When has_complete_erc20_supply_history() is false, get_token leaves that field untouched and get_token_holders falls back to it, so a fresh sync can still return a partial supply before the completeness flag flips. The false branch needs a separate trusted source, or it should suppress total_supply until history is complete.

Also applies to: 137-145

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

In `@backend/crates/atlas-server/src/api/handlers/tokens.rs` around lines 101 -
102, The current logic lets indexed supply leak when history is incomplete: when
has_complete_erc20_supply_history(&state.pool) is false you must not surface
erc20_contracts.total_supply (or any value derived from
get_indexed_total_supply) to callers of get_token or get_token_holders; change
the false-branch so that get_token clears/suppresses contract.total_supply (set
to None) and ensure get_token_holders does not fall back to the stored
erc20_contracts.total_supply value—either query a trusted on-chain source
instead or return no total_supply until has_complete_erc20_supply_history
returns true; apply the same change to the other occurrence around lines 137-145
where total_supply is currently used.

Comment on lines +122 to +127
let erc20_supply_history_status = self.get_erc20_supply_history_status().await?;
let mut erc20_supply_backfill_pending = matches!(erc20_supply_history_status, Some(false))
|| (erc20_supply_history_status.is_none() && start_block == 0);
if erc20_supply_history_status.is_none() && start_block == 0 {
self.set_erc20_supply_history_complete(false).await?;
}
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 | 🔴 Critical

Don’t mark supply history complete while gaps can still exist.

This state machine never checks for outstanding failed blocks. If a block exhausts retries and is parked in failed_blocks, current_block > head can still flip ERC20_SUPPLY_HISTORY_COMPLETE_KEY to true, and once the key is true later gaps will not turn it back off. Since the API uses this flag to trust indexed supply, that can expose incorrect totals after any permanent hole in the indexed history. Tie the flag to a gap-free condition, and reset it when a block is deferred into failed_blocks.

Also applies to: 207-210, 407-410

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

In `@backend/crates/atlas-server/src/indexer/indexer.rs` around lines 122 - 127,
The logic that sets ERC20 supply-history completion (via
get_erc20_supply_history_status and set_erc20_supply_history_complete) must only
mark complete when there are no outstanding gaps or permanently failed blocks;
update the code so it checks the failed_blocks set (or parked failed count) and
any deferred/failed retry state before setting the flag, and ensure any code
paths that move a block into failed_blocks immediately call
set_erc20_supply_history_complete(false) to reset the flag; apply this change to
the same pattern found around the other similar blocks (the other
get_/set_erc20_supply_history_status usages) so the flag represents a gap-free
condition and is cleared when a block is deferred to failed_blocks.

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.

1 participant