use crate::networks::NetworkChain;
use crate::rpc::eth::types::EthAddress;
use crate::shim::address::Address;
use crate::shim::clock::{ChainEpoch, EPOCHS_IN_DAY, EPOCHS_IN_HOUR};
use crate::shim::state_tree::StateTree;
use anyhow::{Context as _, ensure};
use fil_actor_reward_state::v19::DENOM;
use fvm_ipld_blockstore::Blockstore;
use std::str::FromStr as _;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SolsticeRewardBootstrapParams {
pub swa_timelock_epochs: ChainEpoch,
pub consensus_weight_ramp_duration_epochs: ChainEpoch,
pub consensus_weight: SolsticeRewardWeightParams,
pub service_weight: SolsticeRewardWeightParams,
pub swa_actor: Option<Address>,
pub sra_actor: Option<Address>,
pub initial_orchestrator: Option<Address>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SolsticeRewardWeightParams {
pub v_start: u64,
pub floor: u64,
pub cap: u64,
}
pub(super) const PERCENT: u64 = DENOM / 100;
const RAMP_QUARTERS: ChainEpoch = 9;
const MAINNET_EPOCHS_PER_QUARTER: ChainEpoch = 262_974;
const FIP0118: SolsticeRewardBootstrapParams = SolsticeRewardBootstrapParams {
swa_timelock_epochs: 0,
consensus_weight_ramp_duration_epochs: 0,
consensus_weight: SolsticeRewardWeightParams {
v_start: 95 * PERCENT,
floor: 50 * PERCENT,
cap: 95 * PERCENT,
},
service_weight: SolsticeRewardWeightParams {
v_start: 5 * PERCENT,
floor: 5 * PERCENT,
cap: 10 * PERCENT,
},
swa_actor: None,
sra_actor: None,
initial_orchestrator: None,
};
impl SolsticeRewardBootstrapParams {
pub fn for_chain(chain: &NetworkChain) -> Self {
match chain {
NetworkChain::Mainnet => Self {
swa_timelock_epochs: 7 * EPOCHS_IN_DAY,
consensus_weight_ramp_duration_epochs: RAMP_QUARTERS * MAINNET_EPOCHS_PER_QUARTER,
swa_actor: Some(evm_address("0x66C11A9F6dfEC3c1557958cF9f575a023EB01421")),
sra_actor: Some(evm_address("0x0339f205314C8210AF7Cb075d1A96D012e7896a9")),
initial_orchestrator: Some(evm_address(
"0x97A90f5696be5E3C8d3752C92Adac287c2b4484e",
)),
..FIP0118
},
NetworkChain::Calibnet => Self {
swa_timelock_epochs: EPOCHS_IN_HOUR * 6,
consensus_weight_ramp_duration_epochs: RAMP_QUARTERS * EPOCHS_IN_DAY,
swa_actor: Some(evm_address("0x66C11A9F6dfEC3c1557958cF9f575a023EB01421")),
sra_actor: Some(evm_address("0x0339f205314C8210AF7Cb075d1A96D012e7896a9")),
initial_orchestrator: Some(evm_address(
"0x97A90f5696be5E3C8d3752C92Adac287c2b4484e",
)),
..FIP0118
},
NetworkChain::Butterflynet => Self {
swa_timelock_epochs: 40,
consensus_weight_ramp_duration_epochs: RAMP_QUARTERS * 2 * EPOCHS_IN_HOUR,
swa_actor: Some(evm_address("0x17c43bC9d8E8600ebE7599C18f2dA2D5CED68D95")),
sra_actor: Some(evm_address("0xea340224F4df7D01d2657964215E37452165b0A1")),
initial_orchestrator: Some(evm_address(
"0x48C7DC38e74C9fA9eA6484Ad6Ad0520349dC9B40",
)),
..FIP0118
},
NetworkChain::Devnet(_) => Self {
swa_timelock_epochs: 50,
consensus_weight_ramp_duration_epochs: 900,
swa_actor: Some(Address::SYSTEM_ACTOR),
sra_actor: Some(Address::SYSTEM_ACTOR),
initial_orchestrator: Some(Address::SYSTEM_ACTOR),
..FIP0118
},
}
}
pub fn resolve<BS: Blockstore>(mut self, actors: &StateTree<BS>) -> anyhow::Result<Self> {
ensure!(
self.swa_actor.is_some(),
"Solstice bootstrap SWA actor is unset"
);
ensure!(
self.initial_orchestrator != Some(Address::BURNT_FUNDS_ACTOR),
"Solstice bootstrap initial orchestrator is the burn actor"
);
for (name, address) in [
("SWA actor", &mut self.swa_actor),
("SRA actor", &mut self.sra_actor),
("initial orchestrator", &mut self.initial_orchestrator),
] {
let Some(unresolved) = address.take() else {
continue;
};
let id = actors.lookup_id(&unresolved)?.with_context(|| {
format!("Solstice bootstrap {name} {unresolved} is not on chain")
})?;
*address = Some(Address::new_id(id));
}
Ok(self)
}
}
fn evm_address(hex: &str) -> Address {
EthAddress::from_str(hex)
.expect("hard-coded EVM address is well-formed")
.to_filecoin_address()
.expect("EVM address has a Filecoin form")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::db::MemoryDB;
use crate::shim::state_tree::{ActorState, StateTreeVersion};
use crate::utils::cid::CidCborExt as _;
use crate::utils::db::CborStoreExt as _;
use cid::Cid;
use rstest::rstest;
use std::sync::Arc;
fn state_tree_with(addresses: &[Address]) -> (StateTree<Arc<MemoryDB>>, Vec<Address>) {
let store = Arc::new(MemoryDB::default());
let mut init_state =
fil_actor_init_state::v19::State::new(&store, "migrationtest".into()).unwrap();
let ids = addresses
.iter()
.map(|address| {
let (id, _) = init_state
.map_addresses_to_id(&store, &address.into(), None)
.unwrap();
Address::new_id(id)
})
.collect();
let init_head = store.put_cbor_default(&init_state).unwrap();
let init_actor = ActorState::new(
Cid::from_cbor_blake2b256(&"init code").unwrap(),
init_head,
Default::default(),
0,
None,
);
let mut actors = StateTree::new(&store, StateTreeVersion::V5).unwrap();
actors.set_actor(&Address::INIT_ACTOR, init_actor).unwrap();
(actors, ids)
}
fn params_with(
swa: Option<Address>,
sra: Option<Address>,
orchestrator: Option<Address>,
) -> SolsticeRewardBootstrapParams {
SolsticeRewardBootstrapParams {
swa_actor: swa,
sra_actor: sra,
initial_orchestrator: orchestrator,
..FIP0118
}
}
fn contract(seed: u8) -> Address {
Address::new_delegated(
Address::ETHEREUM_ACCOUNT_MANAGER_ACTOR.id().unwrap(),
&[seed; 20],
)
.unwrap()
}
fn wallet(seed: u8) -> Address {
Address::new_secp256k1(&[seed; 65]).unwrap()
}
#[test]
fn resolves_contract_and_wallet_addresses_to_ids() {
let (swa, sra, orchestrator) = (contract(1), contract(2), wallet(3));
let (actors, ids) = state_tree_with(&[swa, sra, orchestrator]);
let resolved = params_with(Some(swa), Some(sra), Some(orchestrator))
.resolve(&actors)
.unwrap();
assert_eq!(
(
resolved.swa_actor,
resolved.sra_actor,
resolved.initial_orchestrator
),
(Some(ids[0]), Some(ids[1]), Some(ids[2]))
);
}
#[test]
fn unset_service_stream_addresses_pass_through() {
let swa = contract(1);
let (actors, ids) = state_tree_with(&[swa]);
let resolved = params_with(Some(swa), None, None).resolve(&actors).unwrap();
assert_eq!(resolved.swa_actor, Some(ids[0]));
assert_eq!(resolved.sra_actor, None);
assert_eq!(resolved.initial_orchestrator, None);
}
#[test]
fn rejects_an_unset_swa() {
let (actors, _) = state_tree_with(&[]);
let error = params_with(None, Some(contract(2)), Some(wallet(3)))
.resolve(&actors)
.unwrap_err();
assert!(
error.to_string().contains("SWA actor is unset"),
"{error:#}"
);
}
#[test]
fn rejects_the_burn_actor_as_orchestrator() {
let (swa, sra) = (contract(1), contract(2));
let (actors, _) = state_tree_with(&[swa, sra]);
let error = params_with(Some(swa), Some(sra), Some(Address::BURNT_FUNDS_ACTOR))
.resolve(&actors)
.unwrap_err();
assert!(
error
.to_string()
.contains("initial orchestrator is the burn actor"),
"{error:#}"
);
}
#[rstest]
#[case::swa_actor("SWA actor", contract(1), [contract(2), wallet(3)])]
#[case::sra_actor("SRA actor", contract(2), [contract(1), wallet(3)])]
#[case::initial_orchestrator("initial orchestrator", wallet(3), [contract(1), contract(2)])]
fn rejects_an_address_missing_from_the_state_tree(
#[case] name: &str,
#[case] missing: Address,
#[case] on_chain: [Address; 2],
) {
let (swa, sra, orchestrator) = (contract(1), contract(2), wallet(3));
let (actors, _) = state_tree_with(&on_chain);
let error = params_with(Some(swa), Some(sra), Some(orchestrator))
.resolve(&actors)
.unwrap_err();
let expected = format!("Solstice bootstrap {name} {missing} is not on chain");
assert!(error.to_string().contains(&expected), "{error:#}");
}
}