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::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    ) -> anyhow::Result<TokenAmount> {
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    ) -> anyhow::Result<TokenAmount> {
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 = ms.total_locked();
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                            circ += avail_balance.clone();
173                            un_circ += actor_balance.clone() - &avail_balance;
174                        } else {
175                            // Assume any error is because the miner state is "broken" (lower actor balance than locked funds)
176                            // In this case, the actor's entire balance is considered "uncirculating"
177                            un_circ += actor_balance;
178                        }
179                    }
180                    _ if is_multisig_actor(&actor.code) => {
181                        let ms = multisig::State::load(&db, actor.code, actor.state)?;
182
183                        let locked_balance = ms.locked_balance(height)?;
184                        let avail_balance = actor_balance.clone() - &locked_balance;
185                        circ += avail_balance.max(TokenAmount::zero());
186                        un_circ += actor_balance.min(locked_balance);
187                    }
188                    _ => bail!("unexpected actor: {:?}", actor),
189                }
190            } else {
191                // Do nothing for zero-balance actors
192            }
193            Ok(())
194        })?;
195
196        let total = circ.clone() + un_circ;
197        if total != *TOTAL_FILECOIN {
198            bail!(
199                "total filecoin didn't add to expected amount: {} != {}",
200                total,
201                *TOTAL_FILECOIN
202            );
203        }
204
205        Ok(circ)
206    }
207}
208
209/// Vesting schedule info. These states are lazily filled, to avoid doing until
210/// needed to calculate circulating supply.
211#[derive(Default, Clone)]
212struct GenesisInfoVesting {
213    genesis: Vec<(ChainEpoch, TokenAmount)>,
214    ignition: Vec<(ChainEpoch, ChainEpoch, TokenAmount)>,
215    calico: Vec<(ChainEpoch, ChainEpoch, TokenAmount)>,
216}
217
218impl GenesisInfoVesting {
219    fn new(liftoff_height: i64) -> Self {
220        Self {
221            genesis: setup_genesis_vesting_schedule(),
222            ignition: setup_ignition_vesting_schedule(liftoff_height),
223            calico: setup_calico_vesting_schedule(liftoff_height),
224        }
225    }
226}
227
228fn get_actor_state<DB: Blockstore>(
229    state_tree: &StateTree<DB>,
230    addr: &Address,
231) -> anyhow::Result<ActorState> {
232    state_tree
233        .get_actor(addr)?
234        .with_context(|| format!("Failed to get Actor for address {addr}"))
235}
236
237fn get_fil_vested(genesis_info: &GenesisInfo, height: ChainEpoch) -> TokenAmount {
238    let mut return_value = TokenAmount::default();
239
240    let pre_ignition = &genesis_info.vesting.genesis;
241    let post_ignition = &genesis_info.vesting.ignition;
242    let calico_vesting = &genesis_info.vesting.calico;
243
244    if height <= genesis_info.chain_config.epoch(Height::Ignition) {
245        for (unlock_duration, initial_balance) in pre_ignition {
246            return_value +=
247                initial_balance - v0_amount_locked(*unlock_duration, initial_balance, height);
248        }
249    } else if height <= genesis_info.chain_config.epoch(Height::Calico) {
250        for (start_epoch, unlock_duration, initial_balance) in post_ignition {
251            return_value += initial_balance
252                - v0_amount_locked(*unlock_duration, initial_balance, height - start_epoch);
253        }
254    } else {
255        for (start_epoch, unlock_duration, initial_balance) in calico_vesting {
256            return_value += initial_balance
257                - v0_amount_locked(*unlock_duration, initial_balance, height - start_epoch);
258        }
259    }
260
261    if height <= genesis_info.chain_config.epoch(Height::Assembly) {
262        return_value += &genesis_info.genesis_pledge + &genesis_info.genesis_market_funds;
263    }
264
265    return_value
266}
267
268fn get_fil_mined<DB: Blockstore>(state_tree: &StateTree<DB>) -> anyhow::Result<TokenAmount> {
269    let state: reward::State = state_tree.get_actor_state()?;
270    Ok(state.into_total_storage_power_reward())
271}
272
273fn get_fil_market_locked<DB: Blockstore>(
274    state_tree: &StateTree<DB>,
275) -> anyhow::Result<TokenAmount> {
276    let actor = state_tree
277        .get_actor(&Address::MARKET_ACTOR)?
278        .ok_or_else(|| Error::state("Market actor address could not be resolved"))?;
279    let state = market::State::load(state_tree.store(), actor.code, actor.state)?;
280
281    Ok(state.total_locked())
282}
283
284fn get_fil_power_locked<DB: Blockstore>(state_tree: &StateTree<DB>) -> anyhow::Result<TokenAmount> {
285    let actor = state_tree
286        .get_actor(&Address::POWER_ACTOR)?
287        .ok_or_else(|| Error::state("Power actor address could not be resolved"))?;
288    let state = power::State::load(state_tree.store(), actor.code, actor.state)?;
289    Ok(state.into_total_locked())
290}
291
292fn get_fil_reserve_disbursed<DB: Blockstore>(
293    chain_config: &ChainConfig,
294    height: ChainEpoch,
295    state_tree: &StateTree<DB>,
296) -> anyhow::Result<TokenAmount> {
297    // FIP-0100 introduced a different hard-coded reserved amount for testnets.
298    // See <https://github.com/filecoin-project/FIPs/blob/master/FIPS/fip-0100.md#special-handling-for-calibration-network>
299    // for details.
300    let fil_reserved = chain_config.initial_fil_reserved_at_height(height);
301    let reserve_actor = get_actor_state(state_tree, &Address::RESERVE_ACTOR)?;
302
303    // If money enters the reserve actor, this could lead to a negative term
304    Ok(fil_reserved - TokenAmount::from(&reserve_actor.balance))
305}
306
307fn get_fil_locked<DB: Blockstore>(
308    state_tree: &StateTree<DB>,
309    network_version: NetworkVersion,
310) -> anyhow::Result<TokenAmount> {
311    let total = if network_version >= NetworkVersion::V23 {
312        get_fil_power_locked(state_tree)?
313    } else {
314        get_fil_market_locked(state_tree)? + get_fil_power_locked(state_tree)?
315    };
316
317    Ok(total)
318}
319
320fn get_fil_burnt<DB: Blockstore>(state_tree: &StateTree<DB>) -> anyhow::Result<TokenAmount> {
321    let burnt_actor = get_actor_state(state_tree, &Address::BURNT_FUNDS_ACTOR)?;
322
323    Ok(TokenAmount::from(&burnt_actor.balance))
324}
325
326fn setup_genesis_vesting_schedule() -> Vec<(ChainEpoch, TokenAmount)> {
327    PRE_CALICO_VESTING
328        .into_iter()
329        .map(|(unlock_duration, initial_balance)| {
330            (unlock_duration, TokenAmount::from_atto(initial_balance))
331        })
332        .collect()
333}
334
335fn setup_ignition_vesting_schedule(
336    liftoff_height: ChainEpoch,
337) -> Vec<(ChainEpoch, ChainEpoch, TokenAmount)> {
338    PRE_CALICO_VESTING
339        .into_iter()
340        .map(|(unlock_duration, initial_balance)| {
341            (
342                liftoff_height,
343                unlock_duration,
344                TokenAmount::from_whole(initial_balance),
345            )
346        })
347        .collect()
348}
349fn setup_calico_vesting_schedule(
350    liftoff_height: ChainEpoch,
351) -> Vec<(ChainEpoch, ChainEpoch, TokenAmount)> {
352    CALICO_VESTING
353        .into_iter()
354        .map(|(unlock_duration, initial_balance)| {
355            (
356                liftoff_height,
357                unlock_duration,
358                TokenAmount::from_whole(initial_balance),
359            )
360        })
361        .collect()
362}
363
364// This exact code (bugs and all) has to be used. The results are locked into
365// the blockchain.
366/// Returns amount locked in multisig contract
367fn v0_amount_locked(
368    unlock_duration: ChainEpoch,
369    initial_balance: &TokenAmount,
370    elapsed_epoch: ChainEpoch,
371) -> TokenAmount {
372    if elapsed_epoch >= unlock_duration {
373        return TokenAmount::zero();
374    }
375    if elapsed_epoch < 0 {
376        return initial_balance.clone();
377    }
378    // Division truncation is broken here: https://github.com/filecoin-project/specs-actors/issues/1131
379    let unit_locked: TokenAmount = initial_balance.div_floor(unlock_duration);
380    unit_locked * (unlock_duration - elapsed_epoch)
381}