forest/state_manager/
circulating_supply.rs

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