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.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 31, 2026

Warning

Rate limit exceeded

@pthmas has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 12 minutes and 54 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 12 minutes and 54 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9ea8839d-b94b-4d50-ac52-4089c1f47e08

📥 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
📝 Walkthrough

Walkthrough

This PR changes ERC-20 total supply calculation from stored metadata to dynamically computed values from indexed token balances. It removes totalSupply() metadata fetching, updates API handlers to prefer indexed aggregations when transfers exist, tracks supply deltas during indexing, and includes database migration and integration tests.

Changes

Cohort / File(s) Summary
API Handler Logic
backend/crates/atlas-server/src/api/handlers/addresses.rs, backend/crates/atlas-server/src/api/handlers/tokens.rs
Added helper functions to compute total supply from indexed erc20_balances. Both handlers now conditionally recalculate total supply from indexed aggregation when transfer count > 0 or stored value is missing, preferring indexed data over stored column.
Indexer Core
backend/crates/atlas-server/src/indexer/batch.rs, backend/crates/atlas-server/src/indexer/indexer.rs
Extended BlockBatch with supply_map to accumulate per-contract supply deltas. Updated transfer processing to record deltas when minting/burning (from/to ZERO_ADDRESS) and persist aggregated deltas via batch UPDATE to erc20_contracts.total_supply.
Metadata Fetching
backend/crates/atlas-server/src/indexer/metadata.rs
Removed totalSupply() interface method and all associated fetch/parse/persist logic for ERC-20 contract metadata, leaving only name, symbol, and decimals.
Database Migration
backend/migrations/20240109000001_recompute_erc20_supply.sql
Migration to recalculate erc20_contracts.total_supply by aggregating positive balances from indexed erc20_balances table for all contracts.
Integration Tests
backend/crates/atlas-server/tests/integration/addresses.rs, backend/crates/atlas-server/tests/integration/tokens.rs
Added seeding helpers and tests verifying API endpoints prefer indexed total supply over stored values, including test with stale stored value to confirm indexed aggregation takes precedence.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant APIHandler
    participant Database as DB (erc20_contracts)
    participant IndexedDB as DB (erc20_balances)

    rect rgba(255, 200, 100, 0.5)
    Note over APIHandler,IndexedDB: New Flow: Total Supply Calculation
    Client->>APIHandler: GET /api/addresses/{erc20_addr}
    APIHandler->>Database: Query contract & check for transfers
    Database-->>APIHandler: Contract row + transfer_count
    
    alt transfer_count > 0 OR total_supply IS NULL
        APIHandler->>IndexedDB: SELECT SUM(balance) WHERE balance > 0
        IndexedDB-->>APIHandler: Computed total_supply
    else
        APIHandler->>Database: Use stored total_supply
        Database-->>APIHandler: Stored value
    end
    
    APIHandler-->>Client: Response with total_supply
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • test: add API integration tests #34: Made HeadTracker and lib module exports public, enabling integration tests in this PR to construct AppState with proper public dependencies.

Suggested reviewers

  • tac0turtle

Poem

🐰 Hop! No more stale supply counts,
Indexed balances are what amounts!
From delta-tracking indexer's care,
To handlers that compute with flair,
Total supply, refreshed through the air!

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% 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
Title check ✅ Passed The title directly describes the main change: fixing stale ERC20 total supply by shifting from one-time metadata fetches to indexed balance-derived values.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
🧪 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

Comment on lines +4 to +15
UPDATE erc20_contracts AS c
SET total_supply = COALESCE(b.total_supply, 0)
FROM (
SELECT
erc20_contracts.address,
COALESCE(SUM(balance), 0) AS total_supply
FROM erc20_contracts
LEFT JOIN erc20_balances ON erc20_balances.contract_address = erc20_contracts.address
AND erc20_balances.balance > 0
GROUP BY erc20_contracts.address
) AS b
WHERE c.address = b.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

This backfill can overwrite good supply with partial balance state.

erc20_balances is only authoritative after a full historical backfill. If indexing begins after a token already exists, this update can replace a previously correct snapshot with 0 or just the post-start delta sum, and the metadata fetcher no longer repopulates that baseline. Please preserve the existing value until the contract's indexed history is known-complete.

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

In `@backend/migrations/20240109000001_recompute_erc20_supply.sql` around lines 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.

@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.

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