Skip to main content

forest/state_manager/
circulating_supply.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4use std::sync::LazyLock;
5
6use crate::blocks::{Tipset, TipsetKey};
7use crate::chain::*;
8use crate::networks::{ChainConfig, Height};
9use crate::prelude::*;
10use crate::rpc::types::CirculatingSupply;
11use crate::shim::actors::{
12    MarketActorStateLoad as _, MinerActorStateLoad as _, MultisigActorStateLoad as _,
13    PowerActorStateLoad as _, is_account_actor, is_ethaccount_actor, is_evm_actor, is_miner_actor,
14    is_multisig_actor, is_paymentchannel_actor, is_placeholder_actor,
15};
16use crate::shim::actors::{market, miner, multisig, power, reward};
17use crate::shim::version::NetworkVersion;
18use crate::shim::{
19    address::Address,
20    clock::{ChainEpoch, EPOCHS_IN_DAY},
21    econ::{TOTAL_FILECOIN, TokenAmount},
22    state_tree::{ActorState, StateTree},
23};
24use anyhow::bail;
25use num_traits::Zero;
26
27const EPOCHS_IN_YEAR: ChainEpoch = 365 * EPOCHS_IN_DAY;
28const PRE_CALICO_VESTING: [(ChainEpoch, usize); 5] = [
29    (183 * EPOCHS_IN_DAY, 82_717_041),
30    (EPOCHS_IN_YEAR, 22_421_712),
31    (2 * EPOCHS_IN_YEAR, 7_223_364),
32    (3 * EPOCHS_IN_YEAR, 87_637_883),
33    (6 * EPOCHS_IN_YEAR, 400_000_000),
34];
35const CALICO_VESTING: [(ChainEpoch, usize); 6] = [
36    (0, 10_632_000),
37    (183 * EPOCHS_IN_DAY, 19_015_887 + 32_787_700),
38    (EPOCHS_IN_YEAR, 22_421_712 + 9_400_000),
39    (2 * EPOCHS_IN_YEAR, 7_223_364),
40    (3 * EPOCHS_IN_YEAR, 87_637_883 + 898_958),
41    (6 * EPOCHS_IN_YEAR, 100_000_000 + 300_000_000 + 9_805_053),
42];
43
44/// Genesis information used when calculating circulating supply.
45#[derive(Default, Clone)]
46pub struct GenesisInfo {
47    vesting: GenesisInfoVesting,
48
49    /// info about the Accounts in the genesis state
50    genesis_pledge: TokenAmount,
51    genesis_market_funds: TokenAmount,
52
53    chain_config: Arc<ChainConfig>,
54}
55
56impl GenesisInfo {
57    pub fn from_chain_config(chain_config: Arc<ChainConfig>) -> Self {
58        let liftoff_height = chain_config.epoch(Height::Liftoff);
59        Self {
60            vesting: GenesisInfoVesting::new(liftoff_height),
61            chain_config,
62            ..GenesisInfo::default()
63        }
64    }
65
66    /// Calculate total FIL circulating supply based on Genesis configuration and state of particular
67    /// actors at a given height and state root.
68    ///
69    /// IMPORTANT: Easy to mistake for [`GenesisInfo::get_state_circulating_supply`], that's being
70    /// calculated differently.
71    pub fn get_vm_circulating_supply<DB: Blockstore + ShallowClone>(
72        &self,
73        height: ChainEpoch,
74        db: &DB,
75        root: &Cid,
76    ) -> anyhow::Result<TokenAmount> {
77        let detailed = self.get_vm_circulating_supply_detailed(height, db, root)?;
78        Ok(detailed.fil_circulating)
79    }
80
81    /// Calculate total FIL circulating supply based on Genesis configuration and state of particular
82    /// actors at a given height and state root.
83    pub fn get_vm_circulating_supply_detailed<DB: Blockstore + ShallowClone>(
84        &self,
85        height: ChainEpoch,
86        db: &DB,
87        root: &Cid,
88    ) -> anyhow::Result<CirculatingSupply> {
89        let state_tree = StateTree::new_from_root(db, root)?;
90
91        let fil_vested = get_fil_vested(self, height);
92        let fil_mined = get_fil_mined(&state_tree)?;
93        let fil_burnt = get_fil_burnt(&state_tree)?;
94
95        let network_version = self.chain_config.network_version(height);
96        let fil_locked = get_fil_locked(&state_tree, network_version)?;
97        let fil_reserve_disbursed = if height > self.chain_config.epoch(Height::Assembly) {
98            get_fil_reserve_disbursed(&self.chain_config, height, &state_tree)?
99        } else {
100            TokenAmount::default()
101        };
102        let fil_circulating = TokenAmount::max(
103            &fil_vested + &fil_mined + &fil_reserve_disbursed - &fil_burnt - &fil_locked,
104            TokenAmount::default(),
105        );
106        Ok(CirculatingSupply {
107            fil_vested,
108            fil_mined,
109            fil_burnt,
110            fil_locked,
111            fil_circulating,
112            fil_reserve_disbursed,
113        })
114    }
115
116    /// Calculate total FIL circulating supply based on state, traversing the state tree and
117    /// checking Actor types. This can be a lengthy operation so cache it's used.
118    ///
119    /// IMPORTANT: Easy to mistake for [`GenesisInfo::get_vm_circulating_supply`], that's being
120    /// calculated differently.
121    pub async fn get_state_circulating_supply_with_cache(
122        self,
123        db: impl Blockstore + ShallowClone + Send + Sync + 'static,
124        ts: Tipset,
125    ) -> anyhow::Result<TokenAmount> {
126        static CACHE: LazyLock<quick_cache::sync::Cache<TipsetKey, Result<TokenAmount, String>>> =
127            LazyLock::new(|| quick_cache::sync::Cache::new(120)); // 120 for 1h-worth epochs
128
129        let height = ts.epoch() - 1;
130        let root = *ts.parent_state();
131        let this = self;
132
133        CACHE
134            .get_or_insert_async(ts.key(), async move {
135                tokio::task::spawn_blocking(move || {
136                    this.get_state_circulating_supply_raw_blocking(height, &db, &root)
137                        .map_err(|e| {
138                            let mut e = e.to_string();
139                            e.truncate(e.floor_char_boundary(100)); // To make error size bounded
140                            e
141                        })
142                })
143                .await
144                .context("error joining tokio handle")
145            })
146            .await?
147            .map_err(|e| anyhow::anyhow!(e))
148    }
149
150    /// Calculate total FIL circulating supply based on state, traversing the state tree and
151    /// checking Actor types. This can be a lengthy operation.
152    ///
153    /// IMPORTANT: Easy to mistake for [`GenesisInfo::get_vm_circulating_supply`], that's being
154    /// calculated differently.
155    pub fn get_state_circulating_supply_raw_blocking(
156        &self,
157        height: ChainEpoch,
158        db: &(impl Blockstore + ShallowClone),
159        root: &Cid,
160    ) -> anyhow::Result<TokenAmount> {
161        let mut circ = TokenAmount::default();
162        let mut un_circ = TokenAmount::default();
163
164        let state_tree = StateTree::new_from_root(db, root)?;
165
166        state_tree.for_each_cacheless(|addr: Address, actor: &ActorState| {
167            let actor_balance = TokenAmount::from(actor.balance.clone());
168            if !actor_balance.is_zero() {
169                match addr {
170                    Address::INIT_ACTOR
171                    | Address::REWARD_ACTOR
172                    | Address::VERIFIED_REGISTRY_ACTOR
173                    // The power actor itself should never receive funds
174                    | Address::POWER_ACTOR
175                    | Address::SYSTEM_ACTOR
176                    | Address::CRON_ACTOR
177                    | Address::BURNT_FUNDS_ACTOR
178                    | Address::SAFT_ACTOR
179                    | Address::RESERVE_ACTOR
180                    | Address::ETHEREUM_ACCOUNT_MANAGER_ACTOR
181                    | Address::DATACAP_TOKEN_ACTOR => {
182                        un_circ += actor_balance;
183                    }
184                    Address::MARKET_ACTOR => {
185                        let network_version = self.chain_config.network_version(height);
186                        if network_version >= NetworkVersion::V23 {
187                            circ += actor_balance;
188                        } else {
189                            let ms = market::State::load(db, actor.code, actor.state)?;
190                            let locked_balance = ms.total_locked();
191                            circ += actor_balance - &locked_balance;
192                            un_circ += locked_balance;
193                        }
194                    }
195                    _ if is_account_actor(&actor.code)
196                    || is_paymentchannel_actor(&actor.code)
197                    || is_ethaccount_actor(&actor.code)
198                    || is_evm_actor(&actor.code)
199                    || is_placeholder_actor(&actor.code) => {
200                        circ += actor_balance;
201                    },
202                    _ if is_miner_actor(&actor.code) => {
203                        let ms = miner::State::load(&db, actor.code, actor.state)?;
204
205                        if let Ok(avail_balance) = ms.available_balance(actor.balance.atto()) {
206                            circ += avail_balance.clone();
207                            un_circ += actor_balance.clone() - &avail_balance;
208                        } else {
209                            // Assume any error is because the miner state is "broken" (lower actor balance than locked funds)
210                            // In this case, the actor's entire balance is considered "uncirculating"
211                            un_circ += actor_balance;
212                        }
213                    }
214                    _ if is_multisig_actor(&actor.code) => {
215                        let ms = multisig::State::load(&db, actor.code, actor.state)?;
216
217                        let locked_balance = ms.locked_balance(height)?;
218                        let avail_balance = actor_balance.clone() - &locked_balance;
219                        circ += avail_balance.max(TokenAmount::zero());
220                        un_circ += actor_balance.min(locked_balance);
221                    }
222                    _ => bail!("unexpected actor at epoch {height}: {actor:?}"),
223                }
224            } else {
225                // Do nothing for zero-balance actors
226            }
227            Ok(())
228        })?;
229
230        let total = circ.clone() + un_circ;
231        if total != *TOTAL_FILECOIN {
232            bail!(
233                "total filecoin didn't add to expected amount: {} != {}",
234                total,
235                *TOTAL_FILECOIN
236            );
237        }
238
239        Ok(circ)
240    }
241}
242
243/// Vesting schedule info. These states are lazily filled, to avoid doing until
244/// needed to calculate circulating supply.
245#[derive(Default, Clone)]
246struct GenesisInfoVesting {
247    genesis: Vec<(ChainEpoch, TokenAmount)>,
248    ignition: Vec<(ChainEpoch, ChainEpoch, TokenAmount)>,
249    calico: Vec<(ChainEpoch, ChainEpoch, TokenAmount)>,
250}
251
252impl GenesisInfoVesting {
253    fn new(liftoff_height: ChainEpoch) -> Self {
254        Self {
255            genesis: setup_genesis_vesting_schedule(),
256            ignition: setup_ignition_vesting_schedule(liftoff_height),
257            calico: setup_calico_vesting_schedule(liftoff_height),
258        }
259    }
260}
261
262fn get_actor_state<DB: Blockstore>(
263    state_tree: &StateTree<DB>,
264    addr: &Address,
265) -> anyhow::Result<ActorState> {
266    state_tree
267        .get_actor(addr)?
268        .with_context(|| format!("Failed to get Actor for address {addr}"))
269}
270
271fn get_fil_vested(genesis_info: &GenesisInfo, height: ChainEpoch) -> TokenAmount {
272    let mut return_value = TokenAmount::default();
273
274    let pre_ignition = &genesis_info.vesting.genesis;
275    let post_ignition = &genesis_info.vesting.ignition;
276    let calico_vesting = &genesis_info.vesting.calico;
277
278    if height <= genesis_info.chain_config.epoch(Height::Ignition) {
279        for (unlock_duration, initial_balance) in pre_ignition {
280            return_value +=
281                initial_balance - v0_amount_locked(*unlock_duration, initial_balance, height);
282        }
283    } else if height <= genesis_info.chain_config.epoch(Height::Calico) {
284        for (start_epoch, unlock_duration, initial_balance) in post_ignition {
285            return_value += initial_balance
286                - v0_amount_locked(*unlock_duration, initial_balance, height - start_epoch);
287        }
288    } else {
289        for (start_epoch, unlock_duration, initial_balance) in calico_vesting {
290            return_value += initial_balance
291                - v0_amount_locked(*unlock_duration, initial_balance, height - start_epoch);
292        }
293    }
294
295    if height <= genesis_info.chain_config.epoch(Height::Assembly) {
296        return_value += &genesis_info.genesis_pledge + &genesis_info.genesis_market_funds;
297    }
298
299    return_value
300}
301
302fn get_fil_mined<DB: Blockstore>(state_tree: &StateTree<DB>) -> anyhow::Result<TokenAmount> {
303    let state: reward::State = state_tree.get_actor_state()?;
304    Ok(state.into_total_storage_power_reward())
305}
306
307fn get_fil_market_locked<DB: Blockstore>(
308    state_tree: &StateTree<DB>,
309) -> anyhow::Result<TokenAmount> {
310    let actor = state_tree
311        .get_actor(&Address::MARKET_ACTOR)?
312        .ok_or_else(|| Error::state("Market actor address could not be resolved"))?;
313    let state = market::State::load(state_tree.store(), actor.code, actor.state)?;
314
315    Ok(state.total_locked())
316}
317
318fn get_fil_power_locked<DB: Blockstore>(state_tree: &StateTree<DB>) -> anyhow::Result<TokenAmount> {
319    let actor = state_tree
320        .get_actor(&Address::POWER_ACTOR)?
321        .ok_or_else(|| Error::state("Power actor address could not be resolved"))?;
322    let state = power::State::load(state_tree.store(), actor.code, actor.state)?;
323    Ok(state.into_total_locked())
324}
325
326fn get_fil_reserve_disbursed<DB: Blockstore>(
327    chain_config: &ChainConfig,
328    height: ChainEpoch,
329    state_tree: &StateTree<DB>,
330) -> anyhow::Result<TokenAmount> {
331    // FIP-0100 introduced a different hard-coded reserved amount for testnets.
332    // See <https://github.com/filecoin-project/FIPs/blob/master/FIPS/fip-0100.md#special-handling-for-calibration-network>
333    // for details.
334    let fil_reserved = chain_config.initial_fil_reserved_at_height(height);
335    let reserve_actor = get_actor_state(state_tree, &Address::RESERVE_ACTOR)?;
336
337    // If money enters the reserve actor, this could lead to a negative term
338    Ok(fil_reserved - TokenAmount::from(&reserve_actor.balance))
339}
340
341fn get_fil_locked<DB: Blockstore>(
342    state_tree: &StateTree<DB>,
343    network_version: NetworkVersion,
344) -> anyhow::Result<TokenAmount> {
345    let total = if network_version >= NetworkVersion::V23 {
346        get_fil_power_locked(state_tree)?
347    } else {
348        get_fil_market_locked(state_tree)? + get_fil_power_locked(state_tree)?
349    };
350
351    Ok(total)
352}
353
354fn get_fil_burnt<DB: Blockstore>(state_tree: &StateTree<DB>) -> anyhow::Result<TokenAmount> {
355    let burnt_actor = get_actor_state(state_tree, &Address::BURNT_FUNDS_ACTOR)?;
356
357    Ok(TokenAmount::from(&burnt_actor.balance))
358}
359
360fn setup_genesis_vesting_schedule() -> Vec<(ChainEpoch, TokenAmount)> {
361    PRE_CALICO_VESTING
362        .into_iter()
363        .map(|(unlock_duration, initial_balance)| {
364            (unlock_duration, TokenAmount::from_atto(initial_balance))
365        })
366        .collect()
367}
368
369fn setup_ignition_vesting_schedule(
370    liftoff_height: ChainEpoch,
371) -> Vec<(ChainEpoch, ChainEpoch, TokenAmount)> {
372    PRE_CALICO_VESTING
373        .into_iter()
374        .map(|(unlock_duration, initial_balance)| {
375            (
376                liftoff_height,
377                unlock_duration,
378                TokenAmount::from_whole(initial_balance),
379            )
380        })
381        .collect()
382}
383fn setup_calico_vesting_schedule(
384    liftoff_height: ChainEpoch,
385) -> Vec<(ChainEpoch, ChainEpoch, TokenAmount)> {
386    CALICO_VESTING
387        .into_iter()
388        .map(|(unlock_duration, initial_balance)| {
389            (
390                liftoff_height,
391                unlock_duration,
392                TokenAmount::from_whole(initial_balance),
393            )
394        })
395        .collect()
396}
397
398// This exact code (bugs and all) has to be used. The results are locked into
399// the blockchain.
400/// Returns amount locked in multisig contract
401fn v0_amount_locked(
402    unlock_duration: ChainEpoch,
403    initial_balance: &TokenAmount,
404    elapsed_epoch: ChainEpoch,
405) -> TokenAmount {
406    if elapsed_epoch >= unlock_duration {
407        return TokenAmount::zero();
408    }
409    if elapsed_epoch < 0 {
410        return initial_balance.clone();
411    }
412    // Division truncation is broken here: https://github.com/filecoin-project/specs-actors/issues/1131
413    let unit_locked: TokenAmount = initial_balance.div_floor(unlock_duration);
414    unit_locked * (unlock_duration - elapsed_epoch)
415}