diff --git a/Cargo.lock b/Cargo.lock index 2ccdfad7f..368f47936 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2811,6 +2811,7 @@ dependencies = [ "fil_actor_eam", "fil_actors_evm_shared", "fil_actors_runtime", + "frc42_dispatch", "fvm_ipld_blockstore", "fvm_ipld_encoding", "fvm_shared", diff --git a/fendermint/actors/eam/Cargo.toml b/fendermint/actors/eam/Cargo.toml index 0dba94b4c..859a87530 100644 --- a/fendermint/actors/eam/Cargo.toml +++ b/fendermint/actors/eam/Cargo.toml @@ -25,6 +25,7 @@ num-derive = { workspace = true } num-traits = { workspace = true } serde = { workspace = true } hex-literal = { workspace = true } +frc42_dispatch = { workspace = true } [dev-dependencies] fil_actors_evm_shared = { workspace = true } diff --git a/fendermint/actors/eam/src/lib.rs b/fendermint/actors/eam/src/lib.rs index 9ebbb1c78..82456dc31 100644 --- a/fendermint/actors/eam/src/lib.rs +++ b/fendermint/actors/eam/src/lib.rs @@ -9,8 +9,11 @@ use fil_actors_runtime::EAM_ACTOR_ID; use fvm_ipld_blockstore::Blockstore; use fvm_ipld_encoding::ipld_block::IpldBlock; use fvm_ipld_encoding::tuple::*; +use fvm_shared::address::Address; use fvm_shared::{ActorID, MethodNum}; +use num_derive::FromPrimitive; +use crate::state::PermissionMode; pub use crate::state::PermissionModeParams; pub use crate::state::State; @@ -24,6 +27,12 @@ pub const IPC_EAM_ACTOR_ID: ActorID = EAM_ACTOR_ID; pub struct IPCEamActor; +#[derive(FromPrimitive)] +#[repr(u64)] +pub enum ExtraMethods { + UpdateDeployers = frc42_dispatch::method_hash!("UpdateDeployers"), +} + impl IPCEamActor { /// Creates the actor. If the `whitelisted_deployers` is empty, that means there is no restriction /// for deployment, i.e any address can deploy. @@ -59,6 +68,33 @@ impl IPCEamActor { Ok(()) } + + fn update_deployers(rt: &impl Runtime, deployers: Vec
) -> Result<(), ActorError> { + // Reject update if we're unrestricted. + let state: State = rt.state()?; + if !matches!(state.permission_mode, PermissionMode::AllowList(_)) { + return Err(ActorError::forbidden(String::from( + "deployers can only be updated in allowlist mode", + ))); + }; + + // Check that the caller is in the allowlist. + let caller_id = rt.message().caller().id().unwrap(); + if !state.can_deploy(rt, caller_id)? { + return Err(ActorError::forbidden(String::from( + "sender not allowed to update deployers", + ))); + } + + // Perform the update. + rt.transaction(|st: &mut State, rt| { + st.permission_mode = + State::new(rt.store(), PermissionModeParams::AllowList(deployers))?.permission_mode; + Ok(()) + })?; + + Ok(()) + } } impl ActorCode for IPCEamActor { @@ -79,6 +115,8 @@ impl ActorCode for IPCEamActor { { if method == Method::Constructor as u64 { fil_actors_runtime::dispatch(rt, method, Self::constructor, params) + } else if method == ExtraMethods::UpdateDeployers as u64 { + fil_actors_runtime::dispatch(rt, method, Self::update_deployers, params) } else { Self::ensure_deployer_allowed(rt)?; EamActor::invoke_method(rt, method, params) @@ -109,9 +147,10 @@ mod tests { use fvm_shared::address::Address; use fvm_shared::econ::TokenAmount; use fvm_shared::error::ExitCode; + use fvm_shared::MethodNum; use crate::state::PermissionModeParams; - use crate::{ConstructorParams as IPCConstructorParams, IPCEamActor, Method}; + use crate::{ConstructorParams as IPCConstructorParams, ExtraMethods, IPCEamActor, Method}; pub fn construct_and_verify(deployers: Vec
) -> MockRuntime { let rt = MockRuntime { @@ -372,4 +411,111 @@ mod tests { assert_eq!(result, expected_return); rt.verify(); } + + #[test] + fn test_update_deployers() { + let deployers = vec![Address::new_id(1000)]; + let rt = construct_and_verify(deployers); + + struct AddrTriple { + eth: EthAddress, + f410: Address, + id: Address, + } + + macro_rules! create_address { + ($hex_addr:expr, $id:expr) => {{ + let eth = EthAddress(hex_literal::hex!($hex_addr)); + let f410 = Address::new_delegated(10, ð.0).unwrap(); + rt.set_delegated_address($id, f410); + AddrTriple { + eth, + f410, + id: Address::new_id($id), + } + }}; + } + + let allowed = create_address!("CAFEB0BA00000000000000000000000000000000", 1000); + let deployer = create_address!("FAAAB0BA00000000000000000000000000000000", 2000); + + let initcode = vec![0xff]; + + let create_params = CreateExternalParams(initcode.clone()); + + // Deployer is not allowed to create yet. + rt.set_caller(*ETHACCOUNT_ACTOR_CODE_ID, deployer.id); + let ret = rt.call::( + Method::CreateExternal as u64, + IpldBlock::serialize_cbor(&create_params).unwrap(), + ); + assert_eq!(ExitCode::USR_FORBIDDEN, ret.err().unwrap().exit_code()); + + // Now add permissions for the deployer from the allowed address. + let update_deployers_params = vec![deployer.f410]; + rt.set_caller(*ETHACCOUNT_ACTOR_CODE_ID, allowed.id); + let ret = rt.call::( + ExtraMethods::UpdateDeployers as MethodNum, + IpldBlock::serialize_cbor(&update_deployers_params).unwrap(), + ); + assert!(ret.is_ok()); + + // Previously allowed deployer no longer allowed. + rt.set_caller(*ETHACCOUNT_ACTOR_CODE_ID, allowed.id); + let ret = rt.call::( + Method::CreateExternal as u64, + IpldBlock::serialize_cbor(&create_params).unwrap(), + ); + assert_eq!(ExitCode::USR_FORBIDDEN, ret.err().unwrap().exit_code()); + + // New deployer is permissioned. + rt.set_caller(*ETHACCOUNT_ACTOR_CODE_ID, deployer.id); + rt.set_origin(deployer.id); + rt.expect_validate_caller_addr(vec![deployer.id]); + let send_return = IpldBlock::serialize_cbor(&Exec4Return { + id_address: Address::new_id(111), + robust_address: Address::new_id(0), // nobody cares + }) + .unwrap(); + + let new_eth_addr = compute_address_create(&rt, &deployer.eth, 0); + let params = { + let evm_params = ConstructorParams { + creator: deployer.eth, + initcode: initcode.clone().into(), + }; + Exec4Params { + code_cid: *EVM_ACTOR_CODE_ID, + constructor_params: RawBytes::serialize(evm_params).unwrap(), + subaddress: new_eth_addr.0[..].to_owned().into(), + } + }; + + rt.expect_send_simple( + INIT_ACTOR_ADDR, + EXEC4_METHOD, + IpldBlock::serialize_cbor(¶ms).unwrap(), + TokenAmount::from_atto(0), + send_return, + ExitCode::OK, + ); + + let ret = rt + .call::( + Method::CreateExternal as u64, + IpldBlock::serialize_cbor(&create_params).unwrap(), + ) + .unwrap() + .unwrap() + .deserialize::() + .unwrap(); + + let expected_return = Return { + actor_id: 111, + robust_address: Some(Address::new_id(0)), + eth_address: new_eth_addr, + }; + + assert_eq!(ret, expected_return); + } } diff --git a/fendermint/actors/eam/src/state.rs b/fendermint/actors/eam/src/state.rs index dd1e90b3f..9d278a208 100644 --- a/fendermint/actors/eam/src/state.rs +++ b/fendermint/actors/eam/src/state.rs @@ -23,7 +23,7 @@ pub enum PermissionModeParams { /// The permission mode for controlling who can deploy contracts #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] -enum PermissionMode { +pub enum PermissionMode { /// No restriction, everyone can deploy Unrestricted, /// Only whitelisted addresses can deploy @@ -32,7 +32,7 @@ enum PermissionMode { #[derive(Serialize_tuple, Deserialize_tuple, Debug, Clone)] pub struct State { - permission_mode: PermissionMode, + pub permission_mode: PermissionMode, } impl State {