diff --git a/crates/aggregator/src/committee_finalizer.rs b/crates/aggregator/src/committee_finalizer.rs index a4618f16ee..dcfe621f44 100644 --- a/crates/aggregator/src/committee_finalizer.rs +++ b/crates/aggregator/src/committee_finalizer.rs @@ -6,8 +6,8 @@ use actix::prelude::*; use e3_events::{ - prelude::*, trap, BusHandle, CommitteeFinalizeRequested, CommitteeRequested, EType, - EnclaveEvent, EnclaveEventData, EventType, Shutdown, TypedEvent, + prelude::*, trap, BusHandle, CommitteeFinalizeRequested, CommitteeRequested, E3Failed, E3Stage, + E3StageChanged, EType, EnclaveEvent, EnclaveEventData, EventType, Shutdown, TypedEvent, }; use e3_utils::{NotifySync, MAILBOX_LIMIT}; use std::collections::HashMap; @@ -33,7 +33,12 @@ impl CommitteeFinalizer { let addr = CommitteeFinalizer::new(bus).start(); bus.subscribe_all( - &[EventType::CommitteeRequested, EventType::Shutdown], + &[ + EventType::CommitteeRequested, + EventType::Shutdown, + EventType::E3Failed, + EventType::E3StageChanged, + ], addr.clone().recipient(), ); @@ -57,6 +62,10 @@ impl Handler for CommitteeFinalizer { self.notify_sync(ctx, TypedEvent::new(data, ec)) } EnclaveEventData::Shutdown(data) => self.notify_sync(ctx, data), + EnclaveEventData::E3Failed(data) => self.notify_sync(ctx, TypedEvent::new(data, ec)), + EnclaveEventData::E3StageChanged(data) => { + self.notify_sync(ctx, TypedEvent::new(data, ec)) + } _ => (), } } @@ -166,3 +175,38 @@ impl Handler for CommitteeFinalizer { ctx.stop(); } } + +impl Handler> for CommitteeFinalizer { + type Result = (); + fn handle(&mut self, msg: TypedEvent, ctx: &mut Self::Context) -> Self::Result { + let e3_id_str = msg.e3_id.to_string(); + if let Some(handle) = self.pending_committees.remove(&e3_id_str) { + info!( + e3_id = %msg.e3_id, + reason = ?msg.reason, + "E3 failed — cancelling pending committee finalization timer" + ); + ctx.cancel_future(handle); + } + } +} + +impl Handler> for CommitteeFinalizer { + type Result = (); + fn handle(&mut self, msg: TypedEvent, ctx: &mut Self::Context) -> Self::Result { + match &msg.new_stage { + E3Stage::Complete | E3Stage::Failed => { + let e3_id_str = msg.e3_id.to_string(); + if let Some(handle) = self.pending_committees.remove(&e3_id_str) { + info!( + e3_id = %msg.e3_id, + stage = ?msg.new_stage, + "E3 reached terminal stage — cancelling pending committee finalization timer" + ); + ctx.cancel_future(handle); + } + } + _ => {} + } + } +} diff --git a/crates/aggregator/src/keyshare_created_filter_buffer.rs b/crates/aggregator/src/keyshare_created_filter_buffer.rs index eea8edd67c..8171f1599c 100644 --- a/crates/aggregator/src/keyshare_created_filter_buffer.rs +++ b/crates/aggregator/src/keyshare_created_filter_buffer.rs @@ -6,13 +6,14 @@ use actix::prelude::*; -use e3_events::{prelude::*, EnclaveEvent, EnclaveEventData}; +use e3_events::{prelude::*, Die, EnclaveEvent, EnclaveEventData}; use e3_utils::MAILBOX_LIMIT; use std::collections::HashSet; +use tracing::info; use crate::PublicKeyAggregator; -/// Buffer KeyshareCreated events until CommitteeFinalized has been published +/// Buffers `KeyshareCreated` events until `CommitteeFinalized` arrives. pub struct KeyshareCreatedFilterBuffer { dest: Addr, committee: Option>, @@ -65,14 +66,38 @@ impl Handler for KeyshareCreatedFilterBuffer { _ => {} }, EnclaveEventData::CommitteeFinalized(data) => { - self.dest.do_send(msg.clone()); // forward committee first + self.dest.do_send(msg.clone()); self.committee = Some(data.committee.iter().cloned().collect()); self.process_buffered_events(); } + EnclaveEventData::CommitteeMemberExpelled(data) => { + // Only process raw events from chain (party_id not yet resolved). + if data.party_id.is_some() { + return; + } + + // Remove expelled node so we don't forward late KeyshareCreated events from them + if let Some(ref mut committee) = self.committee { + let node_addr = data.node.to_string(); + info!( + "KeyshareCreatedFilterBuffer: removing expelled node {} from committee filter (e3_id={})", + node_addr, data.e3_id + ); + committee.remove(&node_addr); + } + // Forward to PublicKeyAggregator for threshold_n adjustment + self.dest.do_send(msg); + } _ => { - // forward all other events self.dest.do_send(msg); } } } } + +impl Handler for KeyshareCreatedFilterBuffer { + type Result = (); + fn handle(&mut self, _: Die, ctx: &mut Self::Context) -> Self::Result { + ctx.stop(); + } +} diff --git a/crates/aggregator/src/publickey_aggregator.rs b/crates/aggregator/src/publickey_aggregator.rs index 1b36c91ad3..d8cf851573 100644 --- a/crates/aggregator/src/publickey_aggregator.rs +++ b/crates/aggregator/src/publickey_aggregator.rs @@ -137,6 +137,50 @@ impl PublicKeyAggregator { }) }) } + + pub fn handle_member_expelled( + &mut self, + node: &str, + ec: &EventContext, + ) -> Result<()> { + self.state.try_mutate(ec, |mut state| { + let PublicKeyAggregatorState::Collecting { + threshold_n, + keyshares, + nodes, + .. + } = &mut state + else { + return Ok(state); + }; + + // Remove the expelled node from the nodes set so it won't appear in + // PublicKeyAggregated.nodes (forwarded on-chain for reward distribution). + // Note: the corresponding keyshare cannot be removed because the + // keyshares OrderedSet is keyed by raw bytes with no node mapping. + // This is acceptable because BFV public key aggregation is additive + // and works correctly with any superset of valid keys. + nodes.remove(&node.to_string()); + + if *threshold_n > 0 { + *threshold_n -= 1; + info!( + "PublicKeyAggregator: reduced threshold_n to {} after expelling {}", + threshold_n, node + ); + } + + if keyshares.len() == *threshold_n && *threshold_n > 0 { + info!("PublicKeyAggregator: enough keyshares after expulsion, computing aggregate"); + return Ok(PublicKeyAggregatorState::Computing { + keyshares: std::mem::take(keyshares), + nodes: std::mem::take(nodes), + }); + } + + Ok(state) + }) + } } impl Actor for PublicKeyAggregator { @@ -155,6 +199,50 @@ impl Handler for PublicKeyAggregator { self.notify_sync(ctx, TypedEvent::new(data, ec)) } EnclaveEventData::E3RequestComplete(_) => self.notify_sync(ctx, Die), + EnclaveEventData::CommitteeMemberExpelled(data) => { + // Only process raw events from chain (party_id not yet resolved). + if data.party_id.is_some() { + return; + } + + let node_addr = data.node.to_string(); + + if data.e3_id != self.e3_id { + error!("Wrong e3_id sent to PublicKeyAggregator for expulsion. This should not happen."); + return; + } + + info!( + "PublicKeyAggregator: committee member expelled: {} for e3_id={}", + node_addr, data.e3_id + ); + trap(EType::PublickeyAggregation, &self.bus.with_ec(&ec), || { + let was_collecting = matches!( + self.state.get(), + Some(PublicKeyAggregatorState::Collecting { .. }) + ); + + self.handle_member_expelled(&node_addr, &ec)?; + + if was_collecting { + if let Some(PublicKeyAggregatorState::Computing { keyshares, .. }) = + &self.state.get() + { + self.notify_sync( + ctx, + TypedEvent::new( + ComputeAggregate { + keyshares: keyshares.clone(), + e3_id: data.e3_id, + }, + ec.clone(), + ), + ); + } + } + Ok(()) + }); + } _ => (), }; } @@ -217,7 +305,6 @@ impl Handler> for PublicKeyAggregator { self.fhe.params.moduli().to_vec(), )?; - // Update the local state self.set_pubkey(pubkey, &ec)?; if let Some(PublicKeyAggregatorState::Complete { diff --git a/crates/ciphernode-builder/src/ciphernode_builder.rs b/crates/ciphernode-builder/src/ciphernode_builder.rs index b44edddbbf..d1a0e67a15 100644 --- a/crates/ciphernode-builder/src/ciphernode_builder.rs +++ b/crates/ciphernode-builder/src/ciphernode_builder.rs @@ -18,7 +18,9 @@ use e3_events::{ AggregateConfig, AggregateId, BusHandle, EnclaveEvent, EventBus, EventBusConfig, EvmEventConfig, }; use e3_evm::{BondingRegistrySolReader, CiphernodeRegistrySolReader, EnclaveSolWriter}; -use e3_evm::{CiphernodeRegistrySol, EnclaveSolReader}; +use e3_evm::{ + CiphernodeRegistrySol, EnclaveSolReader, SlashingManagerSolReader, SlashingManagerSolWriter, +}; use e3_fhe::ext::FheExtension; use e3_fhe_params::BfvPreset; use e3_keyshare::ext::ThresholdKeyshareExtension; @@ -95,6 +97,7 @@ pub struct ContractComponents { enclave: bool, ciphernode_registry: bool, bonding_registry: bool, + slashing_manager: bool, } #[derive(Clone, Debug)] @@ -296,6 +299,13 @@ impl CiphernodeBuilder { self } + /// Setup a SlashingManager writer for submitting slash proposals on-chain. + /// Requires the `slashing_manager` contract address to be configured. + pub fn with_contract_slashing_manager(mut self) -> Self { + self.contract_components.slashing_manager = true; + self + } + /// Setup net package components. pub fn with_net(mut self, peers: Vec, quic_port: u16) -> Self { self.net_config = Some(NetConfig::new(peers, quic_port)); @@ -668,6 +678,41 @@ async fn setup_evm_system( ) } } + + if contract_components.slashing_manager { + if let Some(contract) = &chain.contracts.slashing_manager { + // Reader: read SlashExecuted events from chain + let contract_addr = contract.address()?; + system.with_contract(contract_addr, move |next| { + SlashingManagerSolReader::setup(&next).recipient() + }); + + // Writer: submit proposeSlash transactions + match provider_cache.ensure_write_provider(&chain).await { + Ok(write_provider) => { + match SlashingManagerSolWriter::attach( + &bus, + write_provider.clone(), + contract_addr, + ) + .await + { + Ok(_) => { + info!("SlashingManagerSolWriter attached for fault submission"); + } + Err(e) => { + error!("Failed to attach SlashingManagerSolWriter, skipping: {}", e) + } + } + } + Err(e) => error!( + "Failed to create write provider for SlashingManager, skipping: {}", + e + ), + } + } + } + system.build(); } diff --git a/crates/config/src/contract.rs b/crates/config/src/contract.rs index 420d3c953c..32c856d2ad 100644 --- a/crates/config/src/contract.rs +++ b/crates/config/src/contract.rs @@ -48,6 +48,7 @@ pub struct ContractAddresses { pub bonding_registry: Contract, pub e3_program: Option, pub fee_token: Option, + pub slashing_manager: Option, } impl ContractAddresses { @@ -58,6 +59,7 @@ impl ContractAddresses { Some(&self.bonding_registry), self.e3_program.as_ref(), self.fee_token.as_ref(), + self.slashing_manager.as_ref(), ] .into_iter() .flatten() diff --git a/crates/entrypoint/src/config/setup.rs b/crates/entrypoint/src/config/setup.rs index b8764ff05f..49a9d044a1 100644 --- a/crates/entrypoint/src/config/setup.rs +++ b/crates/entrypoint/src/config/setup.rs @@ -59,6 +59,9 @@ chains: bonding_registry: address: "{}" deploy_block: {} + slashing_manager: + address: "{}" + deploy_block: {} "#, rpc_url, get_contract_info("Enclave")?.address, @@ -67,6 +70,8 @@ chains: get_contract_info("CiphernodeRegistryOwnable")?.deploy_block, get_contract_info("BondingRegistry")?.address, get_contract_info("BondingRegistry")?.deploy_block, + get_contract_info("SlashingManager")?.address, + get_contract_info("SlashingManager")?.deploy_block, ); fs::write(config_path.clone(), config_content)?; diff --git a/crates/entrypoint/src/start/aggregator_start.rs b/crates/entrypoint/src/start/aggregator_start.rs index 1ad2275633..06e5918cc3 100644 --- a/crates/entrypoint/src/start/aggregator_start.rs +++ b/crates/entrypoint/src/start/aggregator_start.rs @@ -31,6 +31,7 @@ pub async fn execute( .with_contract_enclave_full() .with_contract_bonding_registry() .with_contract_ciphernode_registry() + .with_contract_slashing_manager() .with_max_threads() .with_pubkey_aggregation() .with_threshold_plaintext_aggregation() diff --git a/crates/entrypoint/src/start/start.rs b/crates/entrypoint/src/start/start.rs index 6a16d9d5cd..3c1ea3073d 100644 --- a/crates/entrypoint/src/start/start.rs +++ b/crates/entrypoint/src/start/start.rs @@ -35,6 +35,7 @@ pub async fn execute(config: &AppConfig) -> Result { .with_contract_bonding_registry() .with_max_threads() .with_contract_ciphernode_registry() + .with_contract_slashing_manager() .with_trbfv() .with_zkproof(backend) .with_net(config.peers(), config.quic_port()) diff --git a/crates/events/src/committee.rs b/crates/events/src/committee.rs new file mode 100644 index 0000000000..b55a2a04e9 --- /dev/null +++ b/crates/events/src/committee.rs @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +/// Ordered list of committee members where index == party_id. +/// +/// Provides O(1) address→party_id lookups via an internal index. +/// The index is eagerly rebuilt when the struct is deserialized. +/// +/// `PartialEq` compares only the `members` vec (the canonical data); +/// the `index` is a derived cache. +#[derive(Clone, Debug, Serialize)] +pub struct Committee { + /// Ordered member list — index == party_id. + members: Vec, + /// Lowercased-address → party_id for O(1) lookup. + #[serde(skip)] + index: HashMap, +} + +impl PartialEq for Committee { + fn eq(&self, other: &Self) -> bool { + self.members == other.members + } +} + +impl Eq for Committee {} + +impl<'de> Deserialize<'de> for Committee { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + struct Inner { + members: Vec, + } + let inner = Inner::deserialize(deserializer)?; + Ok(Committee::new(inner.members)) + } +} + +impl Committee { + pub fn new(members: Vec) -> Self { + let index = members + .iter() + .enumerate() + .map(|(i, addr)| (addr.to_lowercase(), i as u64)) + .collect(); + Self { members, index } + } + + /// Resolve an address to its party_id (position in the committee list). + pub fn party_id_for(&self, addr: &str) -> Option { + self.index.get(&addr.to_lowercase()).copied() + } + + /// Check if an address is a committee member. + pub fn contains(&self, addr: &str) -> bool { + self.party_id_for(addr).is_some() + } + + /// The ordered member list (index == party_id). + pub fn members(&self) -> &[String] { + &self.members + } + + pub fn len(&self) -> usize { + self.members.len() + } + + pub fn is_empty(&self) -> bool { + self.members.is_empty() + } +} diff --git a/crates/events/src/enclave_event/mod.rs b/crates/events/src/enclave_event/mod.rs index fba6dce82c..43ad475dea 100644 --- a/crates/events/src/enclave_event/mod.rs +++ b/crates/events/src/enclave_event/mod.rs @@ -38,6 +38,7 @@ mod publickey_aggregated; mod publish_document; mod shutdown; mod signed_proof; +mod slash_executed; mod sync_effect; mod sync_end; mod sync_start; @@ -85,6 +86,7 @@ pub use publickey_aggregated::*; pub use publish_document::*; pub use shutdown::*; pub use signed_proof::*; +pub use slash_executed::*; use strum::IntoStaticStr; pub use sync_effect::*; pub use sync_end::*; @@ -232,6 +234,8 @@ pub enum EnclaveEventData { ComputeResponse(ComputeResponse), // ComputeResponseReceived ComputeRequestError(ComputeRequestError), // ComputeRequestFailed SignedProofFailed(SignedProofFailed), + SlashExecuted(SlashExecuted), + CommitteeMemberExpelled(CommitteeMemberExpelled), OutgoingSyncRequested(OutgoingSyncRequested), NetSyncEventsReceived(NetSyncEventsReceived), HistoricalEvmSyncStart(HistoricalEvmSyncStart), @@ -484,6 +488,8 @@ impl EnclaveEventData { EnclaveEventData::ComputeResponse(ref data) => Some(data.e3_id.clone()), EnclaveEventData::TestEvent(ref data) => data.e3_id.clone(), EnclaveEventData::SignedProofFailed(ref data) => Some(data.e3_id.clone()), + EnclaveEventData::SlashExecuted(ref data) => Some(data.e3_id.clone()), + EnclaveEventData::CommitteeMemberExpelled(ref data) => Some(data.e3_id.clone()), EnclaveEventData::E3Failed(ref data) => Some(data.e3_id.clone()), EnclaveEventData::E3StageChanged(ref data) => Some(data.e3_id.clone()), _ => None, @@ -550,6 +556,8 @@ impl_event_types!( ComputeResponse, ComputeRequestError, SignedProofFailed, + SlashExecuted, + CommitteeMemberExpelled, OutgoingSyncRequested, NetSyncEventsReceived, HistoricalEvmSyncStart, diff --git a/crates/events/src/enclave_event/signed_proof.rs b/crates/events/src/enclave_event/signed_proof.rs index 4834144cb7..e26a4ba005 100644 --- a/crates/events/src/enclave_event/signed_proof.rs +++ b/crates/events/src/enclave_event/signed_proof.rs @@ -13,7 +13,7 @@ use crate::{CircuitName, E3id, Proof}; use actix::Message; -use alloy::primitives::{keccak256, Address, Bytes, Signature, U256}; +use alloy::primitives::{keccak256, Address, FixedBytes, Signature, U256}; use alloy::signers::{local::PrivateKeySigner, SignerSync}; use alloy::sol_types::SolValue; use anyhow::{anyhow, Result}; @@ -104,11 +104,36 @@ pub struct ProofPayload { } impl ProofPayload { + /// The typehash that domain-separates the signed message. + /// + /// Must match `PROOF_PAYLOAD_TYPEHASH` in `SlashingManager.sol`: + /// `keccak256("ProofPayload(uint256 chainId,uint256 e3Id,uint256 proofType,bytes zkProof,bytes publicSignals)")` + pub fn typehash() -> [u8; 32] { + keccak256( + "ProofPayload(uint256 chainId,uint256 e3Id,uint256 proofType,bytes zkProof,bytes publicSignals)", + ) + .into() + } + /// Compute the keccak256 digest of the canonical encoding. /// - /// Uses standard ABI encoding (`abi.encode`) which includes offsets and - /// lengths for dynamic types, avoiding collision between the two - /// variable-length `Bytes` fields (`proof` and `publicSignals`). + /// Uses structured hashing with a typehash prefix for domain separation, + /// and keccak256-hashes the dynamic fields (`zkProof`, `publicSignals`) + /// for gas efficiency on the Solidity verification side. + /// + /// The encoding is: + /// ```text + /// keccak256(abi.encode( + /// PROOF_PAYLOAD_TYPEHASH, // bytes32 + /// chainId, // uint256 + /// e3Id, // uint256 + /// proofType, // uint256 + /// keccak256(zkProof), // bytes32 + /// keccak256(publicSignals) // bytes32 + /// )) + /// ``` + /// + /// This matches the reconstruction in `SlashingManager.proposeSlash()`. pub fn digest(&self) -> Result<[u8; 32]> { let e3_id_u256: U256 = self .e3_id @@ -116,13 +141,17 @@ impl ProofPayload { .try_into() .map_err(|_| anyhow!("E3id cannot be converted to U256"))?; - // keccak256(abi.encode(chainId, e3Id, proofType, proof, publicSignals)) + let typehash = Self::typehash(); + + // keccak256(abi.encode(typehash, chainId, e3Id, proofType, keccak256(proof), keccak256(publicSignals))) + // All fields are bytes32/uint256 → pure static ABI encoding (6 × 32 = 192 bytes) let encoded = ( + typehash, U256::from(self.e3_id.chain_id()), e3_id_u256, U256::from(self.proof_type as u8), - Bytes::copy_from_slice(&self.proof.data), - Bytes::copy_from_slice(&self.proof.public_signals), + keccak256(&*self.proof.data), + keccak256(&*self.proof.public_signals), ) .abi_encode(); @@ -206,6 +235,47 @@ impl Display for SignedProofFailed { } } +/// Encode a [`SignedProofFailed`] event into the ABI-encoded evidence bytes +/// expected by `SlashingManager.proposeSlash()`. +/// +/// Returns: `abi.encode(bytes zkProof, bytes32[] publicInputs, bytes signature, uint256 chainId, uint256 proofType, address verifier)` +/// +/// The `verifier` is the current on-chain verifier contract address for this +/// proof type's slash policy. The `FaultSubmitter` actor must look this up +/// before calling this function. +pub fn encode_fault_evidence(failed: &SignedProofFailed, verifier: Address) -> Vec { + use alloy::primitives::Bytes; + + let proof = &failed.signed_payload.payload.proof; + + // Convert raw public_signals bytes → Vec> (one per 32-byte field) + let public_inputs: Vec> = proof + .public_signals + .chunks(32) + .map(|chunk| { + let mut buf = [0u8; 32]; + buf[..chunk.len()].copy_from_slice(chunk); + FixedBytes::from(buf) + }) + .collect(); + + // Must match the decode in SlashingManager.proposeSlash(): + // (bytes zkProof, bytes32[] publicInputs, bytes signature, uint256 chainId, uint256 proofType, address verifier) + // + // IMPORTANT: Use abi_encode_params() (not abi_encode()) because abi_encode() + // wraps dynamic tuples in an outer offset word, but Solidity's abi.decode() + // expects flat parameter encoding — the same as abi.encode(a, b, c, ...). + ( + Bytes::copy_from_slice(&proof.data), + public_inputs, + Bytes::copy_from_slice(&failed.signed_payload.signature), + U256::from(failed.e3_id.chain_id()), + U256::from(failed.signed_payload.payload.proof_type as u8), + verifier, + ) + .abi_encode_params() +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/events/src/enclave_event/slash_executed.rs b/crates/events/src/enclave_event/slash_executed.rs new file mode 100644 index 0000000000..824ae2b6a8 --- /dev/null +++ b/crates/events/src/enclave_event/slash_executed.rs @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +use crate::E3id; +use actix::Message; +use alloy::primitives::Address; +use serde::{Deserialize, Serialize}; +use std::fmt::{self, Display}; + +/// Emitted when a slash proposal is executed on-chain. +/// +/// This event is read from the SlashingManager contract logs. +/// The `CommitteeExpulsionHandler` reacts to this to update local committee state. +#[derive(Message, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[rtype(result = "()")] +pub struct SlashExecuted { + /// The E3 computation this slash relates to. + pub e3_id: E3id, + /// On-chain proposal ID. + pub proposal_id: u128, + /// Address of the slashed operator. + pub operator: Address, + /// Hash of the slash reason. + pub reason: [u8; 32], + /// Amount of ticket collateral slashed. + pub ticket_amount: u128, + /// Amount of license bond slashed. + pub license_amount: u128, +} + +impl Display for SlashExecuted { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "SlashExecuted {{ e3_id: {}, proposal_id: {}, operator: {} }}", + self.e3_id, self.proposal_id, self.operator + ) + } +} + +/// Emitted when a committee member is expelled from an E3 committee. +/// +/// Read from the CiphernodeRegistry contract logs after slashing triggers expulsion. +/// The `CommitteeExpulsionHandler` uses this to update the local committee view +/// and check viability (whether remaining active members >= threshold M). +#[derive(Message, Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[rtype(result = "()")] +pub struct CommitteeMemberExpelled { + /// The E3 computation from which the member was expelled. + pub e3_id: E3id, + /// Address of the expelled committee member. + pub node: Address, + /// Hash of the slash reason that caused the expulsion. + pub reason: [u8; 32], + /// Number of active committee members remaining after expulsion. + pub active_count_after: u64, + /// Party ID (position in the committee list) of the expelled member. + /// `None` when read from chain (not yet resolved); `Some(id)` after + /// enrichment by the Sortition actor. + pub party_id: Option, +} + +impl Display for CommitteeMemberExpelled { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "CommitteeMemberExpelled {{ e3_id: {}, node: {}, active_count_after: {}, party_id: {:?} }}", + self.e3_id, self.node, self.active_count_after, self.party_id + ) + } +} diff --git a/crates/events/src/lib.rs b/crates/events/src/lib.rs index 2d0e7b9050..bd0fe091dc 100644 --- a/crates/events/src/lib.rs +++ b/crates/events/src/lib.rs @@ -5,6 +5,7 @@ // or FITNESS FOR A PARTICULAR PURPOSE. mod bus_handle; +mod committee; mod correlation_id; mod data_events; mod e3id; @@ -29,6 +30,7 @@ mod sync; mod traits; pub use bus_handle::*; +pub use committee::*; pub use correlation_id::*; pub use data_events::*; pub use e3id::*; diff --git a/crates/events/src/ordered_set.rs b/crates/events/src/ordered_set.rs index 13f08295aa..5c5fd0b18d 100644 --- a/crates/events/src/ordered_set.rs +++ b/crates/events/src/ordered_set.rs @@ -33,6 +33,10 @@ impl OrderedSet { self.0.is_empty() } + pub fn remove(&mut self, value: &T) -> bool { + self.0.remove(value) + } + pub fn iter(&self) -> impl Iterator { self.0.iter() } diff --git a/crates/evm/src/ciphernode_registry_sol.rs b/crates/evm/src/ciphernode_registry_sol.rs index d56a9e6c4f..6a1cfe3369 100644 --- a/crates/evm/src/ciphernode_registry_sol.rs +++ b/crates/evm/src/ciphernode_registry_sol.rs @@ -157,6 +157,30 @@ impl From for EnclaveEventData { } } +struct CommitteeMemberExpelledWithChainId( + pub ICiphernodeRegistry::CommitteeMemberExpelled, + pub u64, +); + +impl From for e3_events::CommitteeMemberExpelled { + fn from(value: CommitteeMemberExpelledWithChainId) -> Self { + e3_events::CommitteeMemberExpelled { + e3_id: E3id::new(value.0.e3Id.to_string(), value.1), + node: value.0.node, + reason: value.0.reason.into(), + active_count_after: value.0.activeCountAfter.to(), + party_id: None, + } + } +} + +impl From for EnclaveEventData { + fn from(value: CommitteeMemberExpelledWithChainId) -> Self { + let payload: e3_events::CommitteeMemberExpelled = value.into(); + EnclaveEventData::from(payload) + } +} + pub fn extractor(data: &LogData, topic: Option<&B256>, chain_id: u64) -> Option { match topic { Some(&ICiphernodeRegistry::CiphernodeAdded::SIGNATURE_HASH) => { @@ -204,6 +228,20 @@ pub fn extractor(data: &LogData, topic: Option<&B256>, chain_id: u64) -> Option< event, chain_id, ))) } + Some(&ICiphernodeRegistry::CommitteeMemberExpelled::SIGNATURE_HASH) => { + let Ok(event) = ICiphernodeRegistry::CommitteeMemberExpelled::decode_log_data(data) + else { + error!("Error parsing event CommitteeMemberExpelled after topic was matched!"); + return None; + }; + info!( + "CommitteeMemberExpelled event received: e3_id={}, node={}, reason={:?}, active_count_after={}", + event.e3Id, event.node, event.reason, event.activeCountAfter + ); + Some(EnclaveEventData::from(CommitteeMemberExpelledWithChainId( + event, chain_id, + ))) + } _topic => { trace!( topic=?_topic, diff --git a/crates/evm/src/enclave_sol_writer.rs b/crates/evm/src/enclave_sol_writer.rs index a95416dce0..21a5fd8a42 100644 --- a/crates/evm/src/enclave_sol_writer.rs +++ b/crates/evm/src/enclave_sol_writer.rs @@ -26,7 +26,7 @@ use e3_events::{run_once, EnclaveEvent}; use e3_events::{E3id, EType, PlaintextAggregated}; use e3_utils::NotifySync; use e3_utils::MAILBOX_LIMIT; -use tracing::info; +use tracing::{info, warn}; sol!( #[sol(rpc)] @@ -114,6 +114,15 @@ impl Handler 1 { + warn!( + "E3 {} has {} decrypted outputs but only the first is published on-chain. \ + Multi-output support is not yet implemented.", + e3_id, + decrypted_output.len() + ); + } let result = publish_plaintext_output( provider, contract_address, diff --git a/crates/evm/src/lib.rs b/crates/evm/src/lib.rs index cb6a4717dc..a7eb3533ad 100644 --- a/crates/evm/src/lib.rs +++ b/crates/evm/src/lib.rs @@ -18,6 +18,8 @@ mod fix_historical_order; pub mod helpers; mod log_fetcher; mod repo; +mod slashing_manager_sol_reader; +mod slashing_manager_sol_writer; mod sync_start_extractor; pub use bonding_registry_sol::BondingRegistrySolReader; @@ -35,4 +37,6 @@ pub use evm_router::*; pub use fix_historical_order::*; pub use helpers::*; pub use repo::*; +pub use slashing_manager_sol_reader::SlashingManagerSolReader; +pub use slashing_manager_sol_writer::SlashingManagerSolWriter; pub use sync_start_extractor::*; diff --git a/crates/evm/src/slashing_manager_sol_reader.rs b/crates/evm/src/slashing_manager_sol_reader.rs new file mode 100644 index 0000000000..cb341c95c8 --- /dev/null +++ b/crates/evm/src/slashing_manager_sol_reader.rs @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +use crate::{ + events::EvmEventProcessor, evm_parser::EvmParser, slashing_manager_sol_writer::ISlashingManager, +}; +use actix::{Actor, Addr}; +use alloy::{ + primitives::{LogData, B256}, + sol_types::SolEvent, +}; +use e3_events::{E3id, EnclaveEventData}; +use tracing::{error, info, trace}; + +pub fn extractor(data: &LogData, topic: Option<&B256>, chain_id: u64) -> Option { + match topic { + Some(&ISlashingManager::SlashExecuted::SIGNATURE_HASH) => { + let Ok(event) = ISlashingManager::SlashExecuted::decode_log_data(data) else { + error!("Error parsing event SlashExecuted after topic was matched!"); + return None; + }; + info!( + "SlashExecuted event received: proposal_id={}, e3_id={}, operator={}, reason={:?}, ticket={}, license={}", + event.proposalId, event.e3Id, event.operator, event.reason, event.ticketAmount, event.licenseAmount + ); + Some(EnclaveEventData::from(e3_events::SlashExecuted { + e3_id: E3id::new(event.e3Id.to_string(), chain_id), + proposal_id: event.proposalId.to::(), + operator: event.operator, + reason: event.reason.into(), + ticket_amount: event.ticketAmount.to::(), + license_amount: event.licenseAmount.to::(), + })) + } + _topic => { + trace!( + topic=?_topic, + "Unknown event was received by SlashingManager.sol parser but was ignored" + ); + None + } + } +} + +/// Connects to SlashingManager.sol converting EVM events to EnclaveEvents +pub struct SlashingManagerSolReader; + +impl SlashingManagerSolReader { + pub fn setup(next: &EvmEventProcessor) -> Addr { + EvmParser::new(next, extractor).start() + } +} diff --git a/crates/evm/src/slashing_manager_sol_writer.rs b/crates/evm/src/slashing_manager_sol_writer.rs new file mode 100644 index 0000000000..446f24041c --- /dev/null +++ b/crates/evm/src/slashing_manager_sol_writer.rs @@ -0,0 +1,173 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +//! Subscribes to `SignedProofFailed` events and submits `proposeSlash` +//! transactions on the SlashingManager contract. + +use crate::helpers::EthProvider; +use crate::send_tx_with_retry; +use actix::prelude::*; +use actix::Addr; +use alloy::{ + primitives::{keccak256, Address, Bytes, U256}, + providers::{Provider, WalletProvider}, + rpc::types::TransactionReceipt, + sol, +}; +use anyhow::Result; +use e3_events::prelude::*; +use e3_events::BusHandle; +use e3_events::EnclaveEvent; +use e3_events::EnclaveEventData; +use e3_events::EventType; +use e3_events::Shutdown; +use e3_events::{encode_fault_evidence, EType, SignedProofFailed}; +use e3_utils::NotifySync; +use tracing::info; + +sol!( + #[sol(rpc)] + ISlashingManager, + "../../packages/enclave-contracts/artifacts/contracts/interfaces/ISlashingManager.sol/ISlashingManager.json" +); + +/// Submits `SignedProofFailed` events as slash proposals on-chain. +pub struct SlashingManagerSolWriter

{ + provider: EthProvider

, + contract_address: Address, + bus: BusHandle, +} + +impl SlashingManagerSolWriter

{ + pub fn new( + bus: &BusHandle, + provider: EthProvider

, + contract_address: Address, + ) -> Result { + Ok(Self { + provider, + contract_address, + bus: bus.clone(), + }) + } + + pub async fn attach( + bus: &BusHandle, + provider: EthProvider

, + contract_address: Address, + ) -> Result>> { + let addr = SlashingManagerSolWriter::new(bus, provider, contract_address)?.start(); + bus.subscribe_all( + &[EventType::SignedProofFailed, EventType::Shutdown], + addr.clone().into(), + ); + Ok(addr) + } +} + +impl Actor for SlashingManagerSolWriter

{ + type Context = actix::Context; +} + +impl Handler + for SlashingManagerSolWriter

+{ + type Result = (); + + fn handle(&mut self, msg: EnclaveEvent, ctx: &mut Self::Context) -> Self::Result { + match msg.into_data() { + EnclaveEventData::SignedProofFailed(data) => { + if self.provider.chain_id() == data.e3_id.chain_id() { + ctx.notify(data); + } + } + EnclaveEventData::Shutdown(data) => self.notify_sync(ctx, data), + _ => (), + } + } +} + +impl Handler + for SlashingManagerSolWriter

+{ + type Result = ResponseFuture<()>; + + fn handle(&mut self, msg: SignedProofFailed, _: &mut Self::Context) -> Self::Result { + Box::pin({ + let contract_address = self.contract_address; + let provider = self.provider.clone(); + let bus = self.bus.clone(); + async move { + let result = submit_slash_proposal(provider, contract_address, msg).await; + match result { + Ok(receipt) => { + info!(tx=%receipt.transaction_hash, "Submitted slash proposal on-chain"); + } + Err(err) => { + bus.err( + EType::Evm, + anyhow::anyhow!("Error submitting slash proposal: {:?}", err), + ); + } + } + } + }) + } +} + +impl Handler + for SlashingManagerSolWriter

+{ + type Result = (); + + fn handle(&mut self, _: Shutdown, ctx: &mut Self::Context) -> Self::Result { + ctx.stop(); + } +} + +async fn submit_slash_proposal( + provider: EthProvider

, + contract_address: Address, + data: SignedProofFailed, +) -> Result { + let e3_id: U256 = data.e3_id.clone().try_into()?; + let operator = data.faulting_node; + let reason = keccak256(data.proof_type.slash_reason().as_bytes()); + + // Look up the verifier address from the on-chain slash policy. + // This is required to encode the full 6-tuple evidence that the contract expects: + // (bytes zkProof, bytes32[] publicInputs, bytes signature, uint256 chainId, uint256 proofType, address verifier) + let contract = ISlashingManager::new(contract_address, provider.provider()); + let policy = contract.getSlashPolicy(reason.into()).call().await?; + let verifier = policy.proofVerifier; + + let proof_data = encode_fault_evidence(&data, verifier); + + let from_address = provider.provider().default_signer_address(); + let current_nonce = provider + .provider() + .get_transaction_count(from_address) + .pending() + .await?; + + send_tx_with_retry("proposeSlash", &[], || { + info!( + "proposeSlash() e3_id={:?} operator={:?} reason={:?}", + e3_id, operator, reason + ); + let proof = Bytes::from(proof_data.clone()); + let contract = ISlashingManager::new(contract_address, provider.provider()); + + async move { + let builder = contract + .proposeSlash(e3_id, operator, reason.into(), proof) + .nonce(current_nonce); + let receipt = builder.send().await?.get_receipt().await?; + Ok(receipt) + } + }) + .await +} diff --git a/crates/keyshare/src/encryption_key_collector.rs b/crates/keyshare/src/encryption_key_collector.rs index 15d5ef5d81..bdfb86adc8 100644 --- a/crates/keyshare/src/encryption_key_collector.rs +++ b/crates/keyshare/src/encryption_key_collector.rs @@ -12,7 +12,8 @@ use std::{ use actix::{Actor, ActorContext, Addr, AsyncContext, Handler, Message, SpawnHandle}; use e3_events::{ - E3id, EncryptionKey, EncryptionKeyCollectionFailed, EncryptionKeyCreated, TypedEvent, + E3id, EncryptionKey, EncryptionKeyCollectionFailed, EncryptionKeyCreated, EventContext, + Sequenced, TypedEvent, }; use e3_trbfv::PartyId; use e3_utils::MAILBOX_LIMIT; @@ -57,10 +58,21 @@ impl From>> for AllEncryptionKeysCollected { #[rtype(result = "()")] pub struct EncryptionKeyCollectionTimeout; +/// Removes this party from the `todo` set so the DKG can complete with +/// N-1 keys instead of waiting for a key that will never arrive. +#[derive(Message, Clone, Debug)] +#[rtype(result = "()")] +pub struct ExpelPartyFromKeyCollection { + pub party_id: PartyId, + pub ec: EventContext, +} + /// Actor that collects BFV encryption keys from all parties. /// /// Once all keys are collected, it sends `AllEncryptionKeysCollected` to the parent /// `ThresholdKeyshare` actor. If collection times out, it sends `EncryptionKeyCollectionFailed`. +/// If a party is expelled (slashed), it is removed from the expected set so the +/// collection can complete with N-1 parties. pub struct EncryptionKeyCollector { e3_id: E3id, todo: HashSet, @@ -194,7 +206,60 @@ impl Handler for EncryptionKeyCollector { missing_parties, }); - // Stop the actor ctx.stop(); } } + +impl Handler for EncryptionKeyCollector { + type Result = (); + fn handle( + &mut self, + msg: ExpelPartyFromKeyCollection, + ctx: &mut Self::Context, + ) -> Self::Result { + // Only handle if we're still collecting + if !matches!(self.state, CollectorState::Collecting) { + return; + } + + let party_id = msg.party_id; + + // Remove expelled party from the todo set + if !self.todo.remove(&party_id) { + info!( + e3_id = %self.e3_id, + party_id = party_id, + "Expelled party {} was not in todo set (already received or unknown)", + party_id + ); + return; + } + + info!( + e3_id = %self.e3_id, + party_id = party_id, + remaining = self.todo.len(), + "Removed expelled party {} from encryption key collection, {} remaining", + party_id, + self.todo.len() + ); + + // Check if all remaining keys have been collected + if self.todo.is_empty() { + info!( + e3_id = %self.e3_id, + "All remaining encryption keys collected after party expulsion!" + ); + self.state = CollectorState::Finished; + + // Cancel the timeout since we're done + if let Some(handle) = self.timeout_handle.take() { + ctx.cancel_future(handle); + } + + let event: TypedEvent = + TypedEvent::new(self.keys.clone().into(), msg.ec.clone()); + self.parent.do_send(event); + } + } +} diff --git a/crates/keyshare/src/lib.rs b/crates/keyshare/src/lib.rs index c9d9c80cd9..50bf3dac03 100644 --- a/crates/keyshare/src/lib.rs +++ b/crates/keyshare/src/lib.rs @@ -9,6 +9,8 @@ pub mod ext; mod repo; mod threshold_keyshare; mod threshold_share_collector; -pub use encryption_key_collector::{AllEncryptionKeysCollected, EncryptionKeyCollector}; +pub use encryption_key_collector::{ + AllEncryptionKeysCollected, EncryptionKeyCollector, ExpelPartyFromKeyCollection, +}; pub use repo::*; pub use threshold_keyshare::*; diff --git a/crates/keyshare/src/threshold_keyshare.rs b/crates/keyshare/src/threshold_keyshare.rs index b2a2e3a0f9..b27ba8f8c1 100644 --- a/crates/keyshare/src/threshold_keyshare.rs +++ b/crates/keyshare/src/threshold_keyshare.rs @@ -9,13 +9,13 @@ use anyhow::{anyhow, bail, Context, Result}; use e3_crypto::{Cipher, SensitiveBytes}; use e3_data::Persistable; use e3_events::{ - prelude::*, trap, BusHandle, CiphernodeSelected, CiphertextOutputPublished, ComputeRequest, - ComputeResponse, ComputeResponseKind, CorrelationId, DecryptionshareCreated, Die, - E3RequestComplete, E3id, EType, EnclaveEvent, EnclaveEventData, EncryptionKey, - EncryptionKeyCollectionFailed, EncryptionKeyCreated, EncryptionKeyPending, EventContext, - KeyshareCreated, PartyId, PkGenerationProofRequest, PkGenerationProofSigned, Sequenced, - SignedProofPayload, ThresholdShare, ThresholdShareCollectionFailed, ThresholdShareCreated, - ThresholdSharePending, TypedEvent, + prelude::*, trap, BusHandle, CiphernodeSelected, CiphertextOutputPublished, + CommitteeMemberExpelled, ComputeRequest, ComputeResponse, ComputeResponseKind, CorrelationId, + DecryptionshareCreated, Die, E3RequestComplete, E3id, EType, EnclaveEvent, EnclaveEventData, + EncryptionKey, EncryptionKeyCollectionFailed, EncryptionKeyCreated, EncryptionKeyPending, + EventContext, KeyshareCreated, PartyId, PkGenerationProofRequest, PkGenerationProofSigned, + Sequenced, SignedProofPayload, ThresholdShare, ThresholdShareCollectionFailed, + ThresholdShareCreated, ThresholdSharePending, TypedEvent, }; use e3_fhe_params::create_deterministic_crp_from_default_seed; use e3_fhe_params::{BfvParamSet, BfvPreset}; @@ -37,10 +37,12 @@ use fhe::bfv::{PublicKey, SecretKey}; use fhe_traits::{DeserializeParametrized, Serialize}; use rand::rngs::OsRng; use std::{collections::HashMap, mem, sync::Arc}; -use tracing::{info, trace, warn}; +use tracing::{error, info, trace, warn}; -use crate::encryption_key_collector::{AllEncryptionKeysCollected, EncryptionKeyCollector}; -use crate::threshold_share_collector::ThresholdShareCollector; +use crate::encryption_key_collector::{ + AllEncryptionKeysCollected, EncryptionKeyCollector, ExpelPartyFromKeyCollection, +}; +use crate::threshold_share_collector::{ExpelPartyFromShareCollection, ThresholdShareCollector}; #[derive(Message, Clone, Debug, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] #[rtype(result = "()")] @@ -379,6 +381,36 @@ impl ThresholdKeyshare { Ok(addr.clone()) } + fn handle_committee_member_expelled( + &mut self, + data: CommitteeMemberExpelled, + ec: EventContext, + ) { + // Only process enriched events (party_id resolved by Sortition). + // Raw events from chain (party_id = None) are ignored here; + // Sortition will re-publish them with party_id set. + let Some(party_id) = data.party_id else { + return; + }; + + let node_addr = data.node.to_string(); + info!( + "CommitteeMemberExpelled received (enriched): node={}, party_id={}, e3_id={}, active_count_after={}", + node_addr, party_id, data.e3_id, data.active_count_after + ); + + if let Some(ref collector) = self.encryption_key_collector { + collector.do_send(ExpelPartyFromKeyCollection { + party_id, + ec: ec.clone(), + }); + } + + if let Some(ref collector) = self.decryption_key_collector { + collector.do_send(ExpelPartyFromShareCollection { party_id, ec }); + } + } + pub fn handle_threshold_share_created( &mut self, msg: TypedEvent, @@ -1047,32 +1079,12 @@ impl Handler for ThresholdKeyshare { let _ = self.handle_pk_generation_proof_signed(TypedEvent::new(data, ec)); } EnclaveEventData::E3RequestComplete(data) => self.notify_sync(ctx, data), - EnclaveEventData::E3Failed(data) => { - warn!( - "E3 failed: {:?}. Shutting down ThresholdKeyshare for e3_id={}", - data.reason, data.e3_id - ); - self.notify_sync(ctx, E3RequestComplete { e3_id: data.e3_id }); - } - EnclaveEventData::E3StageChanged(data) => { - use e3_events::E3Stage; - match &data.new_stage { - E3Stage::Complete | E3Stage::Failed => { - info!("E3 reached terminal stage {:?}. Shutting down ThresholdKeyshare for e3_id={}", data.new_stage, data.e3_id); - self.notify_sync(ctx, E3RequestComplete { e3_id: data.e3_id }); - } - _ => { - trace!( - "E3 stage changed to {:?} for e3_id={}", - data.new_stage, - data.e3_id - ); - } - } - } EnclaveEventData::ComputeResponse(data) => { self.notify_sync(ctx, TypedEvent::new(data, ec)) } + EnclaveEventData::CommitteeMemberExpelled(data) => { + self.handle_committee_member_expelled(data, ec); + } _ => (), } } diff --git a/crates/keyshare/src/threshold_share_collector.rs b/crates/keyshare/src/threshold_share_collector.rs index ae1167352f..0c4772eb05 100644 --- a/crates/keyshare/src/threshold_share_collector.rs +++ b/crates/keyshare/src/threshold_share_collector.rs @@ -12,7 +12,8 @@ use std::{ use actix::{Actor, ActorContext, Addr, AsyncContext, Handler, Message, SpawnHandle}; use e3_events::{ - E3id, ThresholdShare, ThresholdShareCollectionFailed, ThresholdShareCreated, TypedEvent, + E3id, EventContext, Sequenced, ThresholdShare, ThresholdShareCollectionFailed, + ThresholdShareCreated, TypedEvent, }; use e3_trbfv::PartyId; use e3_utils::MAILBOX_LIMIT; @@ -33,6 +34,15 @@ pub(crate) enum CollectorState { #[rtype(result = "()")] pub struct ThresholdShareCollectionTimeout; +/// Removes this party from the `todo` set so the DKG can complete with +/// N-1 shares instead of waiting for a share that will never arrive. +#[derive(Message, Clone, Debug)] +#[rtype(result = "()")] +pub struct ExpelPartyFromShareCollection { + pub party_id: PartyId, + pub ec: EventContext, +} + pub struct ThresholdShareCollector { /// The E3id for the round e3_id: E3id, @@ -166,7 +176,60 @@ impl Handler for ThresholdShareCollector { missing_parties, }); - // Stop the actor ctx.stop(); } } + +impl Handler for ThresholdShareCollector { + type Result = (); + fn handle( + &mut self, + msg: ExpelPartyFromShareCollection, + ctx: &mut Self::Context, + ) -> Self::Result { + // Only handle if we're still collecting + if !matches!(self.state, CollectorState::Collecting) { + return; + } + + let party_id = msg.party_id; + + // Remove expelled party from the todo set + if !self.todo.remove(&party_id) { + info!( + e3_id = %self.e3_id, + party_id = party_id, + "Expelled party {} was not in share collection todo set (already received or unknown)", + party_id + ); + return; + } + + info!( + e3_id = %self.e3_id, + party_id = party_id, + remaining = self.todo.len(), + "Removed expelled party {} from threshold share collection, {} remaining", + party_id, + self.todo.len() + ); + + // Check if all remaining shares have been collected + if self.todo.is_empty() { + info!( + e3_id = %self.e3_id, + "All remaining threshold shares collected after party expulsion!" + ); + self.state = CollectorState::Finished; + + // Cancel the timeout since we're done + if let Some(handle) = self.timeout_handle.take() { + ctx.cancel_future(handle); + } + + let event: TypedEvent = + TypedEvent::new(self.shares.clone().into(), msg.ec); + self.parent.do_send(event); + } + } +} diff --git a/crates/request/src/router.rs b/crates/request/src/router.rs index d7a0a52b7e..93fea43614 100644 --- a/crates/request/src/router.rs +++ b/crates/request/src/router.rs @@ -23,6 +23,7 @@ use e3_events::prelude::*; use e3_events::trap; use e3_events::BusHandle; use e3_events::E3RequestComplete; +use e3_events::E3Stage; use e3_events::EType; use e3_events::EnclaveEventData; use e3_events::EventType; @@ -201,6 +202,14 @@ impl Handler for E3Router { // Send to bus so all other actors can react to a request being complete. self.bus.publish(event, ctx)?; } + EnclaveEventData::E3StageChanged(ref data) + if matches!(data.new_stage, E3Stage::Complete | E3Stage::Failed) => + { + let event = E3RequestComplete { + e3_id: e3_id.clone(), + }; + self.bus.publish(event, ctx)?; + } EnclaveEventData::E3RequestComplete(_) => { // Note this will be sent above to the children who can kill themselves based on // the event diff --git a/crates/sortition/src/repo.rs b/crates/sortition/src/repo.rs index ffa3b84cc9..0b25aad99b 100644 --- a/crates/sortition/src/repo.rs +++ b/crates/sortition/src/repo.rs @@ -7,7 +7,7 @@ use crate::backends::SortitionBackend; use crate::sortition::NodeStateStore; use e3_data::{Repositories, Repository}; -use e3_events::{E3id, StoreKeys}; +use e3_events::{Committee, E3id, StoreKeys}; use e3_request::E3Meta; use std::collections::HashMap; @@ -42,11 +42,11 @@ impl NodeStateRepositoryFactory for Repositories { } pub trait FinalizedCommitteesRepositoryFactory { - fn finalized_committees(&self) -> Repository>>; + fn finalized_committees(&self) -> Repository>; } impl FinalizedCommitteesRepositoryFactory for Repositories { - fn finalized_committees(&self) -> Repository>> { + fn finalized_committees(&self) -> Repository> { Repository::new(self.store.scope(StoreKeys::finalized_committees())) } } diff --git a/crates/sortition/src/sortition.rs b/crates/sortition/src/sortition.rs index a690a7f08c..ee6e37ba09 100644 --- a/crates/sortition/src/sortition.rs +++ b/crates/sortition/src/sortition.rs @@ -12,10 +12,11 @@ use alloy::primitives::U256; use anyhow::{anyhow, Result}; use e3_data::{AutoPersist, Persistable, Repository}; use e3_events::{ - prelude::*, trap, CiphernodeAdded, CiphernodeRemoved, CommitteeFinalized, CommitteePublished, - ConfigurationUpdated, E3Failed, E3Requested, E3Stage, E3StageChanged, EType, EnclaveEvent, - EventContext, EventType, OperatorActivationChanged, PlaintextOutputPublished, Seed, Sequenced, - TicketBalanceUpdated, TypedEvent, + prelude::*, trap, CiphernodeAdded, CiphernodeRemoved, Committee, CommitteeFinalized, + CommitteeMemberExpelled, CommitteePublished, ConfigurationUpdated, E3Failed, E3Requested, + E3Stage, E3StageChanged, EType, EnclaveEvent, EventContext, EventType, + OperatorActivationChanged, PlaintextOutputPublished, Seed, Sequenced, TicketBalanceUpdated, + TypedEvent, }; use e3_events::{BusHandle, E3id, EnclaveEventData}; use e3_utils::{NotifySync, MAILBOX_LIMIT}; @@ -212,7 +213,7 @@ pub struct Sortition { /// Event bus for error reporting and enclave event subscription. bus: BusHandle, /// Persistent map of finalized committees per E3 - finalized_committees: Persistable>>, + finalized_committees: Persistable>, /// Address for the CiphernodeSelector ciphernode_selector: Addr, /// Address for the current node @@ -229,7 +230,7 @@ pub struct SortitionParams { /// Node state store per chain pub node_state: Persistable>, /// Persistent map of finalized committees per E3 - pub finalized_committees: Persistable>>, + pub finalized_committees: Persistable>, /// Address for the CiphernodeSelector pub ciphernode_selector: Addr, /// Address for the current node @@ -253,7 +254,7 @@ impl Sortition { bus: &BusHandle, backends_store: Repository>, node_state_store: Repository>, - committees_store: Repository>>, + committees_store: Repository>, default_backend: SortitionBackend, ciphernode_selector: Addr, address: &str, @@ -289,6 +290,7 @@ impl Sortition { EventType::CommitteePublished, EventType::PlaintextOutputPublished, EventType::CommitteeFinalized, + EventType::CommitteeMemberExpelled, EventType::E3Failed, EventType::E3StageChanged, ], @@ -331,23 +333,21 @@ impl Sortition { }) } - fn get_committe(&self, e3_id: &E3id) -> Vec { + fn get_committee(&self, e3_id: &E3id) -> Option { self.finalized_committees .get() .and_then(|committees| committees.get(e3_id).cloned()) - .unwrap_or_else(|| Vec::new()) } fn committee_contains(&mut self, e3_id: E3id, node: String) -> bool { - let committee = self.get_committe(&e3_id); - - if committee.len() == 0 { + let Some(committee) = self.get_committee(&e3_id) else { // Non blocking error self.bus.err( EType::Sortition, anyhow!("No finalized committee found for E3 {}", e3_id), ); - } + return false; + }; committee.contains(&node) } @@ -438,6 +438,13 @@ impl Handler for Sortition { EnclaveEventData::CommitteeFinalized(data) => { self.notify_sync(ctx, TypedEvent::new(data, ec)) } + EnclaveEventData::CommitteeMemberExpelled(data) => { + self.notify_sync(ctx, TypedEvent::new(data, ec)) + } + EnclaveEventData::E3Failed(data) => self.notify_sync(ctx, TypedEvent::new(data, ec)), + EnclaveEventData::E3StageChanged(data) => { + self.notify_sync(ctx, TypedEvent::new(data, ec)) + } _ => (), } } @@ -784,9 +791,64 @@ impl Handler> for Sortition { ); self.finalized_committees.try_mutate(&ec, |mut committees| { - committees.insert(msg.e3_id.clone(), msg.committee.clone()); + committees.insert(msg.e3_id.clone(), Committee::new(msg.committee.clone())); Ok(committees) }) }) } } + +impl Handler> for Sortition { + type Result = (); + + fn handle( + &mut self, + msg: TypedEvent, + _ctx: &mut Self::Context, + ) -> Self::Result { + let (data, ec) = msg.into_components(); + + // Only process raw events from chain (party_id not yet resolved). + // Events we re-publish with party_id set will also arrive here; ignore them. + if data.party_id.is_some() { + return; + } + + trap(EType::Sortition, &self.bus.with_ec(&ec), || { + let node_addr = data.node.to_string(); + + let Some(committee) = self.get_committee(&data.e3_id) else { + warn!( + "CommitteeMemberExpelled for node {} but no finalized committee found for e3_id={}. \ + The committee should always be finalized before expulsions.", + node_addr, data.e3_id + ); + return Ok(()); + }; + + let Some(party_id) = committee.party_id_for(&node_addr) else { + warn!( + "Expelled node {} not found in committee for e3_id={}", + node_addr, data.e3_id + ); + return Ok(()); + }; + + info!( + "Sortition: resolved expelled node {} to party_id={} for e3_id={}, re-publishing enriched event", + node_addr, party_id, data.e3_id + ); + + // Re-publish the event with party_id set to downstream actors + self.bus.publish( + CommitteeMemberExpelled { + party_id: Some(party_id), + ..data + }, + ec, + )?; + + Ok(()) + }) + } +} diff --git a/crates/zk-helpers/src/ciphernodes_committee.rs b/crates/zk-helpers/src/ciphernodes_committee.rs index da5a706e55..f1a912296a 100644 --- a/crates/zk-helpers/src/ciphernodes_committee.rs +++ b/crates/zk-helpers/src/ciphernodes_committee.rs @@ -38,18 +38,16 @@ impl CiphernodesCommitteeSize { h: 5, threshold: 2, }, - _ => unreachable!(), + CiphernodesCommitteeSize::Medium => CiphernodesCommittee { + n: 10, + h: 8, + threshold: 4, + }, + CiphernodesCommitteeSize::Large => CiphernodesCommittee { + n: 20, + h: 15, + threshold: 7, + }, } - // @todo add the other committee sizes - // CiphernodesCommitteeSize::Medium => CiphernodesCommittee { - // n: 5, - // h: 5, - // threshold: 2, - // }, - // CiphernodesCommitteeSize::Large => CiphernodesCommittee { - // n: 5, - // h: 5, - // threshold: 2, - // }, } } diff --git a/crates/zk-prover/src/actors/proof_verification.rs b/crates/zk-prover/src/actors/proof_verification.rs index c0fe9ae3c4..ee3b7f8e10 100644 --- a/crates/zk-prover/src/actors/proof_verification.rs +++ b/crates/zk-prover/src/actors/proof_verification.rs @@ -4,20 +4,9 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -//! Core business logic actor for verifying received encryption keys. -//! -//! This actor verifies `EncryptionKeyReceived` events and converts them -//! to `EncryptionKeyCreated` events after validation. -//! -//! ## Signature Verification -//! -//! Every received key must carry a [`SignedProofPayload`]. This actor: -//! 1. Recovers the address from the ECDSA signature. -//! 2. Delegates the ZK proof to `ZkActor` for verification. -//! 3. On ZK failure, emits [`SignedProofFailed`] with the full evidence bundle -//! and [`E3Failed`] to stop the E3 computation. -//! -//! Keys without a signed proof are rejected outright. +//! Verifies `EncryptionKeyReceived` events: recovers ECDSA address, delegates +//! ZK proof to `ZkActor`, and on failure emits [`SignedProofFailed`] for +//! on-chain fault attribution. use std::collections::HashMap; use std::sync::Arc; @@ -25,14 +14,13 @@ use std::sync::Arc; use actix::{Actor, Addr, AsyncContext, Context, Handler, Message, Recipient}; use alloy::primitives::Address; use e3_events::{ - BusHandle, E3Failed, E3Stage, E3id, EnclaveEvent, EnclaveEventData, EncryptionKey, - EncryptionKeyCreated, EncryptionKeyReceived, EventContext, EventPublisher, EventSubscriber, - EventType, FailureReason, Proof, Sequenced, SignedProofFailed, SignedProofPayload, TypedEvent, + BusHandle, E3id, EnclaveEvent, EnclaveEventData, EncryptionKey, EncryptionKeyCreated, + EncryptionKeyReceived, EventContext, EventPublisher, EventSubscriber, EventType, Proof, + Sequenced, SignedProofFailed, SignedProofPayload, TypedEvent, }; use e3_utils::NotifySync; use tracing::{error, info, warn}; -/// Request to verify a ZK proof. #[derive(Debug, Message)] #[rtype(result = "()")] pub struct ZkVerificationRequest { @@ -42,7 +30,6 @@ pub struct ZkVerificationRequest { pub sender: Recipient>, } -/// Response from ZK proof verification with context. #[derive(Debug, Clone, Message)] #[rtype(result = "()")] pub struct ZkVerificationResponse { @@ -52,23 +39,15 @@ pub struct ZkVerificationResponse { pub key: Arc, } -/// Tracks a pending verification including the signed payload for fault evidence. #[derive(Clone, Debug)] struct PendingVerification { signed_payload: SignedProofPayload, recovered_signer: Address, } -/// Core actor that handles encryption key verification. -/// -/// Requires every received key to carry a [`SignedProofPayload`]. -/// On ZK verification failure, emits both [`SignedProofFailed`] (for fault -/// attribution) and [`E3Failed`] (to stop the E3 computation). pub struct ProofVerificationActor { bus: BusHandle, verifier: Recipient>, - /// Tracks signed payloads for keys currently being verified, - /// keyed by `(e3_id, party_id)`. pending: HashMap<(E3id, u64), PendingVerification>, } @@ -104,7 +83,6 @@ impl ProofVerificationActor { return; }; - // Signed proofs are mandatory — reject keys without a signed payload let signed = match &msg.key.signed_payload { Some(signed) => signed.clone(), None => { @@ -116,7 +94,6 @@ impl ProofVerificationActor { } }; - // Recover the address from the signature let recovered_address = match signed.recover_address() { Ok(addr) => { info!( @@ -134,7 +111,6 @@ impl ProofVerificationActor { } }; - // Store the signed payload so we can reference it in the verification response self.pending.insert( (msg.e3_id.clone(), msg.key.party_id), PendingVerification { @@ -230,7 +206,6 @@ impl Handler> for ProofVerificationActor { msg.key.party_id, error_msg ); - // Emit SignedProofFailed for fault attribution if let Some(PendingVerification { signed_payload, recovered_signer, @@ -253,17 +228,11 @@ impl Handler> for ProofVerificationActor { } } - // Stop the E3 computation — proof verification failure is fatal - if let Err(err) = self.bus.publish( - E3Failed { - e3_id: msg.e3_id, - failed_at_stage: E3Stage::CommitteeFinalized, - reason: FailureReason::VerificationFailed, - }, - ec, - ) { - error!("Failed to publish E3Failed: {err}"); - } + // NOTE: We do NOT emit E3Failed here. The on-chain SlashingManager + // will expel the faulting node and check if the committee drops below + // threshold. If it does, the contract emits E3Failed on-chain, which + // the EVM reader picks up and propagates to all actors. If the committee + // is still above threshold, the DKG continues with N-1 nodes. } } } diff --git a/crates/zk-prover/tests/slashing_integration_tests.rs b/crates/zk-prover/tests/slashing_integration_tests.rs new file mode 100644 index 0000000000..9ba8871bb4 --- /dev/null +++ b/crates/zk-prover/tests/slashing_integration_tests.rs @@ -0,0 +1,981 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +//! Slashing integration tests: complete flow from proof generation → +//! operator signing → evidence encoding → on-chain SlashingManager verification. +//! +//! ## What these tests prove +//! +//! 1. **Signing format alignment**: Rust `ProofPayload.digest()` produces the +//! exact structured hash that `SlashingManager.proposeSlash()` reconstructs. +//! 2. **Evidence encoding**: `encode_fault_evidence()` output is correctly +//! decoded by the Solidity `abi.decode` in `proposeSlash()`. +//! 3. **ECDSA recovery**: Signatures created with alloy's `sign_message_sync` +//! are correctly recovered on-chain via `ECDSA.recover(toEthSignedMessageHash(...))`. +//! 4. **Complete slashing flow**: Valid proofs revert with `ProofIsValid()`, +//! wrong signers revert with `SignerIsNotOperator()`, and invalid proofs +//! result in successful slash execution. +//! +//! ## Prerequisites +//! +//! On-chain tests require: +//! - `anvil` on PATH (from Foundry) +//! - Compiled Hardhat artifacts: `cd packages/enclave-contracts && npx hardhat compile` +//! +//! Run with: `cargo test -p e3-zk-prover --test slashing_integration_tests` + +mod common; + +use alloy::{ + network::TransactionBuilder, + primitives::{keccak256, Address, Bytes, FixedBytes, U256}, + providers::{Provider, ProviderBuilder}, + rpc::types::TransactionRequest, + signers::local::PrivateKeySigner, + sol, + sol_types::SolValue, +}; +use common::find_anvil; +use e3_events::{ + encode_fault_evidence, CircuitName, E3id, Proof, ProofPayload, ProofType, SignedProofFailed, + SignedProofPayload, +}; +use e3_utils::utility_types::ArcBytes; +use std::path::PathBuf; + +// ── Contract ABI definitions (bytecodes loaded from Hardhat artifacts at runtime) ── + +sol! { + #[sol(rpc)] + contract SlashingManager { + struct SlashPolicy { + uint256 ticketPenalty; + uint256 licensePenalty; + bool requiresProof; + address proofVerifier; + bool banNode; + uint256 appealWindow; + bool enabled; + bool affectsCommittee; + uint8 failureReason; + } + + function proposeSlash(uint256 e3Id, address operator, bytes32 reason, bytes calldata proof) external returns (uint256 proposalId); + function setSlashPolicy(bytes32 reason, SlashPolicy calldata policy) external; + function totalProposals() external view returns (uint256); + function isBanned(address node) external view returns (bool); + + error ProofIsValid(); + error SignerIsNotOperator(); + error OperatorNotInCommittee(); + error VerifierMismatch(); + } + + #[sol(rpc)] + contract MockCircuitVerifier { + function setReturnValue(bool _returnValue) external; + } + + #[sol(rpc)] + contract MockCiphernodeRegistry { + function setCommitteeNodes(uint256 e3Id, address[] calldata nodes) external; + } +} + +// ── Helpers ── + +/// No-op contract deployment bytecode. +/// +/// Deploys a contract whose runtime is a single STOP opcode. +/// All calls to this contract succeed with empty return data, making it +/// suitable as a mock for any interface that only has void-returning functions +/// (e.g., IBondingRegistry.slashTicketBalance, IEnclave.onE3Failed). +const NOOP_DEPLOY_BYTECODE: &[u8] = &[ + 0x60, 0x01, // PUSH1 0x01 (runtime size) + 0x60, 0x0c, // PUSH1 0x0c (offset of runtime in init code) + 0x60, 0x00, // PUSH1 0x00 (memory destination) + 0x39, // CODECOPY + 0x60, 0x01, // PUSH1 0x01 (return size) + 0x60, 0x00, // PUSH1 0x00 (return offset) + 0xf3, // RETURN + 0x00, // -- runtime: STOP -- +]; + +fn contracts_artifacts_dir() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../../packages/enclave-contracts/artifacts/contracts") +} + +fn read_artifact_bytecode(subpath: &str) -> Option> { + let path = contracts_artifacts_dir().join(subpath); + let json_str = std::fs::read_to_string(&path).ok()?; + let json: serde_json::Value = serde_json::from_str(&json_str).ok()?; + let hex_str = json["bytecode"].as_str()?; + let clean = hex_str.strip_prefix("0x").unwrap_or(hex_str); + hex::decode(clean).ok() +} + +/// Load all three contract bytecodes, returning None if any are missing. +fn load_slashing_artifacts() -> Option<(Vec, Vec, Vec)> { + let sm = read_artifact_bytecode("slashing/SlashingManager.sol/SlashingManager.json")?; + let mv = read_artifact_bytecode("test/MockSlashingVerifier.sol/MockCircuitVerifier.json")?; + let mr = read_artifact_bytecode("test/MockCiphernodeRegistry.sol/MockCiphernodeRegistry.json")?; + Some((sm, mv, mr)) +} + +/// Deploy a contract on the connected provider. +/// `creation_bytecode` is the compiled init code; `constructor_args` is appended (ABI-encoded). +async fn deploy_contract( + provider: &impl Provider, + creation_bytecode: &[u8], + constructor_args: &[u8], +) -> Address { + let mut deploy_data = creation_bytecode.to_vec(); + deploy_data.extend_from_slice(constructor_args); + let tx = TransactionRequest::default().with_deploy_code(Bytes::from(deploy_data)); + let receipt = provider + .send_transaction(tx) + .await + .expect("failed to send deploy tx") + .get_receipt() + .await + .expect("failed to get deploy receipt"); + receipt + .contract_address + .expect("deploy receipt missing contract address") +} + +/// Create a test ProofPayload with the given parameters. +fn test_proof_payload(e3_id: u64, chain_id: u64) -> ProofPayload { + ProofPayload { + e3_id: E3id::new(&e3_id.to_string(), chain_id), + proof_type: ProofType::T0PkBfv, + proof: Proof::new( + CircuitName::PkBfv, + ArcBytes::from_bytes(&[0xde, 0xad, 0xbe, 0xef]), + // One 32-byte public input (padded zero) + ArcBytes::from_bytes(&[0u8; 32]), + ), + } +} + +// ════════════════════════════════════════════════════════════════════════════ +// Pure Rust tests — no Anvil or artifacts required +// ════════════════════════════════════════════════════════════════════════════ + +/// Verifies the typehash constant matches the keccak256 of the type string. +#[test] +fn test_proof_payload_typehash() { + let expected: [u8; 32] = keccak256( + "ProofPayload(uint256 chainId,uint256 e3Id,uint256 proofType,bytes zkProof,bytes publicSignals)", + ) + .into(); + assert_eq!( + ProofPayload::typehash(), + expected, + "typehash should match keccak256 of the type string" + ); +} + +/// Verifies that digest() uses the structured typehash format with hashed dynamic fields. +#[test] +fn test_proof_payload_digest_matches_manual_computation() { + let payload = test_proof_payload(1, 42); + let digest = payload.digest().expect("digest should succeed"); + + // Manually compute expected digest + let typehash = keccak256( + "ProofPayload(uint256 chainId,uint256 e3Id,uint256 proofType,bytes zkProof,bytes publicSignals)", + ); + let expected_encoded = ( + typehash, + U256::from(42u64), // chainId + U256::from(1u64), // e3Id + U256::from(0u8), // proofType (T0PkBfv = 0) + keccak256(&[0xde, 0xad, 0xbe, 0xef]), // keccak256(zkProof) + keccak256(&[0u8; 32]), // keccak256(publicSignals) + ) + .abi_encode(); + let expected_digest: [u8; 32] = keccak256(&expected_encoded).into(); + + assert_eq!( + digest, expected_digest, + "digest should match manual computation" + ); +} + +/// Verifies sign → recover roundtrip with the structured digest format. +#[test] +fn test_signing_roundtrip_with_structured_digest() { + let signer: PrivateKeySigner = + "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" + .parse() + .unwrap(); + + let payload = test_proof_payload(42, 31337); + let signed = SignedProofPayload::sign(payload, &signer).expect("signing should succeed"); + let recovered = signed.recover_address().expect("recovery should succeed"); + + assert_eq!( + recovered, + signer.address(), + "recovered address should match signer" + ); +} + +/// Verifies that different payloads produce different digests (no collisions). +#[test] +fn test_different_payloads_different_digests() { + let p1 = test_proof_payload(1, 42); + let p2 = test_proof_payload(2, 42); // different e3Id + let mut p3 = test_proof_payload(1, 42); + p3.proof_type = ProofType::T1PkGeneration; // different proofType + + let d1 = p1.digest().unwrap(); + let d2 = p2.digest().unwrap(); + let d3 = p3.digest().unwrap(); + + assert_ne!(d1, d2, "different e3Ids should produce different digests"); + assert_ne!( + d1, d3, + "different proofTypes should produce different digests" + ); +} + +/// Verifies that encode_fault_evidence() produces correctly structured ABI encoding. +#[test] +fn test_encode_fault_evidence_structure() { + let signer: PrivateKeySigner = + "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" + .parse() + .unwrap(); + let verifier_addr: Address = "0x1234567890abcdef1234567890abcdef12345678" + .parse() + .unwrap(); + + let payload = test_proof_payload(42, 31337); + let signed = SignedProofPayload::sign(payload, &signer).expect("signing should succeed"); + + let failed = SignedProofFailed { + e3_id: E3id::new("42", 31337), + faulting_node: signer.address(), + proof_type: ProofType::T0PkBfv, + signed_payload: signed.clone(), + }; + + let evidence = encode_fault_evidence(&failed, verifier_addr); + + // Decode and verify structure: (bytes, bytes32[], bytes, uint256, uint256, address) + type EvidenceTuple = (Bytes, Vec>, Bytes, U256, U256, Address); + let decoded = EvidenceTuple::abi_decode_params(&evidence).expect("evidence should ABI-decode"); + + let (zk_proof, public_inputs, sig, chain_id, proof_type, verifier) = decoded; + + assert_eq!(&zk_proof[..], &[0xde, 0xad, 0xbe, 0xef], "zkProof mismatch"); + assert_eq!(public_inputs.len(), 1, "should have 1 public input"); + assert_eq!( + public_inputs[0], + FixedBytes::from([0u8; 32]), + "public input value mismatch" + ); + assert_eq!(&sig[..], &signed.signature[..], "signature bytes mismatch"); + assert_eq!(chain_id, U256::from(31337u64), "chainId mismatch"); + assert_eq!(proof_type, U256::from(0u8), "proofType mismatch"); + assert_eq!(verifier, verifier_addr, "verifier address mismatch"); +} + +/// Verifies that the digest format matches what Solidity would compute. +/// +/// This is the critical cross-language test: if this passes, then: +/// `keccak256(abi.encode(PROOF_PAYLOAD_TYPEHASH, chainId, e3Id, proofType, keccak256(zkProof), keccak256(abi.encodePacked(publicInputs))))` +/// in Solidity produces the same bytes32 as `ProofPayload::digest()` in Rust. +#[test] +fn test_digest_matches_solidity_encoding() { + let payload = test_proof_payload(42, 31337); + let digest = payload.digest().expect("digest should succeed"); + + // Simulate what Solidity does step by step: + // + // bytes32 messageHash = keccak256(abi.encode( + // PROOF_PAYLOAD_TYPEHASH, // bytes32 + // chainId, // uint256 + // e3Id, // uint256 + // proofType, // uint256 + // keccak256(zkProof), // bytes32 + // keccak256(abi.encodePacked(publicInputs)) // bytes32 + // )); + // + // For publicInputs = [bytes32(0)]: + // abi.encodePacked(publicInputs) = 0x0000...0000 (32 bytes) + // which is the same as the raw publicSignals bytes + + let typehash = keccak256( + "ProofPayload(uint256 chainId,uint256 e3Id,uint256 proofType,bytes zkProof,bytes publicSignals)", + ); + + // abi.encode of all-static types: each word is 32 bytes, no offsets + let mut solidity_encoded = Vec::with_capacity(192); + solidity_encoded.extend_from_slice(typehash.as_ref()); // bytes32 + solidity_encoded.extend_from_slice(&U256::from(31337u64).to_be_bytes::<32>()); // uint256 chainId + solidity_encoded.extend_from_slice(&U256::from(42u64).to_be_bytes::<32>()); // uint256 e3Id + solidity_encoded.extend_from_slice(&U256::from(0u8).to_be_bytes::<32>()); // uint256 proofType + solidity_encoded.extend_from_slice(keccak256(&[0xde, 0xad, 0xbe, 0xef]).as_ref()); // keccak256(zkProof) + + // For publicInputs = [bytes32(0)]: + // Solidity: keccak256(abi.encodePacked(publicInputs)) = keccak256(bytes32(0)) + // Rust: keccak256(public_signals) = keccak256([0u8; 32]) + // These must be the same! + let sol_public_inputs_hash = keccak256(&[0u8; 32]); + solidity_encoded.extend_from_slice(sol_public_inputs_hash.as_ref()); // keccak256(publicSignals) + + let solidity_digest: [u8; 32] = keccak256(&solidity_encoded).into(); + + assert_eq!( + digest, solidity_digest, + "Rust digest must exactly match Solidity messageHash reconstruction" + ); +} + +// ════════════════════════════════════════════════════════════════════════════ +// On-chain integration tests — require Anvil + compiled Hardhat artifacts +// ════════════════════════════════════════════════════════════════════════════ + +/// **Complete flow**: operator signs proof → evidence encoded → SlashingManager +/// reconstructs digest, recovers signer, verifies committee membership, and +/// checks ZK proof validity. +/// +/// With MockCircuitVerifier returning TRUE (proof is valid), the contract +/// reverts with `ProofIsValid()`. This proves the full Rust→Solidity signing +/// pipeline works correctly. +#[tokio::test] +async fn test_onchain_valid_proof_reverts_proof_is_valid() { + if !find_anvil().await { + println!("skipping: anvil not found on PATH"); + return; + } + + let (sm_bytecode, mv_bytecode, mr_bytecode) = match load_slashing_artifacts() { + Some(artifacts) => artifacts, + None => { + println!( + "skipping: contract artifacts not found \ + (run `npx hardhat compile` in packages/enclave-contracts)" + ); + return; + } + }; + + let provider = ProviderBuilder::new().connect_anvil_with_wallet(); + let chain_id = provider.get_chain_id().await.unwrap(); + let accounts = provider.get_accounts().await.unwrap(); + let admin = accounts[0]; + + // Operator uses a separate key (not an Anvil pre-funded account) + let operator_signer = PrivateKeySigner::random(); + let operator_addr = operator_signer.address(); + + // Deploy infrastructure contracts + let noop_addr = deploy_contract(&provider, NOOP_DEPLOY_BYTECODE, &[]).await; + let mock_verifier_addr = deploy_contract(&provider, &mv_bytecode, &[]).await; + let mock_registry_addr = deploy_contract(&provider, &mr_bytecode, &[]).await; + + // Deploy SlashingManager(admin, bondingRegistry, ciphernodeRegistry, enclave) + let sm_args = (admin, noop_addr, mock_registry_addr, noop_addr).abi_encode(); + let sm_addr = deploy_contract(&provider, &sm_bytecode, &sm_args).await; + + println!("deployed: SlashingManager={sm_addr}, MockVerifier={mock_verifier_addr}, MockRegistry={mock_registry_addr}"); + println!("operator: {operator_addr} (chain_id: {chain_id})"); + + // Bind contract instances + let slashing_mgr = SlashingManager::new(sm_addr, &provider); + let mock_verifier = MockCircuitVerifier::new(mock_verifier_addr, &provider); + let mock_registry = MockCiphernodeRegistry::new(mock_registry_addr, &provider); + + // ── Setup: slash policy + committee ── + + let reason: FixedBytes<32> = keccak256("E3_BAD_DKG_PROOF"); + let e3_id: u64 = 42; + + slashing_mgr + .setSlashPolicy( + reason, + SlashingManager::SlashPolicy { + ticketPenalty: U256::from(50_000_000u64), + licensePenalty: U256::from(100_000_000_000_000_000_000u128), + requiresProof: true, + proofVerifier: mock_verifier_addr, + banNode: false, + appealWindow: U256::ZERO, + enabled: true, + affectsCommittee: false, + failureReason: 0u8, + }, + ) + .send() + .await + .unwrap() + .get_receipt() + .await + .unwrap(); + + mock_registry + .setCommitteeNodes(U256::from(e3_id), vec![operator_addr]) + .send() + .await + .unwrap() + .get_receipt() + .await + .unwrap(); + + // MockCircuitVerifier returns TRUE → proof is valid → no fault + mock_verifier + .setReturnValue(true) + .send() + .await + .unwrap() + .get_receipt() + .await + .unwrap(); + + // ── Operator signs proof (Rust-side) ── + + let payload = ProofPayload { + e3_id: E3id::new(&e3_id.to_string(), chain_id), + proof_type: ProofType::T0PkBfv, + proof: Proof::new( + CircuitName::PkBfv, + ArcBytes::from_bytes(&[0xde, 0xad, 0xbe, 0xef]), + ArcBytes::from_bytes(&[0u8; 32]), + ), + }; + + let signed = + SignedProofPayload::sign(payload, &operator_signer).expect("signing should succeed"); + + // ── FaultSubmitter encodes evidence (Rust-side) ── + + let failed = SignedProofFailed { + e3_id: E3id::new(&e3_id.to_string(), chain_id), + faulting_node: operator_addr, + proof_type: ProofType::T0PkBfv, + signed_payload: signed, + }; + + let evidence = encode_fault_evidence(&failed, mock_verifier_addr); + + // ── Submit to SlashingManager (on-chain) ── + + let result = slashing_mgr + .proposeSlash( + U256::from(e3_id), + operator_addr, + reason, + Bytes::from(evidence), + ) + .call() + .await; + + // Should revert with ProofIsValid — the proof is valid, so there's no fault + assert!( + result.is_err(), + "should revert because the proof is valid (no fault to slash)" + ); + + let err_string = format!("{:?}", result.unwrap_err()); + assert!( + err_string.contains("ProofIsValid") || err_string.contains("0x5b718c5b"), + "expected ProofIsValid revert, got: {err_string}" + ); + + println!("PASS: valid proof correctly reverts with ProofIsValid — Rust→Solidity signing alignment verified"); +} + +/// Tests that a wrong signer (attacker) cannot slash an arbitrary operator. +/// +/// The attacker signs the proof with their own key but submits it as evidence +/// against a different operator. The contract should reject because the +/// recovered signer doesn't match the target operator. +#[tokio::test] +async fn test_onchain_wrong_signer_reverts_signer_is_not_operator() { + if !find_anvil().await { + println!("skipping: anvil not found on PATH"); + return; + } + + let (sm_bytecode, mv_bytecode, mr_bytecode) = match load_slashing_artifacts() { + Some(artifacts) => artifacts, + None => { + println!("skipping: contract artifacts not found"); + return; + } + }; + + let provider = ProviderBuilder::new().connect_anvil_with_wallet(); + let chain_id = provider.get_chain_id().await.unwrap(); + let accounts = provider.get_accounts().await.unwrap(); + let admin = accounts[0]; + + let attacker_signer = PrivateKeySigner::random(); + let victim_addr: Address = "0x1111111111111111111111111111111111111111" + .parse() + .unwrap(); + + let noop_addr = deploy_contract(&provider, NOOP_DEPLOY_BYTECODE, &[]).await; + let mock_verifier_addr = deploy_contract(&provider, &mv_bytecode, &[]).await; + let mock_registry_addr = deploy_contract(&provider, &mr_bytecode, &[]).await; + + let sm_args = (admin, noop_addr, mock_registry_addr, noop_addr).abi_encode(); + let sm_addr = deploy_contract(&provider, &sm_bytecode, &sm_args).await; + + let slashing_mgr = SlashingManager::new(sm_addr, &provider); + let mock_registry = MockCiphernodeRegistry::new(mock_registry_addr, &provider); + + let reason: FixedBytes<32> = keccak256("E3_BAD_DKG_PROOF"); + let e3_id: u64 = 42; + + slashing_mgr + .setSlashPolicy( + reason, + SlashingManager::SlashPolicy { + ticketPenalty: U256::from(50_000_000u64), + licensePenalty: U256::from(100_000_000_000_000_000_000u128), + requiresProof: true, + proofVerifier: mock_verifier_addr, + banNode: false, + appealWindow: U256::ZERO, + enabled: true, + affectsCommittee: false, + failureReason: 0u8, + }, + ) + .send() + .await + .unwrap() + .get_receipt() + .await + .unwrap(); + + // Add VICTIM to committee (not the attacker) + mock_registry + .setCommitteeNodes(U256::from(e3_id), vec![victim_addr]) + .send() + .await + .unwrap() + .get_receipt() + .await + .unwrap(); + + // Attacker signs the proof with their own key + let payload = ProofPayload { + e3_id: E3id::new(&e3_id.to_string(), chain_id), + proof_type: ProofType::T0PkBfv, + proof: Proof::new( + CircuitName::PkBfv, + ArcBytes::from_bytes(&[0xde, 0xad]), + ArcBytes::from_bytes(&[0u8; 32]), + ), + }; + let signed = + SignedProofPayload::sign(payload, &attacker_signer).expect("signing should succeed"); + + let failed = SignedProofFailed { + e3_id: E3id::new(&e3_id.to_string(), chain_id), + faulting_node: attacker_signer.address(), + proof_type: ProofType::T0PkBfv, + signed_payload: signed, + }; + + let evidence = encode_fault_evidence(&failed, mock_verifier_addr); + + // Submit evidence targeting the VICTIM, but signed by the ATTACKER + let result = slashing_mgr + .proposeSlash( + U256::from(e3_id), + victim_addr, // <-- target is victim, not the actual signer + reason, + Bytes::from(evidence), + ) + .call() + .await; + + assert!(result.is_err(), "should revert because signer != operator"); + + let err_string = format!("{:?}", result.unwrap_err()); + assert!( + err_string.contains("SignerIsNotOperator") || err_string.contains("0xcd659038"), + "expected SignerIsNotOperator revert, got: {err_string}" + ); + + println!("PASS: wrong signer correctly reverts — V-001 protection verified"); +} + +/// Tests that operators not in the committee cannot be slashed. +#[tokio::test] +async fn test_onchain_non_committee_member_reverts() { + if !find_anvil().await { + println!("skipping: anvil not found on PATH"); + return; + } + + let (sm_bytecode, mv_bytecode, mr_bytecode) = match load_slashing_artifacts() { + Some(artifacts) => artifacts, + None => { + println!("skipping: contract artifacts not found"); + return; + } + }; + + let provider = ProviderBuilder::new().connect_anvil_with_wallet(); + let chain_id = provider.get_chain_id().await.unwrap(); + let accounts = provider.get_accounts().await.unwrap(); + let admin = accounts[0]; + + let operator_signer = PrivateKeySigner::random(); + let operator_addr = operator_signer.address(); + + let noop_addr = deploy_contract(&provider, NOOP_DEPLOY_BYTECODE, &[]).await; + let mock_verifier_addr = deploy_contract(&provider, &mv_bytecode, &[]).await; + let mock_registry_addr = deploy_contract(&provider, &mr_bytecode, &[]).await; + + let sm_args = (admin, noop_addr, mock_registry_addr, noop_addr).abi_encode(); + let sm_addr = deploy_contract(&provider, &sm_bytecode, &sm_args).await; + + let slashing_mgr = SlashingManager::new(sm_addr, &provider); + + let reason: FixedBytes<32> = keccak256("E3_BAD_DKG_PROOF"); + let e3_id: u64 = 42; + + slashing_mgr + .setSlashPolicy( + reason, + SlashingManager::SlashPolicy { + ticketPenalty: U256::from(50_000_000u64), + licensePenalty: U256::from(100_000_000_000_000_000_000u128), + requiresProof: true, + proofVerifier: mock_verifier_addr, + banNode: false, + appealWindow: U256::ZERO, + enabled: true, + affectsCommittee: false, + failureReason: 0u8, + }, + ) + .send() + .await + .unwrap() + .get_receipt() + .await + .unwrap(); + + // NOTE: We do NOT add the operator to the committee + + // Operator signs a proof + let payload = ProofPayload { + e3_id: E3id::new(&e3_id.to_string(), chain_id), + proof_type: ProofType::T0PkBfv, + proof: Proof::new( + CircuitName::PkBfv, + ArcBytes::from_bytes(&[0xab, 0xcd]), + ArcBytes::from_bytes(&[0u8; 32]), + ), + }; + let signed = + SignedProofPayload::sign(payload, &operator_signer).expect("signing should succeed"); + + let failed = SignedProofFailed { + e3_id: E3id::new(&e3_id.to_string(), chain_id), + faulting_node: operator_addr, + proof_type: ProofType::T0PkBfv, + signed_payload: signed, + }; + + let evidence = encode_fault_evidence(&failed, mock_verifier_addr); + + let result = slashing_mgr + .proposeSlash( + U256::from(e3_id), + operator_addr, + reason, + Bytes::from(evidence), + ) + .call() + .await; + + assert!( + result.is_err(), + "should revert because operator is not in committee" + ); + + let err_string = format!("{:?}", result.unwrap_err()); + assert!( + err_string.contains("OperatorNotInCommittee") || err_string.contains("0x7353fac5"), + "expected OperatorNotInCommittee revert, got: {err_string}" + ); + + println!("PASS: non-committee member correctly reverts — committee check verified"); +} + +/// Tests the complete slash execution flow: invalid proof → fault confirmed → slash executed. +/// +/// Uses a NOOP contract as BondingRegistry so that `slashTicketBalance` and +/// `slashLicenseBond` calls succeed silently, allowing the full flow to complete. +#[tokio::test] +async fn test_onchain_invalid_proof_executes_slash() { + if !find_anvil().await { + println!("skipping: anvil not found on PATH"); + return; + } + + let (sm_bytecode, mv_bytecode, mr_bytecode) = match load_slashing_artifacts() { + Some(artifacts) => artifacts, + None => { + println!("skipping: contract artifacts not found"); + return; + } + }; + + let provider = ProviderBuilder::new().connect_anvil_with_wallet(); + let chain_id = provider.get_chain_id().await.unwrap(); + let accounts = provider.get_accounts().await.unwrap(); + let admin = accounts[0]; + + let operator_signer = PrivateKeySigner::random(); + let operator_addr = operator_signer.address(); + + let noop_addr = deploy_contract(&provider, NOOP_DEPLOY_BYTECODE, &[]).await; + let mock_verifier_addr = deploy_contract(&provider, &mv_bytecode, &[]).await; + let mock_registry_addr = deploy_contract(&provider, &mr_bytecode, &[]).await; + + // Use noop as both bondingRegistry and enclave + let sm_args = (admin, noop_addr, mock_registry_addr, noop_addr).abi_encode(); + let sm_addr = deploy_contract(&provider, &sm_bytecode, &sm_args).await; + + let slashing_mgr = SlashingManager::new(sm_addr, &provider); + let mock_verifier = MockCircuitVerifier::new(mock_verifier_addr, &provider); + let mock_registry = MockCiphernodeRegistry::new(mock_registry_addr, &provider); + + let reason: FixedBytes<32> = keccak256("E3_BAD_DKG_PROOF"); + let e3_id: u64 = 42; + + slashing_mgr + .setSlashPolicy( + reason, + SlashingManager::SlashPolicy { + ticketPenalty: U256::from(50_000_000u64), + licensePenalty: U256::from(100_000_000_000_000_000_000u128), + requiresProof: true, + proofVerifier: mock_verifier_addr, + banNode: false, + appealWindow: U256::ZERO, + enabled: true, + affectsCommittee: false, + failureReason: 0u8, + }, + ) + .send() + .await + .unwrap() + .get_receipt() + .await + .unwrap(); + + mock_registry + .setCommitteeNodes(U256::from(e3_id), vec![operator_addr]) + .send() + .await + .unwrap() + .get_receipt() + .await + .unwrap(); + + // MockCircuitVerifier returns FALSE → proof is invalid → fault confirmed + mock_verifier + .setReturnValue(false) + .send() + .await + .unwrap() + .get_receipt() + .await + .unwrap(); + + // Operator signs a (bad) proof + let payload = ProofPayload { + e3_id: E3id::new(&e3_id.to_string(), chain_id), + proof_type: ProofType::T0PkBfv, + proof: Proof::new( + CircuitName::PkBfv, + ArcBytes::from_bytes(&[0xba, 0xd0, 0xba, 0xd0]), + ArcBytes::from_bytes(&[0u8; 32]), + ), + }; + let signed = + SignedProofPayload::sign(payload, &operator_signer).expect("signing should succeed"); + + let failed = SignedProofFailed { + e3_id: E3id::new(&e3_id.to_string(), chain_id), + faulting_node: operator_addr, + proof_type: ProofType::T0PkBfv, + signed_payload: signed, + }; + + let evidence = encode_fault_evidence(&failed, mock_verifier_addr); + + // Verify proposal count before + let proposals_before = slashing_mgr + .totalProposals() + .call() + .await + .expect("totalProposals call failed"); + assert_eq!( + proposals_before, + U256::ZERO, + "should have 0 proposals before" + ); + + // Submit slash — should succeed (invalid proof = fault confirmed) + let receipt = slashing_mgr + .proposeSlash( + U256::from(e3_id), + operator_addr, + reason, + Bytes::from(evidence), + ) + .send() + .await + .expect("proposeSlash tx should not fail to send") + .get_receipt() + .await + .expect("proposeSlash receipt should be obtainable"); + + assert!( + receipt.status(), + "proposeSlash transaction should succeed (invalid proof = fault confirmed, slash executed)" + ); + + // Verify proposal was created and executed + let proposals_after = slashing_mgr + .totalProposals() + .call() + .await + .expect("totalProposals call failed"); + assert_eq!( + proposals_after, + U256::from(1u64), + "should have 1 proposal after slash" + ); + + println!("PASS: invalid proof correctly triggers slash execution — full flow verified"); +} + +/// Tests that verifier mismatch is detected (verifier-upgrade protection). +/// +/// If the evidence references an old verifier address but the policy has been +/// updated to a new verifier, proposeSlash should revert with VerifierMismatch. +#[tokio::test] +async fn test_onchain_verifier_mismatch_reverts() { + if !find_anvil().await { + println!("skipping: anvil not found on PATH"); + return; + } + + let (sm_bytecode, mv_bytecode, mr_bytecode) = match load_slashing_artifacts() { + Some(artifacts) => artifacts, + None => { + println!("skipping: contract artifacts not found"); + return; + } + }; + + let provider = ProviderBuilder::new().connect_anvil_with_wallet(); + let chain_id = provider.get_chain_id().await.unwrap(); + let accounts = provider.get_accounts().await.unwrap(); + let admin = accounts[0]; + + let operator_signer = PrivateKeySigner::random(); + let operator_addr = operator_signer.address(); + + let noop_addr = deploy_contract(&provider, NOOP_DEPLOY_BYTECODE, &[]).await; + let mock_verifier_addr = deploy_contract(&provider, &mv_bytecode, &[]).await; + let mock_registry_addr = deploy_contract(&provider, &mr_bytecode, &[]).await; + + let sm_args = (admin, noop_addr, mock_registry_addr, noop_addr).abi_encode(); + let sm_addr = deploy_contract(&provider, &sm_bytecode, &sm_args).await; + + let slashing_mgr = SlashingManager::new(sm_addr, &provider); + + let reason: FixedBytes<32> = keccak256("E3_BAD_DKG_PROOF"); + let e3_id: u64 = 42; + + slashing_mgr + .setSlashPolicy( + reason, + SlashingManager::SlashPolicy { + ticketPenalty: U256::from(50_000_000u64), + licensePenalty: U256::from(100_000_000_000_000_000_000u128), + requiresProof: true, + proofVerifier: mock_verifier_addr, + banNode: false, + appealWindow: U256::ZERO, + enabled: true, + affectsCommittee: false, + failureReason: 0u8, + }, + ) + .send() + .await + .unwrap() + .get_receipt() + .await + .unwrap(); + + let payload = ProofPayload { + e3_id: E3id::new(&e3_id.to_string(), chain_id), + proof_type: ProofType::T0PkBfv, + proof: Proof::new( + CircuitName::PkBfv, + ArcBytes::from_bytes(&[0xab]), + ArcBytes::from_bytes(&[0u8; 32]), + ), + }; + let signed = + SignedProofPayload::sign(payload, &operator_signer).expect("signing should succeed"); + + let failed = SignedProofFailed { + e3_id: E3id::new(&e3_id.to_string(), chain_id), + faulting_node: operator_addr, + proof_type: ProofType::T0PkBfv, + signed_payload: signed, + }; + + // Encode evidence pointing to a DIFFERENT verifier (simulating stale evidence) + let stale_verifier: Address = "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef" + .parse() + .unwrap(); + let evidence = encode_fault_evidence(&failed, stale_verifier); + + let result = slashing_mgr + .proposeSlash( + U256::from(e3_id), + operator_addr, + reason, + Bytes::from(evidence), + ) + .call() + .await; + + assert!( + result.is_err(), + "should revert because verifier in evidence doesn't match policy" + ); + + let err_string = format!("{:?}", result.unwrap_err()); + assert!( + err_string.contains("VerifierMismatch") || err_string.contains("0x1c485278"), + "expected VerifierMismatch revert, got: {err_string}" + ); + + println!("PASS: verifier mismatch correctly reverts — verifier-upgrade protection verified"); +} diff --git a/examples/CRISP/client/src/App.tsx b/examples/CRISP/client/src/App.tsx index 335d3794d3..ad087a4442 100644 --- a/examples/CRISP/client/src/App.tsx +++ b/examples/CRISP/client/src/App.tsx @@ -45,6 +45,7 @@ const App: React.FC = () => { }) } })() + // eslint-disable-next-line react-hooks/exhaustive-deps }, []) return ( diff --git a/examples/CRISP/client/src/components/Cards/Card.tsx b/examples/CRISP/client/src/components/Cards/Card.tsx index f55705491f..e5ded8c8fb 100644 --- a/examples/CRISP/client/src/components/Cards/Card.tsx +++ b/examples/CRISP/client/src/components/Cards/Card.tsx @@ -4,7 +4,7 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -import React, { useEffect, useState } from 'react' +import React, { useMemo, useState } from 'react' interface CardProps { children: React.ReactNode @@ -17,22 +17,17 @@ interface CardProps { const Card: React.FC = ({ children, isActive, isDetails, checked, onChecked }) => { const [isClicked, setIsClicked] = useState(checked ?? false) - useEffect(() => { - setIsClicked(checked ?? false) - }, [checked]) + const derivedIsClicked = useMemo(() => { + if (isActive) return false + return checked ?? isClicked + }, [isActive, checked, isClicked]) const handleClick = () => { if (isDetails) return - if (onChecked) onChecked(!isClicked) - setIsClicked(!isClicked) + if (onChecked) onChecked(!derivedIsClicked) + setIsClicked(!derivedIsClicked) } - useEffect(() => { - if (isActive) { - setIsClicked(false) - } - }, [isActive]) - return (

= ({ children, isActive, isDetails, checked, onC ${!isDetails && 'shadow-md'} transform border-2 transition-all duration-300 ease-in-out - ${isClicked ? 'scale-105 border-lime-400' : ''} - ${isClicked ? 'border-lime-400' : 'border-slate-600/20'} - ${isClicked ? 'bg-white' : 'bg-slate-100'} + ${derivedIsClicked ? 'scale-105 border-lime-400' : ''} + ${derivedIsClicked ? 'border-lime-400' : 'border-slate-600/20'} + ${derivedIsClicked ? 'bg-white' : 'bg-slate-100'} ${!isDetails && 'hover:border-lime-300 hover:bg-white hover:shadow-lg'} flex w-full items-center justify-center `} diff --git a/examples/CRISP/client/src/components/Cards/PollCard.tsx b/examples/CRISP/client/src/components/Cards/PollCard.tsx index 25713063e0..235ebc0951 100644 --- a/examples/CRISP/client/src/components/Cards/PollCard.tsx +++ b/examples/CRISP/client/src/components/Cards/PollCard.tsx @@ -4,9 +4,9 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -import React, { useEffect, useState } from 'react' +import React, { useEffect, useMemo, useState } from 'react' import { useNavigate } from 'react-router-dom' -import { PollOption, PollResult } from '@/model/poll.model' +import { PollResult } from '@/model/poll.model' import VotesBadge from '@/components/VotesBadge' import PollCardResult from '@/components/Cards/PollCardResult' import { formatDate, markWinner } from '@/utils/methods' @@ -15,7 +15,6 @@ import { usePublicClient } from 'wagmi' const PollCard: React.FC = ({ roundId, options, totalVotes, date, endTime }) => { const navigate = useNavigate() - const [results, setResults] = useState(options) const [isActive, setIsActive] = useState(true) const { roundState, setPollResult, currentRoundId } = useVoteManagementContext() const client = usePublicClient() @@ -40,10 +39,7 @@ const PollCard: React.FC = ({ roundId, options, totalVotes, date, en return () => clearInterval(interval) }, [endTime, client, isActive]) - useEffect(() => { - const newPollOptions = markWinner(options) - setResults(newPollOptions) - }, [options]) + const results = useMemo(() => markWinner(options), [options]) const handleNavigation = () => { if (isActive && isCurrentRound) { diff --git a/examples/CRISP/client/src/components/CircularTiles.tsx b/examples/CRISP/client/src/components/CircularTiles.tsx index 8ccf12c0a3..00c73d5f69 100644 --- a/examples/CRISP/client/src/components/CircularTiles.tsx +++ b/examples/CRISP/client/src/components/CircularTiles.tsx @@ -4,17 +4,23 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. -import { memo } from 'react' +import { memo, useEffect, useState } from 'react' import CircularTile from './CircularTile' +const generateRotations = (count: number) => [...Array(count)].map(() => [0, 90, 180, 270][Math.floor(Math.random() * 4)]) + const CircularTiles = ({ count = 1, className }: { count?: number; className?: string }) => { + const [rotations, setRotations] = useState(() => generateRotations(count)) + + useEffect(() => { + setRotations(generateRotations(count)) + }, [count]) + return ( <> - {[...Array(count)].map((_i, index) => { - const rand_index = Math.floor(Math.random() * 4) - const rotation = [0, 90, 180, 270][rand_index] - return - })} + {rotations.map((rotation, index) => ( + + ))} ) } diff --git a/examples/CRISP/client/src/components/NavMenu.tsx b/examples/CRISP/client/src/components/NavMenu.tsx index 59034ebeb2..1f28b6062f 100644 --- a/examples/CRISP/client/src/components/NavMenu.tsx +++ b/examples/CRISP/client/src/components/NavMenu.tsx @@ -35,16 +35,19 @@ const NavMenu: React.FC = () => { const [isOpen, setIsOpen] = useState(false) const buttonRef = useRef(null) - const handleClickOutside = (event: MouseEvent) => { - if ( - isOpen && - menuRef.current && - !menuRef.current.contains(event.target as Node) && - !buttonRef.current?.contains(event.target as Node) - ) { - setIsOpen(false) - } - } + const handleClickOutside = React.useCallback( + (event: MouseEvent) => { + if ( + isOpen && + menuRef.current && + !menuRef.current.contains(event.target as Node) && + !buttonRef.current?.contains(event.target as Node) + ) { + setIsOpen(false) + } + }, + [isOpen], + ) const toggleMenu = (event: React.MouseEvent) => { event.stopPropagation() @@ -59,7 +62,7 @@ const NavMenu: React.FC = () => { return () => { document.removeEventListener('mousedown', handleClickOutside) } - }, [isOpen]) + }, [isOpen, handleClickOutside]) const handleNavigation = (path: string) => { navigate(path) diff --git a/examples/CRISP/client/src/context/NotificationAlert/NotificationAlert.context.tsx b/examples/CRISP/client/src/context/NotificationAlert/NotificationAlert.context.tsx index 34397220c3..159a390791 100644 --- a/examples/CRISP/client/src/context/NotificationAlert/NotificationAlert.context.tsx +++ b/examples/CRISP/client/src/context/NotificationAlert/NotificationAlert.context.tsx @@ -85,4 +85,5 @@ const NotificationAlertProvider = ({ children }: NotificationAlertProviderProps) ) } +// eslint-disable-next-line react-refresh/only-export-components export { useNotificationAlertContext, NotificationAlertProvider } diff --git a/examples/CRISP/client/src/context/voteManagement/VoteManagement.context.tsx b/examples/CRISP/client/src/context/voteManagement/VoteManagement.context.tsx index 1299e8d105..ede1bea4ef 100644 --- a/examples/CRISP/client/src/context/voteManagement/VoteManagement.context.tsx +++ b/examples/CRISP/client/src/context/voteManagement/VoteManagement.context.tsx @@ -239,4 +239,5 @@ const VoteManagementProvider = ({ children }: VoteManagementProviderProps) => { ) } +// eslint-disable-next-line react-refresh/only-export-components export { useVoteManagementContext, VoteManagementProvider } diff --git a/examples/CRISP/client/src/pages/DailyPoll/components/ConfirmVote.tsx b/examples/CRISP/client/src/pages/DailyPoll/components/ConfirmVote.tsx index 0393c73b38..a17d3200f5 100644 --- a/examples/CRISP/client/src/pages/DailyPoll/components/ConfirmVote.tsx +++ b/examples/CRISP/client/src/pages/DailyPoll/components/ConfirmVote.tsx @@ -15,6 +15,7 @@ const ConfirmVote: React.FC<{ confirmationUrl: string }> = ({ confirmationUrl }) return () => { setTxUrl(undefined) } + // eslint-disable-next-line react-hooks/exhaustive-deps }, []) return ( diff --git a/examples/CRISP/client/src/pages/PollResult/PollResult.tsx b/examples/CRISP/client/src/pages/PollResult/PollResult.tsx index 50107fa8cd..4c5db262f8 100644 --- a/examples/CRISP/client/src/pages/PollResult/PollResult.tsx +++ b/examples/CRISP/client/src/pages/PollResult/PollResult.tsx @@ -45,12 +45,14 @@ const PollResult: React.FC = () => { setLoading(false) } } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [pastPolls, roundId, roundState, activeTotalCount]) useEffect(() => { if (pollResult && loading) { setLoading(false) } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [pollResult]) return ( diff --git a/examples/CRISP/enclave.config.yaml b/examples/CRISP/enclave.config.yaml index 976928cd43..71e18d12b6 100644 --- a/examples/CRISP/enclave.config.yaml +++ b/examples/CRISP/enclave.config.yaml @@ -3,20 +3,23 @@ chains: rpc_url: ws://localhost:8545 contracts: e3_program: - address: '0x84eA74d481Ee0A5332c457a4d796187F6Ba67fEB' + address: '0x1613beB3B2C4f22Ee086B2b38C1476A3cE7f78E8' deploy_block: 37 enclave: address: '0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0' - deploy_block: 15 + deploy_block: 13 ciphernode_registry: address: '0x610178dA211FEF7D417bC0e6FeD39F05609AD788' - deploy_block: 13 + deploy_block: 11 bonding_registry: address: '0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6' - deploy_block: 10 + deploy_block: 8 fee_token: address: '0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0' - deploy_block: 5 + deploy_block: 4 + slashing_manager: + address: '0x0165878A594ca255338adfa4d48449f69242Eb8F' + deploy_block: 8 program: dev: true nodes: diff --git a/examples/CRISP/packages/crisp-contracts/deploy/deploy.ts b/examples/CRISP/packages/crisp-contracts/deploy/deploy.ts index 90f7a359fd..5582d17bb0 100644 --- a/examples/CRISP/packages/crisp-contracts/deploy/deploy.ts +++ b/examples/CRISP/packages/crisp-contracts/deploy/deploy.ts @@ -16,6 +16,7 @@ const contractMapping: Record = { Enclave: 'enclave', CiphernodeRegistryOwnable: 'ciphernode_registry', BondingRegistry: 'bonding_registry', + SlashingManager: 'slashing_manager', MockUSDC: 'fee_token', } diff --git a/examples/CRISP/packages/crisp-contracts/deployed_contracts.json b/examples/CRISP/packages/crisp-contracts/deployed_contracts.json index 43557cade4..98a62ff20d 100644 --- a/examples/CRISP/packages/crisp-contracts/deployed_contracts.json +++ b/examples/CRISP/packages/crisp-contracts/deployed_contracts.json @@ -154,5 +154,155 @@ "address": "0x6d97bDf6741905F63bd99e0EB920FFe5e5498544", "blockNumber": 10285602 } + }, + "localhost": { + "PoseidonT3": { + "blockNumber": 3, + "address": "0x3333333C0A88F9BE4fd23ed0536F9B6c427e3B93" + }, + "MockUSDC": { + "constructorArgs": { + "initialSupply": "1000000" + }, + "blockNumber": 4, + "address": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" + }, + "EnclaveToken": { + "constructorArgs": { + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + }, + "blockNumber": 5, + "address": "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9" + }, + "EnclaveTicketToken": { + "constructorArgs": { + "baseToken": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", + "registry": "0x0000000000000000000000000000000000000001", + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + }, + "blockNumber": 7, + "address": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707" + }, + "SlashingManager": { + "constructorArgs": { + "admin": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "bondingRegistry": "0x0000000000000000000000000000000000000001", + "ciphernodeRegistry": "0x0000000000000000000000000000000000000001", + "enclave": "0x0000000000000000000000000000000000000001" + }, + "blockNumber": 8, + "address": "0x0165878A594ca255338adfa4d48449f69242Eb8F" + }, + "BondingRegistry": { + "constructorArgs": { + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "ticketToken": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707", + "licenseToken": "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9", + "registry": "0x0000000000000000000000000000000000000001", + "slashedFundsTreasury": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "ticketPrice": "10000000", + "licenseRequiredBond": "100000000000000000000", + "minTicketBalance": "1", + "exitDelay": "604800" + }, + "proxyRecords": { + "initData": "0x7333fa82000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb922660000000000000000000000005fc8d32690cc91d4c39d9d3abcbd16989f875707000000000000000000000000cf7ed3acca5a467e9e704c703e8d87f634fb0fc90000000000000000000000000000000000000000000000000000000000000001000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb9226600000000000000000000000000000000000000000000000000000000009896800000000000000000000000000000000000000000000000056bc75e2d6310000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000093a80", + "initialOwner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "proxyAddress": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6", + "proxyAdminAddress": "0x94099942864EA81cCF197E9D71ac53310b1468D8", + "implementationAddress": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" + }, + "blockNumber": 8, + "address": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" + }, + "CiphernodeRegistryOwnable": { + "constructorArgs": { + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "enclaveAddress": "0x0000000000000000000000000000000000000001", + "submissionWindow": "10" + }, + "proxyRecords": { + "initData": "0x1794bb3c000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb922660000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000a", + "initialOwner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "proxyAddress": "0x610178dA211FEF7D417bC0e6FeD39F05609AD788", + "proxyAdminAddress": "0x6F1216D1BFe15c98520CA1434FC1d9D57AC95321", + "implementationAddress": "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" + }, + "blockNumber": 11, + "address": "0x610178dA211FEF7D417bC0e6FeD39F05609AD788" + }, + "Enclave": { + "constructorArgs": { + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "registry": "0x610178dA211FEF7D417bC0e6FeD39F05609AD788", + "bondingRegistry": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6", + "e3RefundManager": "0x0000000000000000000000000000000000000001", + "feeToken": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", + "maxDuration": "2592000", + "timeoutConfig": "{\"committeeFormationWindow\":3600,\"dkgWindow\":7200,\"computeWindow\":86400,\"decryptionWindow\":3600,\"gracePeriod\":600}", + "params": [ + "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000ffffee0010000000000000000000000000000000000000000000000000000000ffffc400100000000000000000000000000000000000000000000000000000000000000013300000000000000000000000000000000000000000000000000000000000000" + ] + }, + "proxyRecords": { + "initData": "0x69c5b347000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266000000000000000000000000610178da211fef7d417bc0e6fed39f05609ad7880000000000000000000000002279b7a0a67db372996a5fab50d91eaa73d2ebe600000000000000000000000000000000000000000000000000000000000000010000000000000000000000009fe46736679d2d9a65f0992f2272de9f3c7fa6e00000000000000000000000000000000000000000000000000000000000278d000000000000000000000000000000000000000000000000000000000000001c2000000000000000000000000000000000000000000000000000000000000151800000000000000000000000000000000000000000000000000000000000000e1000000000000000000000000000000000000000000000000000000000000002580000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000ffffee0010000000000000000000000000000000000000000000000000000000ffffc400100000000000000000000000000000000000000000000000000000000000000013300000000000000000000000000000000000000000000000000000000000000", + "initialOwner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "proxyAddress": "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0", + "proxyAdminAddress": "0x1F708C24a0D3A740cD47cC0444E9480899f3dA7D", + "implementationAddress": "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e" + }, + "blockNumber": 13, + "address": "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0" + }, + "E3RefundManager": { + "constructorArgs": { + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "enclave": "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0", + "treasury": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + }, + "proxyRecords": { + "initData": "0xc0c53b8b000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266000000000000000000000000a51c1fc2f0d1a1b8494ed1fe312d7c3a78ed91c0000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266", + "initialOwner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "proxyAddress": "0x9A676e781A523b5d0C0e43731313A708CB607508", + "proxyAdminAddress": "0x8e80FFe6Dc044F4A766Afd6e5a8732Fe0977A493", + "implementationAddress": "0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82" + }, + "blockNumber": 15, + "address": "0x9A676e781A523b5d0C0e43731313A708CB607508" + }, + "MockComputeProvider": { + "blockNumber": 29, + "address": "0x09635F643e140090A9A8Dcd712eD6285858ceBef" + }, + "MockDecryptionVerifier": { + "blockNumber": 30, + "address": "0xc5a5C42992dECbae36851359345FE25997F5C42d" + }, + "MockE3Program": { + "blockNumber": 31, + "address": "0x67d269191c92Caf3cD7723F116c85e6E9bf55933" + }, + "MockRISC0Verifier": { + "address": "0x84eA74d481Ee0A5332c457a4d796187F6Ba67fEB", + "blockNumber": 34 + }, + "HonkVerifier": { + "address": "0xa82fF9aFd8f496c3d6ac40E2a0F282E47488CFc9", + "blockNumber": 36 + }, + "CRISPProgram": { + "address": "0x1613beB3B2C4f22Ee086B2b38C1476A3cE7f78E8", + "blockNumber": 37, + "constructorArgs": { + "enclave": "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0", + "verifierAddress": "0x84eA74d481Ee0A5332c457a4d796187F6Ba67fEB", + "honkVerifierAddress": "0xa82fF9aFd8f496c3d6ac40E2a0F282E47488CFc9", + "imageId": "0x23734b77b0f76e85623a88d7a82f24c34c94834f2501964ea123b7a2027013a2" + } + }, + "MockVotingToken": { + "address": "0xf5059a5D33d5853360D16C683c16e67980206f36", + "blockNumber": 39 + } } } \ No newline at end of file diff --git a/examples/CRISP/server/.env.example b/examples/CRISP/server/.env.example index 0d81df449f..58d0f1c6e4 100644 --- a/examples/CRISP/server/.env.example +++ b/examples/CRISP/server/.env.example @@ -15,7 +15,7 @@ CRON_API_KEY=1234567890 # Based on Default Hardhat Deployments (Only for testing) ENCLAVE_ADDRESS="0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0" CIPHERNODE_REGISTRY_ADDRESS="0x610178dA211FEF7D417bC0e6FeD39F05609AD788" -E3_PROGRAM_ADDRESS="0x84eA74d481Ee0A5332c457a4d796187F6Ba67fEB" # CRISPProgram Contract Address +E3_PROGRAM_ADDRESS="0x1613beB3B2C4f22Ee086B2b38C1476A3cE7f78E8" # CRISPProgram Contract Address FEE_TOKEN_ADDRESS="0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" # E3 Config diff --git a/packages/enclave-contracts/.gitignore b/packages/enclave-contracts/.gitignore index 919a65a056..5d288546ee 100644 --- a/packages/enclave-contracts/.gitignore +++ b/packages/enclave-contracts/.gitignore @@ -3,20 +3,26 @@ .coverage_cache .coverage_contracts +# Base Dirs + # Base Dirs /artifacts/ !/artifacts/ /artifacts/** !/artifacts/contracts/ +# Interfaces + # Interfaces !/artifacts/contracts/interfaces/ !/artifacts/contracts/interfaces/IEnclave.sol/ !/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ !/artifacts/contracts/interfaces/IBondingRegistry.sol/ +!/artifacts/contracts/interfaces/ISlashingManager.sol/ !/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json !/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json !/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json +!/artifacts/contracts/interfaces/ISlashingManager.sol/ISlashingManager.json # Registry !/artifacts/contracts/registry/ @@ -26,6 +32,13 @@ !/artifacts/contracts/token/EnclaveTicketToken.sol/ !/artifacts/contracts/token/EnclaveTicketToken.sol/EnclaveTicketToken.json +# Verifier contracts +!/artifacts/contracts/verifier/ +!/artifacts/contracts/verifier/DkgPkVerifier.sol/ +!/artifacts/contracts/verifier/DkgPkVerifier.sol/DkgPkVerifier.json +!/artifacts/contracts/verifier/DkgPkVerifier.sol/ZKTranscriptLib.json + + # Verifier contracts !/artifacts/contracts/verifier/ !/artifacts/contracts/verifier/DkgPkVerifier.sol/ diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json b/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json index a9daa05c33..b87aa0ec93 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json @@ -611,6 +611,19 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "address", + "name": "distributor", + "type": "address" + } + ], + "name": "revokeRewardDistributor", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -890,5 +903,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/IBondingRegistry.sol", - "buildInfoId": "solc-0_8_28-e60a5d7c133605edcf61acdd5ba43ab44ee0928e" + "buildInfoId": "solc-0_8_28-2d0c0fb6cf254d92756645139041fc1083954efa" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json b/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json index 7c6ad1ce87..28e91369d8 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json @@ -128,6 +128,37 @@ "name": "CommitteeFormationFailed", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "node", + "type": "address" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "reason", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "activeCountAfter", + "type": "uint256" + } + ], + "name": "CommitteeMemberExpelled", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -190,6 +221,37 @@ "name": "CommitteeRequested", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "activeCount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "thresholdM", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bool", + "name": "viable", + "type": "bool" + } + ], + "name": "CommitteeViabilityUpdated", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -279,6 +341,40 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + }, + { + "internalType": "address", + "name": "node", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "reason", + "type": "bytes32" + } + ], + "name": "expelCommitteeMember", + "outputs": [ + { + "internalType": "uint256", + "name": "activeCount", + "type": "uint256" + }, + { + "internalType": "uint32", + "name": "thresholdM", + "type": "uint32" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -298,6 +394,44 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + } + ], + "name": "getActiveCommitteeCount", + "outputs": [ + { + "internalType": "uint256", + "name": "count", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + } + ], + "name": "getActiveCommitteeNodes", + "outputs": [ + { + "internalType": "address[]", + "name": "nodes", + "type": "address[]" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "getBondingRegistry", @@ -349,6 +483,25 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + } + ], + "name": "getCommitteeThreshold", + "outputs": [ + { + "internalType": "uint32[2]", + "name": "threshold", + "type": "uint32[2]" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -368,6 +521,30 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + }, + { + "internalType": "address", + "name": "node", + "type": "address" + } + ], + "name": "isCommitteeMemberActive", + "outputs": [ + { + "internalType": "bool", + "name": "active", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -590,5 +767,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/ICiphernodeRegistry.sol", - "buildInfoId": "solc-0_8_28-e60a5d7c133605edcf61acdd5ba43ab44ee0928e" + "buildInfoId": "solc-0_8_28-2d0c0fb6cf254d92756645139041fc1083954efa" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json b/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json index 28c6fdb611..2bfe082fbd 100644 --- a/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json @@ -163,6 +163,19 @@ "name": "E3ProgramEnabled", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bytes[]", + "name": "e3ProgramParams", + "type": "bytes[]" + } + ], + "name": "E3ProgramsParamsRemoved", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -420,6 +433,19 @@ "name": "RewardsDistributed", "type": "event" }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "slashingManager", + "type": "address" + } + ], + "name": "SlashingManagerSet", + "type": "event" + }, { "anonymous": false, "inputs": [ @@ -439,11 +465,6 @@ "internalType": "uint256", "name": "decryptionWindow", "type": "uint256" - }, - { - "internalType": "uint256", - "name": "gracePeriod", - "type": "uint256" } ], "indexed": false, @@ -813,11 +834,6 @@ "internalType": "uint256", "name": "decryptionWindow", "type": "uint256" - }, - { - "internalType": "uint256", - "name": "gracePeriod", - "type": "uint256" } ], "internalType": "struct IEnclave.E3TimeoutConfig", @@ -954,6 +970,19 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [ + { + "internalType": "bytes[]", + "name": "_e3ProgramsParams", + "type": "bytes[]" + } + ], + "name": "removeE3ProgramsParams", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { @@ -1178,11 +1207,6 @@ "internalType": "uint256", "name": "decryptionWindow", "type": "uint256" - }, - { - "internalType": "uint256", - "name": "gracePeriod", - "type": "uint256" } ], "internalType": "struct IEnclave.E3TimeoutConfig", @@ -1202,5 +1226,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/interfaces/IEnclave.sol", - "buildInfoId": "solc-0_8_28-e60a5d7c133605edcf61acdd5ba43ab44ee0928e" + "buildInfoId": "solc-0_8_28-2d0c0fb6cf254d92756645139041fc1083954efa" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/interfaces/ISlashingManager.sol/ISlashingManager.json b/packages/enclave-contracts/artifacts/contracts/interfaces/ISlashingManager.sol/ISlashingManager.json new file mode 100644 index 0000000000..6ba870c913 --- /dev/null +++ b/packages/enclave-contracts/artifacts/contracts/interfaces/ISlashingManager.sol/ISlashingManager.json @@ -0,0 +1,849 @@ +{ + "_format": "hh3-artifact-1", + "contractName": "ISlashingManager", + "sourceName": "contracts/interfaces/ISlashingManager.sol", + "abi": [ + { + "inputs": [], + "name": "AlreadyAppealed", + "type": "error" + }, + { + "inputs": [], + "name": "AlreadyExecuted", + "type": "error" + }, + { + "inputs": [], + "name": "AlreadyResolved", + "type": "error" + }, + { + "inputs": [], + "name": "AppealPending", + "type": "error" + }, + { + "inputs": [], + "name": "AppealUpheld", + "type": "error" + }, + { + "inputs": [], + "name": "AppealWindowActive", + "type": "error" + }, + { + "inputs": [], + "name": "AppealWindowExpired", + "type": "error" + }, + { + "inputs": [], + "name": "CiphernodeBanned", + "type": "error" + }, + { + "inputs": [], + "name": "DuplicateEvidence", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidPolicy", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidProof", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidProposal", + "type": "error" + }, + { + "inputs": [], + "name": "OperatorNotInCommittee", + "type": "error" + }, + { + "inputs": [], + "name": "ProofIsValid", + "type": "error" + }, + { + "inputs": [], + "name": "ProofRequired", + "type": "error" + }, + { + "inputs": [], + "name": "SignerIsNotOperator", + "type": "error" + }, + { + "inputs": [], + "name": "SlashReasonDisabled", + "type": "error" + }, + { + "inputs": [], + "name": "SlashReasonNotFound", + "type": "error" + }, + { + "inputs": [], + "name": "Unauthorized", + "type": "error" + }, + { + "inputs": [], + "name": "VerifierCallFailed", + "type": "error" + }, + { + "inputs": [], + "name": "VerifierMismatch", + "type": "error" + }, + { + "inputs": [], + "name": "VerifierNotSet", + "type": "error" + }, + { + "inputs": [], + "name": "ZeroAddress", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "proposalId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "reason", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "string", + "name": "evidence", + "type": "string" + } + ], + "name": "AppealFiled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "proposalId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "appealUpheld", + "type": "bool" + }, + { + "indexed": false, + "internalType": "address", + "name": "resolver", + "type": "address" + }, + { + "indexed": false, + "internalType": "string", + "name": "resolution", + "type": "string" + } + ], + "name": "AppealResolved", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "node", + "type": "address" + }, + { + "indexed": false, + "internalType": "bool", + "name": "status", + "type": "bool" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "reason", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "address", + "name": "updater", + "type": "address" + } + ], + "name": "NodeBanUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "proposalId", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "reason", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "ticketAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "licenseAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "bool", + "name": "executed", + "type": "bool" + } + ], + "name": "SlashExecuted", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "reason", + "type": "bytes32" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "ticketPenalty", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "licensePenalty", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "requiresProof", + "type": "bool" + }, + { + "internalType": "address", + "name": "proofVerifier", + "type": "address" + }, + { + "internalType": "bool", + "name": "banNode", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "appealWindow", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "enabled", + "type": "bool" + }, + { + "internalType": "bool", + "name": "affectsCommittee", + "type": "bool" + }, + { + "internalType": "uint8", + "name": "failureReason", + "type": "uint8" + } + ], + "indexed": false, + "internalType": "struct ISlashingManager.SlashPolicy", + "name": "policy", + "type": "tuple" + } + ], + "name": "SlashPolicyUpdated", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "uint256", + "name": "proposalId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "indexed": false, + "internalType": "bytes32", + "name": "reason", + "type": "bytes32" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "ticketAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "licenseAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "executableAt", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "proposer", + "type": "address" + } + ], + "name": "SlashProposed", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "slasher", + "type": "address" + } + ], + "name": "addSlasher", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "bondingRegistry", + "outputs": [ + { + "internalType": "contract IBondingRegistry", + "name": "registry", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "proposalId", + "type": "uint256" + } + ], + "name": "executeSlash", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "proposalId", + "type": "uint256" + }, + { + "internalType": "string", + "name": "evidence", + "type": "string" + } + ], + "name": "fileAppeal", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "reason", + "type": "bytes32" + } + ], + "name": "getSlashPolicy", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "ticketPenalty", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "licensePenalty", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "requiresProof", + "type": "bool" + }, + { + "internalType": "address", + "name": "proofVerifier", + "type": "address" + }, + { + "internalType": "bool", + "name": "banNode", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "appealWindow", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "enabled", + "type": "bool" + }, + { + "internalType": "bool", + "name": "affectsCommittee", + "type": "bool" + }, + { + "internalType": "uint8", + "name": "failureReason", + "type": "uint8" + } + ], + "internalType": "struct ISlashingManager.SlashPolicy", + "name": "policy", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "proposalId", + "type": "uint256" + } + ], + "name": "getSlashProposal", + "outputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + }, + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "reason", + "type": "bytes32" + }, + { + "internalType": "uint256", + "name": "ticketAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "licenseAmount", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "executed", + "type": "bool" + }, + { + "internalType": "bool", + "name": "appealed", + "type": "bool" + }, + { + "internalType": "bool", + "name": "resolved", + "type": "bool" + }, + { + "internalType": "bool", + "name": "appealUpheld", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "proposedAt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "executableAt", + "type": "uint256" + }, + { + "internalType": "address", + "name": "proposer", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "proofHash", + "type": "bytes32" + }, + { + "internalType": "bool", + "name": "proofVerified", + "type": "bool" + } + ], + "internalType": "struct ISlashingManager.SlashProposal", + "name": "proposal", + "type": "tuple" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "node", + "type": "address" + } + ], + "name": "isBanned", + "outputs": [ + { + "internalType": "bool", + "name": "isBanned", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + }, + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "reason", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "proof", + "type": "bytes" + } + ], + "name": "proposeSlash", + "outputs": [ + { + "internalType": "uint256", + "name": "proposalId", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "e3Id", + "type": "uint256" + }, + { + "internalType": "address", + "name": "operator", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "reason", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "evidence", + "type": "bytes" + } + ], + "name": "proposeSlashEvidence", + "outputs": [ + { + "internalType": "uint256", + "name": "proposalId", + "type": "uint256" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "slasher", + "type": "address" + } + ], + "name": "removeSlasher", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "proposalId", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "appealUpheld", + "type": "bool" + }, + { + "internalType": "string", + "name": "resolution", + "type": "string" + } + ], + "name": "resolveAppeal", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newBondingRegistry", + "type": "address" + } + ], + "name": "setBondingRegistry", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "reason", + "type": "bytes32" + }, + { + "components": [ + { + "internalType": "uint256", + "name": "ticketPenalty", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "licensePenalty", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "requiresProof", + "type": "bool" + }, + { + "internalType": "address", + "name": "proofVerifier", + "type": "address" + }, + { + "internalType": "bool", + "name": "banNode", + "type": "bool" + }, + { + "internalType": "uint256", + "name": "appealWindow", + "type": "uint256" + }, + { + "internalType": "bool", + "name": "enabled", + "type": "bool" + }, + { + "internalType": "bool", + "name": "affectsCommittee", + "type": "bool" + }, + { + "internalType": "uint8", + "name": "failureReason", + "type": "uint8" + } + ], + "internalType": "struct ISlashingManager.SlashPolicy", + "name": "policy", + "type": "tuple" + } + ], + "name": "setSlashPolicy", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "totalProposals", + "outputs": [ + { + "internalType": "uint256", + "name": "count", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "node", + "type": "address" + }, + { + "internalType": "bool", + "name": "status", + "type": "bool" + }, + { + "internalType": "bytes32", + "name": "reason", + "type": "bytes32" + } + ], + "name": "updateBanStatus", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + } + ], + "bytecode": "0x", + "deployedBytecode": "0x", + "linkReferences": {}, + "deployedLinkReferences": {}, + "immutableReferences": {}, + "inputSourceName": "project/contracts/interfaces/ISlashingManager.sol", + "buildInfoId": "solc-0_8_28-2d0c0fb6cf254d92756645139041fc1083954efa" +} \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/token/EnclaveTicketToken.sol/EnclaveTicketToken.json b/packages/enclave-contracts/artifacts/contracts/token/EnclaveTicketToken.sol/EnclaveTicketToken.json index c995f68ebf..17d318e4f7 100644 --- a/packages/enclave-contracts/artifacts/contracts/token/EnclaveTicketToken.sol/EnclaveTicketToken.json +++ b/packages/enclave-contracts/artifacts/contracts/token/EnclaveTicketToken.sol/EnclaveTicketToken.json @@ -508,12 +508,12 @@ "inputs": [ { "internalType": "address", - "name": "spender", + "name": "", "type": "address" }, { "internalType": "uint256", - "name": "value", + "name": "", "type": "uint256" } ], @@ -525,7 +525,7 @@ "type": "bool" } ], - "stateMutability": "nonpayable", + "stateMutability": "pure", "type": "function" }, { @@ -919,6 +919,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "payableBalance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -1143,72 +1156,72 @@ "type": "function" } ], - "bytecode": "0x610180604052348015610010575f5ffd5b5060405161289638038061289683398101604081905261002f9161037d565b82816040518060400160405280601481526020017f456e636c617665205469636b657420546f6b656e00000000000000000000000081525080604051806040016040528060018152602001603160f81b8152506040518060400160405280601481526020017f456e636c617665205469636b657420546f6b656e0000000000000000000000008152506040518060400160405280600381526020016245544b60e81b81525081600390816100e3919061045f565b5060046100f0828261045f565b5061010091508390506005610226565b6101205261010f816006610226565b61014052815160208084019190912060e052815190820120610100524660a05261019b60e05161010051604080517f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f60208201529081019290925260608201524660808201523060a08201525f9060c00160405160208183030381529060405280519060200120905090565b60805250503060c052506001600160a01b0381166101d357604051631e4fbdf760e01b81525f60048201526024015b60405180910390fd5b6101dc81610258565b50306001600160a01b038216036102085760405163438d6fe360e01b81523060048201526024016101ca565b6001600160a01b03166101605261021e826102a9565b505050610571565b5f6020835110156102415761023a836102fa565b9050610252565b8161024c848261045f565b5060ff90505b92915050565b600b80546001600160a01b038381166001600160a01b0319831681179093556040519116919082907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a35050565b6102b1610337565b6001600160a01b0381166102d85760405163d92e233d60e01b815260040160405180910390fd5b600c80546001600160a01b0319166001600160a01b0392909216919091179055565b5f5f829050601f81511115610324578260405163305a27a960e01b81526004016101ca9190610519565b805161032f8261054e565b179392505050565b600b546001600160a01b031633146103645760405163118cdaa760e01b81523360048201526024016101ca565b565b6001600160a01b038116811461037a575f5ffd5b50565b5f5f5f6060848603121561038f575f5ffd5b835161039a81610366565b60208501519093506103ab81610366565b60408501519092506103bc81610366565b809150509250925092565b634e487b7160e01b5f52604160045260245ffd5b600181811c908216806103ef57607f821691505b60208210810361040d57634e487b7160e01b5f52602260045260245ffd5b50919050565b601f82111561045a57805f5260205f20601f840160051c810160208510156104385750805b601f840160051c820191505b81811015610457575f8155600101610444565b50505b505050565b81516001600160401b03811115610478576104786103c7565b61048c8161048684546103db565b84610413565b6020601f8211600181146104be575f83156104a75750848201515b5f19600385901b1c1916600184901b178455610457565b5f84815260208120601f198516915b828110156104ed57878501518255602094850194600190920191016104cd565b508482101561050a57868401515f19600387901b60f8161c191681555b50505050600190811b01905550565b602081525f82518060208401528060208501604085015e5f604082850101526040601f19601f83011684010191505092915050565b8051602080830151919081101561040d575f1960209190910360031b1b16919050565b60805160a05160c05160e051610100516101205161014051610160516122b56105e15f395f81816107c401528181610ba901528181610cda0152610d8701525f61119c01525f61116f01525f610ee701525f610ebf01525f610e1a01525f610e4401525f610e6e01526122b55ff3fe608060405234801561000f575f5ffd5b50600436106101c1575f3560e01c8063715018a6116100f657806395d89b411161009a57806395d89b41146103e05780639ab24eb0146103e8578063a9059cbb146103fb578063a91ee0dc1461040e578063c3cda52014610421578063d505accf1461042f578063dd62ed3e14610442578063f1127ed814610455578063f2fde38b14610494575f5ffd5b8063715018a6146103415780637b103999146103495780637ecebe001461035c57806384b0196e1461036f57806385bc898c1461038a5780638da5cb5b1461039d5780638e539e8c146103ae57806391ddadf4146103c1575f5ffd5b80633644e515116101685780633644e515146102805780633a46b1a8146102885780634bf5d7e91461029b578063587cde1e146102a35780635c19a95c146102c357806368a9674d146102d65780636f307dc3146102e95780636fcfff45146102f157806370a0823114610319575f5ffd5b806306fdde03146101c5578063095ea7b3146101e3578063117de2fd1461020657806318160ddd1461021b578063205c28781461022d57806323b872dd146102405780632f4f21e214610253578063313ce56714610266575b5f5ffd5b6101cd6104a7565b6040516101da9190611ec3565b60405180910390f35b6101f66101f1366004611eeb565b610537565b60405190151581526020016101da565b610219610214366004611eeb565b610550565b005b6002545b6040519081526020016101da565b6101f661023b366004611eeb565b610591565b6101f661024e366004611f13565b6105cf565b6101f6610261366004611eeb565b6105f2565b61026e61064d565b60405160ff90911681526020016101da565b61021f61065b565b61021f610296366004611eeb565b610664565b6101cd61069e565b6102b66102b1366004611f4d565b610716565b6040516101da9190611f66565b6102196102d1366004611f4d565b610733565b6101f66102e4366004611f13565b61074c565b6102b66107c2565b6103046102ff366004611f4d565b6107e6565b60405163ffffffff90911681526020016101da565b61021f610327366004611f4d565b6001600160a01b03165f9081526020819052604090205490565b6102196107f0565b600c546102b6906001600160a01b031681565b61021f61036a366004611f4d565b610803565b61037761080d565b6040516101da9796959493929190611f7a565b610219610398366004611eeb565b61084f565b600b546001600160a01b03166102b6565b61021f6103bc366004612010565b610884565b6103c96108a8565b60405165ffffffffffff90911681526020016101da565b6101cd6108b1565b61021f6103f6366004611f4d565b6108c0565b6101f6610409366004611eeb565b6108e0565b61021961041c366004611f4d565b6108ed565b6102196102d1366004612035565b61021961043d36600461208b565b61093e565b61021f6104503660046120f3565b610a79565b610468610463366004612124565b610aa3565b60408051825165ffffffffffff1681526020928301516001600160d01b031692810192909252016101da565b6102196104a2366004611f4d565b610ac0565b6060600380546104b690612161565b80601f01602080910402602001604051908101604052809291908181526020018280546104e290612161565b801561052d5780601f106105045761010080835404028352916020019161052d565b820191905f5260205f20905b81548152906001019060200180831161051057829003601f168201915b5050505050905090565b5f33610544818585610afd565b60019150505b92915050565b600c546001600160a01b0316331461057b57604051633217675b60e21b815260040160405180910390fd5b61058d6105866107c2565b8383610b0f565b5050565b600c545f906001600160a01b031633146105be57604051633217675b60e21b815260040160405180910390fd5b6105c88383610b6e565b9392505050565b5f336105dc858285610bd8565b6105e7858585610c29565b506001949350505050565b600c545f906001600160a01b0316331461061f57604051633217675b60e21b815260040160405180910390fd5b6106298383610c86565b90505f61063584610716565b6001600160a01b03160361054a5761054a8384610d0b565b5f610656610d84565b905090565b5f610656610e0e565b5f61068e61067183610f37565b6001600160a01b0385165f90815260096020526040902090610f85565b6001600160d01b03169392505050565b60606106a8611035565b65ffffffffffff166106b86108a8565b65ffffffffffff16146106de576040516301bfc1c560e61b815260040160405180910390fd5b5060408051808201909152601d81527f6d6f64653d626c6f636b6e756d6265722666726f6d3d64656661756c74000000602082015290565b6001600160a01b039081165f908152600860205260409020541690565b604051635e81118160e11b815260040160405180910390fd5b600c545f906001600160a01b0316331461077957604051633217675b60e21b815260040160405180910390fd5b61078c6107846107c2565b85308561103f565b6107968383611078565b5f6107a084610716565b6001600160a01b0316036107b8576107b88384610d0b565b5060019392505050565b7f000000000000000000000000000000000000000000000000000000000000000090565b5f61054a826110ac565b6107f86110cd565b6108015f6110fa565b565b5f61054a8261114b565b5f6060805f5f5f606061081e611168565b610826611195565b604080515f80825260208201909252600f60f81b9b939a50919850469750309650945092509050565b600c546001600160a01b0316331461087a57604051633217675b60e21b815260040160405180910390fd5b61058d82826111c2565b5f61089961089183610f37565b600a90610f85565b6001600160d01b031692915050565b5f610656611035565b6060600480546104b690612161565b6001600160a01b0381165f908152600960205260408120610899906111f6565b5f33610544818585610c29565b6108f56110cd565b6001600160a01b03811661091c5760405163d92e233d60e01b815260040160405180910390fd5b600c80546001600160a01b0319166001600160a01b0392909216919091179055565b834211156109675760405163313c898160e11b8152600481018590526024015b60405180910390fd5b5f7f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c98888886109b28c6001600160a01b03165f90815260076020526040902080546001810190915590565b6040805160208101969096526001600160a01b0394851690860152929091166060840152608083015260a082015260c0810186905260e0016040516020818303038152906040528051906020012090505f610a0c8261122d565b90505f610a1b82878787611259565b9050896001600160a01b0316816001600160a01b031614610a62576040516325c0072360e11b81526001600160a01b0380831660048301528b16602482015260440161095e565b610a6d8a8a8a610afd565b50505050505050505050565b6001600160a01b039182165f90815260016020908152604080832093909416825291909152205490565b604080518082019091525f80825260208201526105c88383611285565b610ac86110cd565b6001600160a01b038116610af1575f604051631e4fbdf760e01b815260040161095e9190611f66565b610afa816110fa565b50565b610b0a83838360016112b9565b505050565b6040516001600160a01b03838116602483015260448201839052610b0a91859182169063a9059cbb906064015b604051602081830303815290604052915060e01b6020820180516001600160e01b03838183161783525050505061138b565b5f306001600160a01b03841603610b9a578260405163ec442f0560e01b815260040161095e9190611f66565b610ba433836111c2565b610bcf7f00000000000000000000000000000000000000000000000000000000000000008484610b0f565b50600192915050565b5f610be38484610a79565b90505f19811015610c235781811015610c1557828183604051637dc7a0d960e11b815260040161095e93929190612199565b610c2384848484035f6112b9565b50505050565b6001600160a01b038316610c52575f604051634b637e8f60e11b815260040161095e9190611f66565b6001600160a01b038216610c7b575f60405163ec442f0560e01b815260040161095e9190611f66565b610b0a8383836113ee565b5f33308103610caa5730604051634b637e8f60e11b815260040161095e9190611f66565b306001600160a01b03851603610cd5578360405163ec442f0560e01b815260040161095e9190611f66565b610d017f000000000000000000000000000000000000000000000000000000000000000082308661103f565b6105448484611078565b5f610d1583610716565b6001600160a01b038481165f8181526008602052604080822080546001600160a01b031916888616908117909155905194955093928516927f3134e8a2e6d97e929a7e54011ea5485d7d196dd5f0ba4d4ef95803e8e3fc257f9190a4610b0a8183610d7f86611437565b611454565b5f7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031663313ce5676040518163ffffffff1660e01b8152600401602060405180830381865afa925050508015610dff575060408051601f3d908101601f19168201909252610dfc918101906121ba565b60015b610e095750601290565b919050565b5f306001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016148015610e6657507f000000000000000000000000000000000000000000000000000000000000000046145b15610e9057507f000000000000000000000000000000000000000000000000000000000000000090565b610656604080517f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f60208201527f0000000000000000000000000000000000000000000000000000000000000000918101919091527f000000000000000000000000000000000000000000000000000000000000000060608201524660808201523060a08201525f9060c00160405160208183030381529060405280519060200120905090565b5f5f610f416108a8565b90508065ffffffffffff168310610f7c57604051637669fc0f60e11b81526004810184905265ffffffffffff8216602482015260440161095e565b6105c8836115bd565b81545f9081816005811115610fe1575f610f9e846115f3565b610fa890856121e9565b5f8881526020902090915081015465ffffffffffff9081169087161015610fd157809150610fdf565b610fdc8160016121fc565b92505b505b5f610fee87878585611746565b9050801561102857611012876110056001846121e9565b5f91825260209091200190565b54600160301b90046001600160d01b031661102a565b5f5b979650505050505050565b5f610656436115bd565b6040516001600160a01b038481166024830152838116604483015260648201839052610c239186918216906323b872dd90608401610b3c565b6001600160a01b0382166110a1575f60405163ec442f0560e01b815260040161095e9190611f66565b61058d5f83836113ee565b6001600160a01b0381165f9081526009602052604081205461054a906117a5565b600b546001600160a01b03163314610801573360405163118cdaa760e01b815260040161095e9190611f66565b600b80546001600160a01b038381166001600160a01b0319831681179093556040519116919082907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a35050565b6001600160a01b0381165f9081526007602052604081205461054a565b60606106567f000000000000000000000000000000000000000000000000000000000000000060056117d5565b60606106567f000000000000000000000000000000000000000000000000000000000000000060066117d5565b6001600160a01b0382166111eb575f604051634b637e8f60e11b815260040161095e9190611f66565b61058d825f836113ee565b80545f9080156112255761120f836110056001846121e9565b54600160301b90046001600160d01b03166105c8565b5f9392505050565b5f61054a611239610e0e565b8360405161190160f01b8152600281019290925260228201526042902090565b5f5f5f5f6112698888888861187e565b925092509250611279828261193c565b50909695505050505050565b604080518082019091525f80825260208201526001600160a01b0383165f9081526009602052604090206105c890836119f4565b6001600160a01b0384166112e2575f60405163e602df0560e01b815260040161095e9190611f66565b6001600160a01b03831661130b575f604051634a1406b160e11b815260040161095e9190611f66565b6001600160a01b038085165f9081526001602090815260408083209387168352929052208290558015610c2357826001600160a01b0316846001600160a01b03167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9258460405161137d91815260200190565b60405180910390a350505050565b5f5f60205f8451602086015f885af1806113aa576040513d5f823e3d81fd5b50505f513d915081156113c15780600114156113ce565b6001600160a01b0384163b155b15610c235783604051635274afe760e01b815260040161095e9190611f66565b6001600160a01b0383161580159061140e57506001600160a01b03821615155b1561142c57604051638cd22d1960e01b815260040160405180910390fd5b610b0a838383611a61565b6001600160a01b0381165f9081526020819052604081205461054a565b816001600160a01b0316836001600160a01b03161415801561147557505f81115b15610b0a576001600160a01b0383161561151c576001600160a01b0383165f90815260096020526040812081906114b790611ac76114b286611ad2565b611b05565b6001600160d01b031691506001600160d01b03169150846001600160a01b03167fdec2bacdd2f05b59de34da9b523dff8be42e5e38e818c82fdb0bae774387a7248383604051611511929190918252602082015260400190565b60405180910390a250505b6001600160a01b03821615610b0a576001600160a01b0382165f908152600960205260408120819061155490611b3d6114b286611ad2565b6001600160d01b031691506001600160d01b03169150836001600160a01b03167fdec2bacdd2f05b59de34da9b523dff8be42e5e38e818c82fdb0bae774387a72483836040516115ae929190918252602082015260400190565b60405180910390a25050505050565b5f65ffffffffffff8211156115ef576040516306dfcc6560e41b8152603060048201526024810183905260440161095e565b5090565b5f60018211611600575090565b816001600160801b82106116195760809190911c9060401b5b600160401b821061162f5760409190911c9060201b5b64010000000082106116465760209190911c9060101b5b62010000821061165b5760109190911c9060081b5b610100821061166f5760089190911c9060041b5b601082106116825760049190911c9060021b5b6004821061168e5760011b5b600302600190811c908185816116a6576116a661220f565b048201901c905060018185816116be576116be61220f565b048201901c905060018185816116d6576116d661220f565b048201901c905060018185816116ee576116ee61220f565b048201901c905060018185816117065761170661220f565b048201901c9050600181858161171e5761171e61220f565b048201901c905061173d8185816117375761173761220f565b04821190565b90039392505050565b5f5b8183101561179d575f61175b8484611b48565b5f8781526020902090915065ffffffffffff86169082015465ffffffffffff16111561178957809250611797565b6117948160016121fc565b93505b50611748565b509392505050565b5f63ffffffff8211156115ef576040516306dfcc6560e41b8152602060048201526024810183905260440161095e565b606060ff83146117ef576117e883611b62565b905061054a565b8180546117fb90612161565b80601f016020809104026020016040519081016040528092919081815260200182805461182790612161565b80156118725780601f1061184957610100808354040283529160200191611872565b820191905f5260205f20905b81548152906001019060200180831161185557829003601f168201915b5050505050905061054a565b5f80806fa2a8918ca85bafe22016d0b997e4df60600160ff1b038411156118ad57505f91506003905082611932565b604080515f808252602082018084528a905260ff891692820192909252606081018790526080810186905260019060a0016020604051602081039080840390855afa1580156118fe573d5f5f3e3d5ffd5b5050604051601f1901519150506001600160a01b03811661192957505f925060019150829050611932565b92505f91508190505b9450945094915050565b5f82600381111561194f5761194f612223565b03611958575050565b600182600381111561196c5761196c612223565b0361198a5760405163f645eedf60e01b815260040160405180910390fd5b600282600381111561199e5761199e612223565b036119bf5760405163fce698f760e01b81526004810182905260240161095e565b60038260038111156119d3576119d3612223565b0361058d576040516335e2f38360e21b81526004810182905260240161095e565b604080518082019091525f8082526020820152825f018263ffffffff1681548110611a2157611a21612237565b5f9182526020918290206040805180820190915291015465ffffffffffff81168252600160301b90046001600160d01b0316918101919091529392505050565b611a6c838383611b9f565b6001600160a01b038316611abc575f611a8460025490565b90506001600160d01b0380821115611ab957604051630e58ae9360e11b8152600481018390526024810182905260440161095e565b50505b610b0a838383611cb2565b5f6105c8828461224b565b5f6001600160d01b038211156115ef576040516306dfcc6560e41b815260d060048201526024810183905260440161095e565b5f5f611b30611b126108a8565b611b28611b1e886111f6565b868863ffffffff16565b879190611d11565b915091505b935093915050565b5f6105c8828461226a565b5f611b566002848418612289565b6105c8908484166121fc565b60605f611b6e83611d1e565b6040805160208082528183019092529192505f91906020820181803683375050509182525060208101929092525090565b6001600160a01b038316611bc9578060025f828254611bbe91906121fc565b90915550611c269050565b6001600160a01b0383165f9081526020819052604090205481811015611c085783818360405163391434e360e21b815260040161095e93929190612199565b6001600160a01b0384165f9081526020819052604090209082900390555b6001600160a01b038216611c4257600280548290039055611c60565b6001600160a01b0382165f9081526020819052604090208054820190555b816001600160a01b0316836001600160a01b03167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef83604051611ca591815260200190565b60405180910390a3505050565b6001600160a01b038316611cd457611cd1600a611b3d6114b284611ad2565b50505b6001600160a01b038216611cf657611cf3600a611ac76114b284611ad2565b50505b610b0a611d0284610716565b611d0b84610716565b83611454565b5f80611b30858585611d45565b5f60ff8216601f81111561054a57604051632cd44ac360e21b815260040160405180910390fd5b82545f9081908015611e3b575f611d61876110056001856121e9565b805490915065ffffffffffff80821691600160301b90046001600160d01b0316908816821115611da457604051632520601d60e01b815260040160405180910390fd5b8765ffffffffffff168265ffffffffffff1603611ddd57825465ffffffffffff16600160301b6001600160d01b03891602178355611e2d565b6040805180820190915265ffffffffffff808a1682526001600160d01b03808a1660208085019182528d54600181018f555f8f81529190912094519151909216600160301b029216919091179101555b9450859350611b3592505050565b50506040805180820190915265ffffffffffff80851682526001600160d01b0380851660208085019182528854600181018a555f8a815291822095519251909316600160301b029190931617920191909155905081611b35565b5f81518084528060208401602086015e5f602082860101526020601f19601f83011685010191505092915050565b602081525f6105c86020830184611e95565b80356001600160a01b0381168114610e09575f5ffd5b5f5f60408385031215611efc575f5ffd5b611f0583611ed5565b946020939093013593505050565b5f5f5f60608486031215611f25575f5ffd5b611f2e84611ed5565b9250611f3c60208501611ed5565b929592945050506040919091013590565b5f60208284031215611f5d575f5ffd5b6105c882611ed5565b6001600160a01b0391909116815260200190565b60ff60f81b8816815260e060208201525f611f9860e0830189611e95565b8281036040840152611faa8189611e95565b606084018890526001600160a01b038716608085015260a0840186905283810360c0850152845180825260208087019350909101905f5b81811015611fff578351835260209384019390920191600101611fe1565b50909b9a5050505050505050505050565b5f60208284031215612020575f5ffd5b5035919050565b60ff81168114610afa575f5ffd5b5f5f5f5f5f5f60c0878903121561204a575f5ffd5b61205387611ed5565b95506020870135945060408701359350606087013561207181612027565b9598949750929560808101359460a0909101359350915050565b5f5f5f5f5f5f5f60e0888a0312156120a1575f5ffd5b6120aa88611ed5565b96506120b860208901611ed5565b9550604088013594506060880135935060808801356120d681612027565b9699959850939692959460a0840135945060c09093013592915050565b5f5f60408385031215612104575f5ffd5b61210d83611ed5565b915061211b60208401611ed5565b90509250929050565b5f5f60408385031215612135575f5ffd5b61213e83611ed5565b9150602083013563ffffffff81168114612156575f5ffd5b809150509250929050565b600181811c9082168061217557607f821691505b60208210810361219357634e487b7160e01b5f52602260045260245ffd5b50919050565b6001600160a01b039390931683526020830191909152604082015260600190565b5f602082840312156121ca575f5ffd5b81516105c881612027565b634e487b7160e01b5f52601160045260245ffd5b8181038181111561054a5761054a6121d5565b8082018082111561054a5761054a6121d5565b634e487b7160e01b5f52601260045260245ffd5b634e487b7160e01b5f52602160045260245ffd5b634e487b7160e01b5f52603260045260245ffd5b6001600160d01b03828116828216039081111561054a5761054a6121d5565b6001600160d01b03818116838216019081111561054a5761054a6121d5565b5f826122a357634e487b7160e01b5f52601260045260245ffd5b50049056fea164736f6c634300081c000a", - "deployedBytecode": "0x608060405234801561000f575f5ffd5b50600436106101c1575f3560e01c8063715018a6116100f657806395d89b411161009a57806395d89b41146103e05780639ab24eb0146103e8578063a9059cbb146103fb578063a91ee0dc1461040e578063c3cda52014610421578063d505accf1461042f578063dd62ed3e14610442578063f1127ed814610455578063f2fde38b14610494575f5ffd5b8063715018a6146103415780637b103999146103495780637ecebe001461035c57806384b0196e1461036f57806385bc898c1461038a5780638da5cb5b1461039d5780638e539e8c146103ae57806391ddadf4146103c1575f5ffd5b80633644e515116101685780633644e515146102805780633a46b1a8146102885780634bf5d7e91461029b578063587cde1e146102a35780635c19a95c146102c357806368a9674d146102d65780636f307dc3146102e95780636fcfff45146102f157806370a0823114610319575f5ffd5b806306fdde03146101c5578063095ea7b3146101e3578063117de2fd1461020657806318160ddd1461021b578063205c28781461022d57806323b872dd146102405780632f4f21e214610253578063313ce56714610266575b5f5ffd5b6101cd6104a7565b6040516101da9190611ec3565b60405180910390f35b6101f66101f1366004611eeb565b610537565b60405190151581526020016101da565b610219610214366004611eeb565b610550565b005b6002545b6040519081526020016101da565b6101f661023b366004611eeb565b610591565b6101f661024e366004611f13565b6105cf565b6101f6610261366004611eeb565b6105f2565b61026e61064d565b60405160ff90911681526020016101da565b61021f61065b565b61021f610296366004611eeb565b610664565b6101cd61069e565b6102b66102b1366004611f4d565b610716565b6040516101da9190611f66565b6102196102d1366004611f4d565b610733565b6101f66102e4366004611f13565b61074c565b6102b66107c2565b6103046102ff366004611f4d565b6107e6565b60405163ffffffff90911681526020016101da565b61021f610327366004611f4d565b6001600160a01b03165f9081526020819052604090205490565b6102196107f0565b600c546102b6906001600160a01b031681565b61021f61036a366004611f4d565b610803565b61037761080d565b6040516101da9796959493929190611f7a565b610219610398366004611eeb565b61084f565b600b546001600160a01b03166102b6565b61021f6103bc366004612010565b610884565b6103c96108a8565b60405165ffffffffffff90911681526020016101da565b6101cd6108b1565b61021f6103f6366004611f4d565b6108c0565b6101f6610409366004611eeb565b6108e0565b61021961041c366004611f4d565b6108ed565b6102196102d1366004612035565b61021961043d36600461208b565b61093e565b61021f6104503660046120f3565b610a79565b610468610463366004612124565b610aa3565b60408051825165ffffffffffff1681526020928301516001600160d01b031692810192909252016101da565b6102196104a2366004611f4d565b610ac0565b6060600380546104b690612161565b80601f01602080910402602001604051908101604052809291908181526020018280546104e290612161565b801561052d5780601f106105045761010080835404028352916020019161052d565b820191905f5260205f20905b81548152906001019060200180831161051057829003601f168201915b5050505050905090565b5f33610544818585610afd565b60019150505b92915050565b600c546001600160a01b0316331461057b57604051633217675b60e21b815260040160405180910390fd5b61058d6105866107c2565b8383610b0f565b5050565b600c545f906001600160a01b031633146105be57604051633217675b60e21b815260040160405180910390fd5b6105c88383610b6e565b9392505050565b5f336105dc858285610bd8565b6105e7858585610c29565b506001949350505050565b600c545f906001600160a01b0316331461061f57604051633217675b60e21b815260040160405180910390fd5b6106298383610c86565b90505f61063584610716565b6001600160a01b03160361054a5761054a8384610d0b565b5f610656610d84565b905090565b5f610656610e0e565b5f61068e61067183610f37565b6001600160a01b0385165f90815260096020526040902090610f85565b6001600160d01b03169392505050565b60606106a8611035565b65ffffffffffff166106b86108a8565b65ffffffffffff16146106de576040516301bfc1c560e61b815260040160405180910390fd5b5060408051808201909152601d81527f6d6f64653d626c6f636b6e756d6265722666726f6d3d64656661756c74000000602082015290565b6001600160a01b039081165f908152600860205260409020541690565b604051635e81118160e11b815260040160405180910390fd5b600c545f906001600160a01b0316331461077957604051633217675b60e21b815260040160405180910390fd5b61078c6107846107c2565b85308561103f565b6107968383611078565b5f6107a084610716565b6001600160a01b0316036107b8576107b88384610d0b565b5060019392505050565b7f000000000000000000000000000000000000000000000000000000000000000090565b5f61054a826110ac565b6107f86110cd565b6108015f6110fa565b565b5f61054a8261114b565b5f6060805f5f5f606061081e611168565b610826611195565b604080515f80825260208201909252600f60f81b9b939a50919850469750309650945092509050565b600c546001600160a01b0316331461087a57604051633217675b60e21b815260040160405180910390fd5b61058d82826111c2565b5f61089961089183610f37565b600a90610f85565b6001600160d01b031692915050565b5f610656611035565b6060600480546104b690612161565b6001600160a01b0381165f908152600960205260408120610899906111f6565b5f33610544818585610c29565b6108f56110cd565b6001600160a01b03811661091c5760405163d92e233d60e01b815260040160405180910390fd5b600c80546001600160a01b0319166001600160a01b0392909216919091179055565b834211156109675760405163313c898160e11b8152600481018590526024015b60405180910390fd5b5f7f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c98888886109b28c6001600160a01b03165f90815260076020526040902080546001810190915590565b6040805160208101969096526001600160a01b0394851690860152929091166060840152608083015260a082015260c0810186905260e0016040516020818303038152906040528051906020012090505f610a0c8261122d565b90505f610a1b82878787611259565b9050896001600160a01b0316816001600160a01b031614610a62576040516325c0072360e11b81526001600160a01b0380831660048301528b16602482015260440161095e565b610a6d8a8a8a610afd565b50505050505050505050565b6001600160a01b039182165f90815260016020908152604080832093909416825291909152205490565b604080518082019091525f80825260208201526105c88383611285565b610ac86110cd565b6001600160a01b038116610af1575f604051631e4fbdf760e01b815260040161095e9190611f66565b610afa816110fa565b50565b610b0a83838360016112b9565b505050565b6040516001600160a01b03838116602483015260448201839052610b0a91859182169063a9059cbb906064015b604051602081830303815290604052915060e01b6020820180516001600160e01b03838183161783525050505061138b565b5f306001600160a01b03841603610b9a578260405163ec442f0560e01b815260040161095e9190611f66565b610ba433836111c2565b610bcf7f00000000000000000000000000000000000000000000000000000000000000008484610b0f565b50600192915050565b5f610be38484610a79565b90505f19811015610c235781811015610c1557828183604051637dc7a0d960e11b815260040161095e93929190612199565b610c2384848484035f6112b9565b50505050565b6001600160a01b038316610c52575f604051634b637e8f60e11b815260040161095e9190611f66565b6001600160a01b038216610c7b575f60405163ec442f0560e01b815260040161095e9190611f66565b610b0a8383836113ee565b5f33308103610caa5730604051634b637e8f60e11b815260040161095e9190611f66565b306001600160a01b03851603610cd5578360405163ec442f0560e01b815260040161095e9190611f66565b610d017f000000000000000000000000000000000000000000000000000000000000000082308661103f565b6105448484611078565b5f610d1583610716565b6001600160a01b038481165f8181526008602052604080822080546001600160a01b031916888616908117909155905194955093928516927f3134e8a2e6d97e929a7e54011ea5485d7d196dd5f0ba4d4ef95803e8e3fc257f9190a4610b0a8183610d7f86611437565b611454565b5f7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031663313ce5676040518163ffffffff1660e01b8152600401602060405180830381865afa925050508015610dff575060408051601f3d908101601f19168201909252610dfc918101906121ba565b60015b610e095750601290565b919050565b5f306001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016148015610e6657507f000000000000000000000000000000000000000000000000000000000000000046145b15610e9057507f000000000000000000000000000000000000000000000000000000000000000090565b610656604080517f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f60208201527f0000000000000000000000000000000000000000000000000000000000000000918101919091527f000000000000000000000000000000000000000000000000000000000000000060608201524660808201523060a08201525f9060c00160405160208183030381529060405280519060200120905090565b5f5f610f416108a8565b90508065ffffffffffff168310610f7c57604051637669fc0f60e11b81526004810184905265ffffffffffff8216602482015260440161095e565b6105c8836115bd565b81545f9081816005811115610fe1575f610f9e846115f3565b610fa890856121e9565b5f8881526020902090915081015465ffffffffffff9081169087161015610fd157809150610fdf565b610fdc8160016121fc565b92505b505b5f610fee87878585611746565b9050801561102857611012876110056001846121e9565b5f91825260209091200190565b54600160301b90046001600160d01b031661102a565b5f5b979650505050505050565b5f610656436115bd565b6040516001600160a01b038481166024830152838116604483015260648201839052610c239186918216906323b872dd90608401610b3c565b6001600160a01b0382166110a1575f60405163ec442f0560e01b815260040161095e9190611f66565b61058d5f83836113ee565b6001600160a01b0381165f9081526009602052604081205461054a906117a5565b600b546001600160a01b03163314610801573360405163118cdaa760e01b815260040161095e9190611f66565b600b80546001600160a01b038381166001600160a01b0319831681179093556040519116919082907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a35050565b6001600160a01b0381165f9081526007602052604081205461054a565b60606106567f000000000000000000000000000000000000000000000000000000000000000060056117d5565b60606106567f000000000000000000000000000000000000000000000000000000000000000060066117d5565b6001600160a01b0382166111eb575f604051634b637e8f60e11b815260040161095e9190611f66565b61058d825f836113ee565b80545f9080156112255761120f836110056001846121e9565b54600160301b90046001600160d01b03166105c8565b5f9392505050565b5f61054a611239610e0e565b8360405161190160f01b8152600281019290925260228201526042902090565b5f5f5f5f6112698888888861187e565b925092509250611279828261193c565b50909695505050505050565b604080518082019091525f80825260208201526001600160a01b0383165f9081526009602052604090206105c890836119f4565b6001600160a01b0384166112e2575f60405163e602df0560e01b815260040161095e9190611f66565b6001600160a01b03831661130b575f604051634a1406b160e11b815260040161095e9190611f66565b6001600160a01b038085165f9081526001602090815260408083209387168352929052208290558015610c2357826001600160a01b0316846001600160a01b03167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9258460405161137d91815260200190565b60405180910390a350505050565b5f5f60205f8451602086015f885af1806113aa576040513d5f823e3d81fd5b50505f513d915081156113c15780600114156113ce565b6001600160a01b0384163b155b15610c235783604051635274afe760e01b815260040161095e9190611f66565b6001600160a01b0383161580159061140e57506001600160a01b03821615155b1561142c57604051638cd22d1960e01b815260040160405180910390fd5b610b0a838383611a61565b6001600160a01b0381165f9081526020819052604081205461054a565b816001600160a01b0316836001600160a01b03161415801561147557505f81115b15610b0a576001600160a01b0383161561151c576001600160a01b0383165f90815260096020526040812081906114b790611ac76114b286611ad2565b611b05565b6001600160d01b031691506001600160d01b03169150846001600160a01b03167fdec2bacdd2f05b59de34da9b523dff8be42e5e38e818c82fdb0bae774387a7248383604051611511929190918252602082015260400190565b60405180910390a250505b6001600160a01b03821615610b0a576001600160a01b0382165f908152600960205260408120819061155490611b3d6114b286611ad2565b6001600160d01b031691506001600160d01b03169150836001600160a01b03167fdec2bacdd2f05b59de34da9b523dff8be42e5e38e818c82fdb0bae774387a72483836040516115ae929190918252602082015260400190565b60405180910390a25050505050565b5f65ffffffffffff8211156115ef576040516306dfcc6560e41b8152603060048201526024810183905260440161095e565b5090565b5f60018211611600575090565b816001600160801b82106116195760809190911c9060401b5b600160401b821061162f5760409190911c9060201b5b64010000000082106116465760209190911c9060101b5b62010000821061165b5760109190911c9060081b5b610100821061166f5760089190911c9060041b5b601082106116825760049190911c9060021b5b6004821061168e5760011b5b600302600190811c908185816116a6576116a661220f565b048201901c905060018185816116be576116be61220f565b048201901c905060018185816116d6576116d661220f565b048201901c905060018185816116ee576116ee61220f565b048201901c905060018185816117065761170661220f565b048201901c9050600181858161171e5761171e61220f565b048201901c905061173d8185816117375761173761220f565b04821190565b90039392505050565b5f5b8183101561179d575f61175b8484611b48565b5f8781526020902090915065ffffffffffff86169082015465ffffffffffff16111561178957809250611797565b6117948160016121fc565b93505b50611748565b509392505050565b5f63ffffffff8211156115ef576040516306dfcc6560e41b8152602060048201526024810183905260440161095e565b606060ff83146117ef576117e883611b62565b905061054a565b8180546117fb90612161565b80601f016020809104026020016040519081016040528092919081815260200182805461182790612161565b80156118725780601f1061184957610100808354040283529160200191611872565b820191905f5260205f20905b81548152906001019060200180831161185557829003601f168201915b5050505050905061054a565b5f80806fa2a8918ca85bafe22016d0b997e4df60600160ff1b038411156118ad57505f91506003905082611932565b604080515f808252602082018084528a905260ff891692820192909252606081018790526080810186905260019060a0016020604051602081039080840390855afa1580156118fe573d5f5f3e3d5ffd5b5050604051601f1901519150506001600160a01b03811661192957505f925060019150829050611932565b92505f91508190505b9450945094915050565b5f82600381111561194f5761194f612223565b03611958575050565b600182600381111561196c5761196c612223565b0361198a5760405163f645eedf60e01b815260040160405180910390fd5b600282600381111561199e5761199e612223565b036119bf5760405163fce698f760e01b81526004810182905260240161095e565b60038260038111156119d3576119d3612223565b0361058d576040516335e2f38360e21b81526004810182905260240161095e565b604080518082019091525f8082526020820152825f018263ffffffff1681548110611a2157611a21612237565b5f9182526020918290206040805180820190915291015465ffffffffffff81168252600160301b90046001600160d01b0316918101919091529392505050565b611a6c838383611b9f565b6001600160a01b038316611abc575f611a8460025490565b90506001600160d01b0380821115611ab957604051630e58ae9360e11b8152600481018390526024810182905260440161095e565b50505b610b0a838383611cb2565b5f6105c8828461224b565b5f6001600160d01b038211156115ef576040516306dfcc6560e41b815260d060048201526024810183905260440161095e565b5f5f611b30611b126108a8565b611b28611b1e886111f6565b868863ffffffff16565b879190611d11565b915091505b935093915050565b5f6105c8828461226a565b5f611b566002848418612289565b6105c8908484166121fc565b60605f611b6e83611d1e565b6040805160208082528183019092529192505f91906020820181803683375050509182525060208101929092525090565b6001600160a01b038316611bc9578060025f828254611bbe91906121fc565b90915550611c269050565b6001600160a01b0383165f9081526020819052604090205481811015611c085783818360405163391434e360e21b815260040161095e93929190612199565b6001600160a01b0384165f9081526020819052604090209082900390555b6001600160a01b038216611c4257600280548290039055611c60565b6001600160a01b0382165f9081526020819052604090208054820190555b816001600160a01b0316836001600160a01b03167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef83604051611ca591815260200190565b60405180910390a3505050565b6001600160a01b038316611cd457611cd1600a611b3d6114b284611ad2565b50505b6001600160a01b038216611cf657611cf3600a611ac76114b284611ad2565b50505b610b0a611d0284610716565b611d0b84610716565b83611454565b5f80611b30858585611d45565b5f60ff8216601f81111561054a57604051632cd44ac360e21b815260040160405180910390fd5b82545f9081908015611e3b575f611d61876110056001856121e9565b805490915065ffffffffffff80821691600160301b90046001600160d01b0316908816821115611da457604051632520601d60e01b815260040160405180910390fd5b8765ffffffffffff168265ffffffffffff1603611ddd57825465ffffffffffff16600160301b6001600160d01b03891602178355611e2d565b6040805180820190915265ffffffffffff808a1682526001600160d01b03808a1660208085019182528d54600181018f555f8f81529190912094519151909216600160301b029216919091179101555b9450859350611b3592505050565b50506040805180820190915265ffffffffffff80851682526001600160d01b0380851660208085019182528854600181018a555f8a815291822095519251909316600160301b029190931617920191909155905081611b35565b5f81518084528060208401602086015e5f602082860101526020601f19601f83011685010191505092915050565b602081525f6105c86020830184611e95565b80356001600160a01b0381168114610e09575f5ffd5b5f5f60408385031215611efc575f5ffd5b611f0583611ed5565b946020939093013593505050565b5f5f5f60608486031215611f25575f5ffd5b611f2e84611ed5565b9250611f3c60208501611ed5565b929592945050506040919091013590565b5f60208284031215611f5d575f5ffd5b6105c882611ed5565b6001600160a01b0391909116815260200190565b60ff60f81b8816815260e060208201525f611f9860e0830189611e95565b8281036040840152611faa8189611e95565b606084018890526001600160a01b038716608085015260a0840186905283810360c0850152845180825260208087019350909101905f5b81811015611fff578351835260209384019390920191600101611fe1565b50909b9a5050505050505050505050565b5f60208284031215612020575f5ffd5b5035919050565b60ff81168114610afa575f5ffd5b5f5f5f5f5f5f60c0878903121561204a575f5ffd5b61205387611ed5565b95506020870135945060408701359350606087013561207181612027565b9598949750929560808101359460a0909101359350915050565b5f5f5f5f5f5f5f60e0888a0312156120a1575f5ffd5b6120aa88611ed5565b96506120b860208901611ed5565b9550604088013594506060880135935060808801356120d681612027565b9699959850939692959460a0840135945060c09093013592915050565b5f5f60408385031215612104575f5ffd5b61210d83611ed5565b915061211b60208401611ed5565b90509250929050565b5f5f60408385031215612135575f5ffd5b61213e83611ed5565b9150602083013563ffffffff81168114612156575f5ffd5b809150509250929050565b600181811c9082168061217557607f821691505b60208210810361219357634e487b7160e01b5f52602260045260245ffd5b50919050565b6001600160a01b039390931683526020830191909152604082015260600190565b5f602082840312156121ca575f5ffd5b81516105c881612027565b634e487b7160e01b5f52601160045260245ffd5b8181038181111561054a5761054a6121d5565b8082018082111561054a5761054a6121d5565b634e487b7160e01b5f52601260045260245ffd5b634e487b7160e01b5f52602160045260245ffd5b634e487b7160e01b5f52603260045260245ffd5b6001600160d01b03828116828216039081111561054a5761054a6121d5565b6001600160d01b03818116838216019081111561054a5761054a6121d5565b5f826122a357634e487b7160e01b5f52601260045260245ffd5b50049056fea164736f6c634300081c000a", + "bytecode": "0x610180604052348015610010575f5ffd5b5060405161292e38038061292e83398101604081905261002f9161037d565b82816040518060400160405280601481526020017f456e636c617665205469636b657420546f6b656e00000000000000000000000081525080604051806040016040528060018152602001603160f81b8152506040518060400160405280601481526020017f456e636c617665205469636b657420546f6b656e0000000000000000000000008152506040518060400160405280600381526020016245544b60e81b81525081600390816100e3919061045f565b5060046100f0828261045f565b5061010091508390506005610226565b6101205261010f816006610226565b61014052815160208084019190912060e052815190820120610100524660a05261019b60e05161010051604080517f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f60208201529081019290925260608201524660808201523060a08201525f9060c00160405160208183030381529060405280519060200120905090565b60805250503060c052506001600160a01b0381166101d357604051631e4fbdf760e01b81525f60048201526024015b60405180910390fd5b6101dc81610258565b50306001600160a01b038216036102085760405163438d6fe360e01b81523060048201526024016101ca565b6001600160a01b03166101605261021e826102a9565b505050610571565b5f6020835110156102415761023a836102fa565b9050610252565b8161024c848261045f565b5060ff90505b92915050565b600b80546001600160a01b038381166001600160a01b0319831681179093556040519116919082907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a35050565b6102b1610337565b6001600160a01b0381166102d85760405163d92e233d60e01b815260040160405180910390fd5b600c80546001600160a01b0319166001600160a01b0392909216919091179055565b5f5f829050601f81511115610324578260405163305a27a960e01b81526004016101ca9190610519565b805161032f8261054e565b179392505050565b600b546001600160a01b031633146103645760405163118cdaa760e01b81523360048201526024016101ca565b565b6001600160a01b038116811461037a575f5ffd5b50565b5f5f5f6060848603121561038f575f5ffd5b835161039a81610366565b60208501519093506103ab81610366565b60408501519092506103bc81610366565b809150509250925092565b634e487b7160e01b5f52604160045260245ffd5b600181811c908216806103ef57607f821691505b60208210810361040d57634e487b7160e01b5f52602260045260245ffd5b50919050565b601f82111561045a57805f5260205f20601f840160051c810160208510156104385750805b601f840160051c820191505b81811015610457575f8155600101610444565b50505b505050565b81516001600160401b03811115610478576104786103c7565b61048c8161048684546103db565b84610413565b6020601f8211600181146104be575f83156104a75750848201515b5f19600385901b1c1916600184901b178455610457565b5f84815260208120601f198516915b828110156104ed57878501518255602094850194600190920191016104cd565b508482101561050a57868401515f19600387901b60f8161c191681555b50505050600190811b01905550565b602081525f82518060208401528060208501604085015e5f604082850101526040601f19601f83011684010191505092915050565b8051602080830151919081101561040d575f1960209190910360031b1b16919050565b60805160a05160c05160e0516101005161012051610140516101605161234d6105e15f395f818161084301528181610c2d01528181610d5e0152610e0b01525f61122701525f6111fa01525f610f6b01525f610f4301525f610e9e01525f610ec801525f610ef2015261234d5ff3fe608060405234801561000f575f5ffd5b50600436106101cc575f3560e01c806370a082311161010157806395d89b411161009a57806395d89b41146103f45780639ab24eb0146103fc578063a9059cbb1461040f578063a91ee0dc14610422578063c3cda52014610435578063d505accf14610443578063dd62ed3e14610456578063f1127ed814610469578063f2fde38b146104a8575f5ffd5b806370a082311461032d578063715018a6146103555780637b1039991461035d5780637ecebe001461037057806384b0196e1461038357806385bc898c1461039e5780638da5cb5b146103b15780638e539e8c146103c257806391ddadf4146103d5575f5ffd5b80633644e515116101735780633644e5151461028b5780633a46b1a81461029357806344b279a2146102a65780634bf5d7e9146102af578063587cde1e146102b75780635c19a95c146102d757806368a9674d146102ea5780636f307dc3146102fd5780636fcfff4514610305575f5ffd5b806306fdde03146101d0578063095ea7b3146101ee578063117de2fd1461021157806318160ddd14610226578063205c28781461023857806323b872dd1461024b5780632f4f21e21461025e578063313ce56714610271575b5f5ffd5b6101d86104bb565b6040516101e59190611f5b565b60405180910390f35b6102016101fc366004611f83565b61054b565b60405190151581526020016101e5565b61022461021f366004611f83565b610565565b005b6002545b6040519081526020016101e5565b610201610246366004611f83565b61060e565b610201610259366004611fab565b61064e565b61020161026c366004611f83565b610671565b6102796106cc565b60405160ff90911681526020016101e5565b61022a6106da565b61022a6102a1366004611f83565b6106e3565b61022a600d5481565b6101d861071d565b6102ca6102c5366004611fe5565b610795565b6040516101e59190611ffe565b6102246102e5366004611fe5565b6107b2565b6102016102f8366004611fab565b6107cb565b6102ca610841565b610318610313366004611fe5565b610865565b60405163ffffffff90911681526020016101e5565b61022a61033b366004611fe5565b6001600160a01b03165f9081526020819052604090205490565b61022461086f565b600c546102ca906001600160a01b031681565b61022a61037e366004611fe5565b610882565b61038b61088c565b6040516101e59796959493929190612012565b6102246103ac366004611f83565b6108ce565b600b546001600160a01b03166102ca565b61022a6103d03660046120a8565b61091a565b6103dd61093e565b60405165ffffffffffff90911681526020016101e5565b6101d8610947565b61022a61040a366004611fe5565b610956565b61020161041d366004611f83565b610976565b610224610430366004611fe5565b610983565b6102246102e53660046120cd565b610224610451366004612123565b6109d4565b61022a61046436600461218b565b610b0a565b61047c6104773660046121bc565b610b34565b60408051825165ffffffffffff1681526020928301516001600160d01b031692810192909252016101e5565b6102246104b6366004611fe5565b610b51565b6060600380546104ca906121f9565b80601f01602080910402602001604051908101604052809291908181526020018280546104f6906121f9565b80156105415780601f1061051857610100808354040283529160200191610541565b820191905f5260205f20905b81548152906001019060200180831161052457829003601f168201915b5050505050905090565b5f604051638cd22d1960e01b815260040160405180910390fd5b600c546001600160a01b0316331461059057604051633217675b60e21b815260040160405180910390fd5b600d548111156105e15760405162461bcd60e51b8152602060048201526017602482015276457863656564732070617961626c652062616c616e636560481b60448201526064015b60405180910390fd5b80600d5f8282546105f29190612245565b9091555061060a9050610603610841565b8383610b8e565b5050565b600c545f906001600160a01b0316331461063b57604051633217675b60e21b815260040160405180910390fd5b6106458383610bf2565b90505b92915050565b5f3361065b858285610c5c565b610666858585610cad565b506001949350505050565b600c545f906001600160a01b0316331461069e57604051633217675b60e21b815260040160405180910390fd5b6106a88383610d0a565b90505f6106b484610795565b6001600160a01b031603610648576106488384610d8f565b5f6106d5610e08565b905090565b5f6106d5610e92565b5f61070d6106f083610fbb565b6001600160a01b0385165f90815260096020526040902090611010565b6001600160d01b03169392505050565b60606107276110c0565b65ffffffffffff1661073761093e565b65ffffffffffff161461075d576040516301bfc1c560e61b815260040160405180910390fd5b5060408051808201909152601d81527f6d6f64653d626c6f636b6e756d6265722666726f6d3d64656661756c74000000602082015290565b6001600160a01b039081165f908152600860205260409020541690565b604051635e81118160e11b815260040160405180910390fd5b600c545f906001600160a01b031633146107f857604051633217675b60e21b815260040160405180910390fd5b61080b610803610841565b8530856110ca565b6108158383611103565b5f61081f84610795565b6001600160a01b031603610837576108378384610d8f565b5060019392505050565b7f000000000000000000000000000000000000000000000000000000000000000090565b5f61064882611137565b610877611158565b6108805f611185565b565b5f610648826111d6565b5f6060805f5f5f606061089d6111f3565b6108a5611220565b604080515f80825260208201909252600f60f81b9b939a50919850469750309650945092509050565b600c546001600160a01b031633146108f957604051633217675b60e21b815260040160405180910390fd5b80600d5f82825461090a9190612258565b9091555061060a9050828261124d565b5f61092f61092783610fbb565b600a90611010565b6001600160d01b031692915050565b5f6106d56110c0565b6060600480546104ca906121f9565b6001600160a01b0381165f90815260096020526040812061092f90611281565b5f33610837818585610cad565b61098b611158565b6001600160a01b0381166109b25760405163d92e233d60e01b815260040160405180910390fd5b600c80546001600160a01b0319166001600160a01b0392909216919091179055565b834211156109f85760405163313c898160e11b8152600481018590526024016105d8565b5f7f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9888888610a438c6001600160a01b03165f90815260076020526040902080546001810190915590565b6040805160208101969096526001600160a01b0394851690860152929091166060840152608083015260a082015260c0810186905260e0016040516020818303038152906040528051906020012090505f610a9d826112b8565b90505f610aac828787876112e4565b9050896001600160a01b0316816001600160a01b031614610af3576040516325c0072360e11b81526001600160a01b0380831660048301528b1660248201526044016105d8565b610afe8a8a8a611310565b50505050505050505050565b6001600160a01b039182165f90815260016020908152604080832093909416825291909152205490565b604080518082019091525f8082526020820152610645838361131d565b610b59611158565b6001600160a01b038116610b82575f604051631e4fbdf760e01b81526004016105d89190611ffe565b610b8b81611185565b50565b6040516001600160a01b03838116602483015260448201839052610bed91859182169063a9059cbb906064015b604051602081830303815290604052915060e01b6020820180516001600160e01b038381831617835250505050611351565b505050565b5f306001600160a01b03841603610c1e578260405163ec442f0560e01b81526004016105d89190611ffe565b610c28338361124d565b610c537f00000000000000000000000000000000000000000000000000000000000000008484610b8e565b50600192915050565b5f610c678484610b0a565b90505f19811015610ca75781811015610c9957828183604051637dc7a0d960e11b81526004016105d89392919061226b565b610ca784848484035f6113b4565b50505050565b6001600160a01b038316610cd6575f604051634b637e8f60e11b81526004016105d89190611ffe565b6001600160a01b038216610cff575f60405163ec442f0560e01b81526004016105d89190611ffe565b610bed838383611486565b5f33308103610d2e5730604051634b637e8f60e11b81526004016105d89190611ffe565b306001600160a01b03851603610d59578360405163ec442f0560e01b81526004016105d89190611ffe565b610d857f00000000000000000000000000000000000000000000000000000000000000008230866110ca565b6108378484611103565b5f610d9983610795565b6001600160a01b038481165f8181526008602052604080822080546001600160a01b031916888616908117909155905194955093928516927f3134e8a2e6d97e929a7e54011ea5485d7d196dd5f0ba4d4ef95803e8e3fc257f9190a4610bed8183610e03866114cf565b6114ec565b5f7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031663313ce5676040518163ffffffff1660e01b8152600401602060405180830381865afa925050508015610e83575060408051601f3d908101601f19168201909252610e809181019061228c565b60015b610e8d5750601290565b919050565b5f306001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016148015610eea57507f000000000000000000000000000000000000000000000000000000000000000046145b15610f1457507f000000000000000000000000000000000000000000000000000000000000000090565b6106d5604080517f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f60208201527f0000000000000000000000000000000000000000000000000000000000000000918101919091527f000000000000000000000000000000000000000000000000000000000000000060608201524660808201523060a08201525f9060c00160405160208183030381529060405280519060200120905090565b5f5f610fc561093e565b90508065ffffffffffff16831061100057604051637669fc0f60e11b81526004810184905265ffffffffffff821660248201526044016105d8565b61100983611655565b9392505050565b81545f908181600581111561106c575f6110298461168b565b6110339085612245565b5f8881526020902090915081015465ffffffffffff908116908716101561105c5780915061106a565b611067816001612258565b92505b505b5f611079878785856117de565b905080156110b35761109d87611090600184612245565b5f91825260209091200190565b54600160301b90046001600160d01b03166110b5565b5f5b979650505050505050565b5f6106d543611655565b6040516001600160a01b038481166024830152838116604483015260648201839052610ca79186918216906323b872dd90608401610bbb565b6001600160a01b03821661112c575f60405163ec442f0560e01b81526004016105d89190611ffe565b61060a5f8383611486565b6001600160a01b0381165f908152600960205260408120546106489061183d565b600b546001600160a01b03163314610880573360405163118cdaa760e01b81526004016105d89190611ffe565b600b80546001600160a01b038381166001600160a01b0319831681179093556040519116919082907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a35050565b6001600160a01b0381165f90815260076020526040812054610648565b60606106d57f0000000000000000000000000000000000000000000000000000000000000000600561186d565b60606106d57f0000000000000000000000000000000000000000000000000000000000000000600661186d565b6001600160a01b038216611276575f604051634b637e8f60e11b81526004016105d89190611ffe565b61060a825f83611486565b80545f9080156112b05761129a83611090600184612245565b54600160301b90046001600160d01b0316611009565b5f9392505050565b5f6106486112c4610e92565b8360405161190160f01b8152600281019290925260228201526042902090565b5f5f5f5f6112f488888888611916565b92509250925061130482826119d4565b50909695505050505050565b610bed83838360016113b4565b604080518082019091525f80825260208201526001600160a01b0383165f9081526009602052604090206106459083611a8c565b5f5f60205f8451602086015f885af180611370576040513d5f823e3d81fd5b50505f513d91508115611387578060011415611394565b6001600160a01b0384163b155b15610ca75783604051635274afe760e01b81526004016105d89190611ffe565b6001600160a01b0384166113dd575f60405163e602df0560e01b81526004016105d89190611ffe565b6001600160a01b038316611406575f604051634a1406b160e11b81526004016105d89190611ffe565b6001600160a01b038085165f9081526001602090815260408083209387168352929052208290558015610ca757826001600160a01b0316846001600160a01b03167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9258460405161147891815260200190565b60405180910390a350505050565b6001600160a01b038316158015906114a657506001600160a01b03821615155b156114c457604051638cd22d1960e01b815260040160405180910390fd5b610bed838383611af9565b6001600160a01b0381165f90815260208190526040812054610648565b816001600160a01b0316836001600160a01b03161415801561150d57505f81115b15610bed576001600160a01b038316156115b4576001600160a01b0383165f908152600960205260408120819061154f90611b5f61154a86611b6a565b611b9d565b6001600160d01b031691506001600160d01b03169150846001600160a01b03167fdec2bacdd2f05b59de34da9b523dff8be42e5e38e818c82fdb0bae774387a72483836040516115a9929190918252602082015260400190565b60405180910390a250505b6001600160a01b03821615610bed576001600160a01b0382165f90815260096020526040812081906115ec90611bd561154a86611b6a565b6001600160d01b031691506001600160d01b03169150836001600160a01b03167fdec2bacdd2f05b59de34da9b523dff8be42e5e38e818c82fdb0bae774387a7248383604051611646929190918252602082015260400190565b60405180910390a25050505050565b5f65ffffffffffff821115611687576040516306dfcc6560e41b815260306004820152602481018390526044016105d8565b5090565b5f60018211611698575090565b816001600160801b82106116b15760809190911c9060401b5b600160401b82106116c75760409190911c9060201b5b64010000000082106116de5760209190911c9060101b5b6201000082106116f35760109190911c9060081b5b61010082106117075760089190911c9060041b5b6010821061171a5760049190911c9060021b5b600482106117265760011b5b600302600190811c9081858161173e5761173e6122a7565b048201901c90506001818581611756576117566122a7565b048201901c9050600181858161176e5761176e6122a7565b048201901c90506001818581611786576117866122a7565b048201901c9050600181858161179e5761179e6122a7565b048201901c905060018185816117b6576117b66122a7565b048201901c90506117d58185816117cf576117cf6122a7565b04821190565b90039392505050565b5f5b81831015611835575f6117f38484611be0565b5f8781526020902090915065ffffffffffff86169082015465ffffffffffff1611156118215780925061182f565b61182c816001612258565b93505b506117e0565b509392505050565b5f63ffffffff821115611687576040516306dfcc6560e41b815260206004820152602481018390526044016105d8565b606060ff83146118875761188083611bfa565b9050610648565b818054611893906121f9565b80601f01602080910402602001604051908101604052809291908181526020018280546118bf906121f9565b801561190a5780601f106118e15761010080835404028352916020019161190a565b820191905f5260205f20905b8154815290600101906020018083116118ed57829003601f168201915b50505050509050610648565b5f80806fa2a8918ca85bafe22016d0b997e4df60600160ff1b0384111561194557505f915060039050826119ca565b604080515f808252602082018084528a905260ff891692820192909252606081018790526080810186905260019060a0016020604051602081039080840390855afa158015611996573d5f5f3e3d5ffd5b5050604051601f1901519150506001600160a01b0381166119c157505f9250600191508290506119ca565b92505f91508190505b9450945094915050565b5f8260038111156119e7576119e76122bb565b036119f0575050565b6001826003811115611a0457611a046122bb565b03611a225760405163f645eedf60e01b815260040160405180910390fd5b6002826003811115611a3657611a366122bb565b03611a575760405163fce698f760e01b8152600481018290526024016105d8565b6003826003811115611a6b57611a6b6122bb565b0361060a576040516335e2f38360e21b8152600481018290526024016105d8565b604080518082019091525f8082526020820152825f018263ffffffff1681548110611ab957611ab96122cf565b5f9182526020918290206040805180820190915291015465ffffffffffff81168252600160301b90046001600160d01b0316918101919091529392505050565b611b04838383611c37565b6001600160a01b038316611b54575f611b1c60025490565b90506001600160d01b0380821115611b5157604051630e58ae9360e11b815260048101839052602481018290526044016105d8565b50505b610bed838383611d4a565b5f61064582846122e3565b5f6001600160d01b03821115611687576040516306dfcc6560e41b815260d06004820152602481018390526044016105d8565b5f5f611bc8611baa61093e565b611bc0611bb688611281565b868863ffffffff16565b879190611da9565b915091505b935093915050565b5f6106458284612302565b5f611bee6002848418612321565b61064590848416612258565b60605f611c0683611db6565b6040805160208082528183019092529192505f91906020820181803683375050509182525060208101929092525090565b6001600160a01b038316611c61578060025f828254611c569190612258565b90915550611cbe9050565b6001600160a01b0383165f9081526020819052604090205481811015611ca05783818360405163391434e360e21b81526004016105d89392919061226b565b6001600160a01b0384165f9081526020819052604090209082900390555b6001600160a01b038216611cda57600280548290039055611cf8565b6001600160a01b0382165f9081526020819052604090208054820190555b816001600160a01b0316836001600160a01b03167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef83604051611d3d91815260200190565b60405180910390a3505050565b6001600160a01b038316611d6c57611d69600a611bd561154a84611b6a565b50505b6001600160a01b038216611d8e57611d8b600a611b5f61154a84611b6a565b50505b610bed611d9a84610795565b611da384610795565b836114ec565b5f80611bc8858585611ddd565b5f60ff8216601f81111561064857604051632cd44ac360e21b815260040160405180910390fd5b82545f9081908015611ed3575f611df987611090600185612245565b805490915065ffffffffffff80821691600160301b90046001600160d01b0316908816821115611e3c57604051632520601d60e01b815260040160405180910390fd5b8765ffffffffffff168265ffffffffffff1603611e7557825465ffffffffffff16600160301b6001600160d01b03891602178355611ec5565b6040805180820190915265ffffffffffff808a1682526001600160d01b03808a1660208085019182528d54600181018f555f8f81529190912094519151909216600160301b029216919091179101555b9450859350611bcd92505050565b50506040805180820190915265ffffffffffff80851682526001600160d01b0380851660208085019182528854600181018a555f8a815291822095519251909316600160301b029190931617920191909155905081611bcd565b5f81518084528060208401602086015e5f602082860101526020601f19601f83011685010191505092915050565b602081525f6106456020830184611f2d565b80356001600160a01b0381168114610e8d575f5ffd5b5f5f60408385031215611f94575f5ffd5b611f9d83611f6d565b946020939093013593505050565b5f5f5f60608486031215611fbd575f5ffd5b611fc684611f6d565b9250611fd460208501611f6d565b929592945050506040919091013590565b5f60208284031215611ff5575f5ffd5b61064582611f6d565b6001600160a01b0391909116815260200190565b60ff60f81b8816815260e060208201525f61203060e0830189611f2d565b82810360408401526120428189611f2d565b606084018890526001600160a01b038716608085015260a0840186905283810360c0850152845180825260208087019350909101905f5b81811015612097578351835260209384019390920191600101612079565b50909b9a5050505050505050505050565b5f602082840312156120b8575f5ffd5b5035919050565b60ff81168114610b8b575f5ffd5b5f5f5f5f5f5f60c087890312156120e2575f5ffd5b6120eb87611f6d565b955060208701359450604087013593506060870135612109816120bf565b9598949750929560808101359460a0909101359350915050565b5f5f5f5f5f5f5f60e0888a031215612139575f5ffd5b61214288611f6d565b965061215060208901611f6d565b95506040880135945060608801359350608088013561216e816120bf565b9699959850939692959460a0840135945060c09093013592915050565b5f5f6040838503121561219c575f5ffd5b6121a583611f6d565b91506121b360208401611f6d565b90509250929050565b5f5f604083850312156121cd575f5ffd5b6121d683611f6d565b9150602083013563ffffffff811681146121ee575f5ffd5b809150509250929050565b600181811c9082168061220d57607f821691505b60208210810361222b57634e487b7160e01b5f52602260045260245ffd5b50919050565b634e487b7160e01b5f52601160045260245ffd5b8181038181111561064857610648612231565b8082018082111561064857610648612231565b6001600160a01b039390931683526020830191909152604082015260600190565b5f6020828403121561229c575f5ffd5b8151611009816120bf565b634e487b7160e01b5f52601260045260245ffd5b634e487b7160e01b5f52602160045260245ffd5b634e487b7160e01b5f52603260045260245ffd5b6001600160d01b03828116828216039081111561064857610648612231565b6001600160d01b03818116838216019081111561064857610648612231565b5f8261233b57634e487b7160e01b5f52601260045260245ffd5b50049056fea164736f6c634300081c000a", + "deployedBytecode": "0x608060405234801561000f575f5ffd5b50600436106101cc575f3560e01c806370a082311161010157806395d89b411161009a57806395d89b41146103f45780639ab24eb0146103fc578063a9059cbb1461040f578063a91ee0dc14610422578063c3cda52014610435578063d505accf14610443578063dd62ed3e14610456578063f1127ed814610469578063f2fde38b146104a8575f5ffd5b806370a082311461032d578063715018a6146103555780637b1039991461035d5780637ecebe001461037057806384b0196e1461038357806385bc898c1461039e5780638da5cb5b146103b15780638e539e8c146103c257806391ddadf4146103d5575f5ffd5b80633644e515116101735780633644e5151461028b5780633a46b1a81461029357806344b279a2146102a65780634bf5d7e9146102af578063587cde1e146102b75780635c19a95c146102d757806368a9674d146102ea5780636f307dc3146102fd5780636fcfff4514610305575f5ffd5b806306fdde03146101d0578063095ea7b3146101ee578063117de2fd1461021157806318160ddd14610226578063205c28781461023857806323b872dd1461024b5780632f4f21e21461025e578063313ce56714610271575b5f5ffd5b6101d86104bb565b6040516101e59190611f5b565b60405180910390f35b6102016101fc366004611f83565b61054b565b60405190151581526020016101e5565b61022461021f366004611f83565b610565565b005b6002545b6040519081526020016101e5565b610201610246366004611f83565b61060e565b610201610259366004611fab565b61064e565b61020161026c366004611f83565b610671565b6102796106cc565b60405160ff90911681526020016101e5565b61022a6106da565b61022a6102a1366004611f83565b6106e3565b61022a600d5481565b6101d861071d565b6102ca6102c5366004611fe5565b610795565b6040516101e59190611ffe565b6102246102e5366004611fe5565b6107b2565b6102016102f8366004611fab565b6107cb565b6102ca610841565b610318610313366004611fe5565b610865565b60405163ffffffff90911681526020016101e5565b61022a61033b366004611fe5565b6001600160a01b03165f9081526020819052604090205490565b61022461086f565b600c546102ca906001600160a01b031681565b61022a61037e366004611fe5565b610882565b61038b61088c565b6040516101e59796959493929190612012565b6102246103ac366004611f83565b6108ce565b600b546001600160a01b03166102ca565b61022a6103d03660046120a8565b61091a565b6103dd61093e565b60405165ffffffffffff90911681526020016101e5565b6101d8610947565b61022a61040a366004611fe5565b610956565b61020161041d366004611f83565b610976565b610224610430366004611fe5565b610983565b6102246102e53660046120cd565b610224610451366004612123565b6109d4565b61022a61046436600461218b565b610b0a565b61047c6104773660046121bc565b610b34565b60408051825165ffffffffffff1681526020928301516001600160d01b031692810192909252016101e5565b6102246104b6366004611fe5565b610b51565b6060600380546104ca906121f9565b80601f01602080910402602001604051908101604052809291908181526020018280546104f6906121f9565b80156105415780601f1061051857610100808354040283529160200191610541565b820191905f5260205f20905b81548152906001019060200180831161052457829003601f168201915b5050505050905090565b5f604051638cd22d1960e01b815260040160405180910390fd5b600c546001600160a01b0316331461059057604051633217675b60e21b815260040160405180910390fd5b600d548111156105e15760405162461bcd60e51b8152602060048201526017602482015276457863656564732070617961626c652062616c616e636560481b60448201526064015b60405180910390fd5b80600d5f8282546105f29190612245565b9091555061060a9050610603610841565b8383610b8e565b5050565b600c545f906001600160a01b0316331461063b57604051633217675b60e21b815260040160405180910390fd5b6106458383610bf2565b90505b92915050565b5f3361065b858285610c5c565b610666858585610cad565b506001949350505050565b600c545f906001600160a01b0316331461069e57604051633217675b60e21b815260040160405180910390fd5b6106a88383610d0a565b90505f6106b484610795565b6001600160a01b031603610648576106488384610d8f565b5f6106d5610e08565b905090565b5f6106d5610e92565b5f61070d6106f083610fbb565b6001600160a01b0385165f90815260096020526040902090611010565b6001600160d01b03169392505050565b60606107276110c0565b65ffffffffffff1661073761093e565b65ffffffffffff161461075d576040516301bfc1c560e61b815260040160405180910390fd5b5060408051808201909152601d81527f6d6f64653d626c6f636b6e756d6265722666726f6d3d64656661756c74000000602082015290565b6001600160a01b039081165f908152600860205260409020541690565b604051635e81118160e11b815260040160405180910390fd5b600c545f906001600160a01b031633146107f857604051633217675b60e21b815260040160405180910390fd5b61080b610803610841565b8530856110ca565b6108158383611103565b5f61081f84610795565b6001600160a01b031603610837576108378384610d8f565b5060019392505050565b7f000000000000000000000000000000000000000000000000000000000000000090565b5f61064882611137565b610877611158565b6108805f611185565b565b5f610648826111d6565b5f6060805f5f5f606061089d6111f3565b6108a5611220565b604080515f80825260208201909252600f60f81b9b939a50919850469750309650945092509050565b600c546001600160a01b031633146108f957604051633217675b60e21b815260040160405180910390fd5b80600d5f82825461090a9190612258565b9091555061060a9050828261124d565b5f61092f61092783610fbb565b600a90611010565b6001600160d01b031692915050565b5f6106d56110c0565b6060600480546104ca906121f9565b6001600160a01b0381165f90815260096020526040812061092f90611281565b5f33610837818585610cad565b61098b611158565b6001600160a01b0381166109b25760405163d92e233d60e01b815260040160405180910390fd5b600c80546001600160a01b0319166001600160a01b0392909216919091179055565b834211156109f85760405163313c898160e11b8152600481018590526024016105d8565b5f7f6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9888888610a438c6001600160a01b03165f90815260076020526040902080546001810190915590565b6040805160208101969096526001600160a01b0394851690860152929091166060840152608083015260a082015260c0810186905260e0016040516020818303038152906040528051906020012090505f610a9d826112b8565b90505f610aac828787876112e4565b9050896001600160a01b0316816001600160a01b031614610af3576040516325c0072360e11b81526001600160a01b0380831660048301528b1660248201526044016105d8565b610afe8a8a8a611310565b50505050505050505050565b6001600160a01b039182165f90815260016020908152604080832093909416825291909152205490565b604080518082019091525f8082526020820152610645838361131d565b610b59611158565b6001600160a01b038116610b82575f604051631e4fbdf760e01b81526004016105d89190611ffe565b610b8b81611185565b50565b6040516001600160a01b03838116602483015260448201839052610bed91859182169063a9059cbb906064015b604051602081830303815290604052915060e01b6020820180516001600160e01b038381831617835250505050611351565b505050565b5f306001600160a01b03841603610c1e578260405163ec442f0560e01b81526004016105d89190611ffe565b610c28338361124d565b610c537f00000000000000000000000000000000000000000000000000000000000000008484610b8e565b50600192915050565b5f610c678484610b0a565b90505f19811015610ca75781811015610c9957828183604051637dc7a0d960e11b81526004016105d89392919061226b565b610ca784848484035f6113b4565b50505050565b6001600160a01b038316610cd6575f604051634b637e8f60e11b81526004016105d89190611ffe565b6001600160a01b038216610cff575f60405163ec442f0560e01b81526004016105d89190611ffe565b610bed838383611486565b5f33308103610d2e5730604051634b637e8f60e11b81526004016105d89190611ffe565b306001600160a01b03851603610d59578360405163ec442f0560e01b81526004016105d89190611ffe565b610d857f00000000000000000000000000000000000000000000000000000000000000008230866110ca565b6108378484611103565b5f610d9983610795565b6001600160a01b038481165f8181526008602052604080822080546001600160a01b031916888616908117909155905194955093928516927f3134e8a2e6d97e929a7e54011ea5485d7d196dd5f0ba4d4ef95803e8e3fc257f9190a4610bed8183610e03866114cf565b6114ec565b5f7f00000000000000000000000000000000000000000000000000000000000000006001600160a01b031663313ce5676040518163ffffffff1660e01b8152600401602060405180830381865afa925050508015610e83575060408051601f3d908101601f19168201909252610e809181019061228c565b60015b610e8d5750601290565b919050565b5f306001600160a01b037f000000000000000000000000000000000000000000000000000000000000000016148015610eea57507f000000000000000000000000000000000000000000000000000000000000000046145b15610f1457507f000000000000000000000000000000000000000000000000000000000000000090565b6106d5604080517f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f60208201527f0000000000000000000000000000000000000000000000000000000000000000918101919091527f000000000000000000000000000000000000000000000000000000000000000060608201524660808201523060a08201525f9060c00160405160208183030381529060405280519060200120905090565b5f5f610fc561093e565b90508065ffffffffffff16831061100057604051637669fc0f60e11b81526004810184905265ffffffffffff821660248201526044016105d8565b61100983611655565b9392505050565b81545f908181600581111561106c575f6110298461168b565b6110339085612245565b5f8881526020902090915081015465ffffffffffff908116908716101561105c5780915061106a565b611067816001612258565b92505b505b5f611079878785856117de565b905080156110b35761109d87611090600184612245565b5f91825260209091200190565b54600160301b90046001600160d01b03166110b5565b5f5b979650505050505050565b5f6106d543611655565b6040516001600160a01b038481166024830152838116604483015260648201839052610ca79186918216906323b872dd90608401610bbb565b6001600160a01b03821661112c575f60405163ec442f0560e01b81526004016105d89190611ffe565b61060a5f8383611486565b6001600160a01b0381165f908152600960205260408120546106489061183d565b600b546001600160a01b03163314610880573360405163118cdaa760e01b81526004016105d89190611ffe565b600b80546001600160a01b038381166001600160a01b0319831681179093556040519116919082907f8be0079c531659141344cd1fd0a4f28419497f9722a3daafe3b4186f6b6457e0905f90a35050565b6001600160a01b0381165f90815260076020526040812054610648565b60606106d57f0000000000000000000000000000000000000000000000000000000000000000600561186d565b60606106d57f0000000000000000000000000000000000000000000000000000000000000000600661186d565b6001600160a01b038216611276575f604051634b637e8f60e11b81526004016105d89190611ffe565b61060a825f83611486565b80545f9080156112b05761129a83611090600184612245565b54600160301b90046001600160d01b0316611009565b5f9392505050565b5f6106486112c4610e92565b8360405161190160f01b8152600281019290925260228201526042902090565b5f5f5f5f6112f488888888611916565b92509250925061130482826119d4565b50909695505050505050565b610bed83838360016113b4565b604080518082019091525f80825260208201526001600160a01b0383165f9081526009602052604090206106459083611a8c565b5f5f60205f8451602086015f885af180611370576040513d5f823e3d81fd5b50505f513d91508115611387578060011415611394565b6001600160a01b0384163b155b15610ca75783604051635274afe760e01b81526004016105d89190611ffe565b6001600160a01b0384166113dd575f60405163e602df0560e01b81526004016105d89190611ffe565b6001600160a01b038316611406575f604051634a1406b160e11b81526004016105d89190611ffe565b6001600160a01b038085165f9081526001602090815260408083209387168352929052208290558015610ca757826001600160a01b0316846001600160a01b03167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b9258460405161147891815260200190565b60405180910390a350505050565b6001600160a01b038316158015906114a657506001600160a01b03821615155b156114c457604051638cd22d1960e01b815260040160405180910390fd5b610bed838383611af9565b6001600160a01b0381165f90815260208190526040812054610648565b816001600160a01b0316836001600160a01b03161415801561150d57505f81115b15610bed576001600160a01b038316156115b4576001600160a01b0383165f908152600960205260408120819061154f90611b5f61154a86611b6a565b611b9d565b6001600160d01b031691506001600160d01b03169150846001600160a01b03167fdec2bacdd2f05b59de34da9b523dff8be42e5e38e818c82fdb0bae774387a72483836040516115a9929190918252602082015260400190565b60405180910390a250505b6001600160a01b03821615610bed576001600160a01b0382165f90815260096020526040812081906115ec90611bd561154a86611b6a565b6001600160d01b031691506001600160d01b03169150836001600160a01b03167fdec2bacdd2f05b59de34da9b523dff8be42e5e38e818c82fdb0bae774387a7248383604051611646929190918252602082015260400190565b60405180910390a25050505050565b5f65ffffffffffff821115611687576040516306dfcc6560e41b815260306004820152602481018390526044016105d8565b5090565b5f60018211611698575090565b816001600160801b82106116b15760809190911c9060401b5b600160401b82106116c75760409190911c9060201b5b64010000000082106116de5760209190911c9060101b5b6201000082106116f35760109190911c9060081b5b61010082106117075760089190911c9060041b5b6010821061171a5760049190911c9060021b5b600482106117265760011b5b600302600190811c9081858161173e5761173e6122a7565b048201901c90506001818581611756576117566122a7565b048201901c9050600181858161176e5761176e6122a7565b048201901c90506001818581611786576117866122a7565b048201901c9050600181858161179e5761179e6122a7565b048201901c905060018185816117b6576117b66122a7565b048201901c90506117d58185816117cf576117cf6122a7565b04821190565b90039392505050565b5f5b81831015611835575f6117f38484611be0565b5f8781526020902090915065ffffffffffff86169082015465ffffffffffff1611156118215780925061182f565b61182c816001612258565b93505b506117e0565b509392505050565b5f63ffffffff821115611687576040516306dfcc6560e41b815260206004820152602481018390526044016105d8565b606060ff83146118875761188083611bfa565b9050610648565b818054611893906121f9565b80601f01602080910402602001604051908101604052809291908181526020018280546118bf906121f9565b801561190a5780601f106118e15761010080835404028352916020019161190a565b820191905f5260205f20905b8154815290600101906020018083116118ed57829003601f168201915b50505050509050610648565b5f80806fa2a8918ca85bafe22016d0b997e4df60600160ff1b0384111561194557505f915060039050826119ca565b604080515f808252602082018084528a905260ff891692820192909252606081018790526080810186905260019060a0016020604051602081039080840390855afa158015611996573d5f5f3e3d5ffd5b5050604051601f1901519150506001600160a01b0381166119c157505f9250600191508290506119ca565b92505f91508190505b9450945094915050565b5f8260038111156119e7576119e76122bb565b036119f0575050565b6001826003811115611a0457611a046122bb565b03611a225760405163f645eedf60e01b815260040160405180910390fd5b6002826003811115611a3657611a366122bb565b03611a575760405163fce698f760e01b8152600481018290526024016105d8565b6003826003811115611a6b57611a6b6122bb565b0361060a576040516335e2f38360e21b8152600481018290526024016105d8565b604080518082019091525f8082526020820152825f018263ffffffff1681548110611ab957611ab96122cf565b5f9182526020918290206040805180820190915291015465ffffffffffff81168252600160301b90046001600160d01b0316918101919091529392505050565b611b04838383611c37565b6001600160a01b038316611b54575f611b1c60025490565b90506001600160d01b0380821115611b5157604051630e58ae9360e11b815260048101839052602481018290526044016105d8565b50505b610bed838383611d4a565b5f61064582846122e3565b5f6001600160d01b03821115611687576040516306dfcc6560e41b815260d06004820152602481018390526044016105d8565b5f5f611bc8611baa61093e565b611bc0611bb688611281565b868863ffffffff16565b879190611da9565b915091505b935093915050565b5f6106458284612302565b5f611bee6002848418612321565b61064590848416612258565b60605f611c0683611db6565b6040805160208082528183019092529192505f91906020820181803683375050509182525060208101929092525090565b6001600160a01b038316611c61578060025f828254611c569190612258565b90915550611cbe9050565b6001600160a01b0383165f9081526020819052604090205481811015611ca05783818360405163391434e360e21b81526004016105d89392919061226b565b6001600160a01b0384165f9081526020819052604090209082900390555b6001600160a01b038216611cda57600280548290039055611cf8565b6001600160a01b0382165f9081526020819052604090208054820190555b816001600160a01b0316836001600160a01b03167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef83604051611d3d91815260200190565b60405180910390a3505050565b6001600160a01b038316611d6c57611d69600a611bd561154a84611b6a565b50505b6001600160a01b038216611d8e57611d8b600a611b5f61154a84611b6a565b50505b610bed611d9a84610795565b611da384610795565b836114ec565b5f80611bc8858585611ddd565b5f60ff8216601f81111561064857604051632cd44ac360e21b815260040160405180910390fd5b82545f9081908015611ed3575f611df987611090600185612245565b805490915065ffffffffffff80821691600160301b90046001600160d01b0316908816821115611e3c57604051632520601d60e01b815260040160405180910390fd5b8765ffffffffffff168265ffffffffffff1603611e7557825465ffffffffffff16600160301b6001600160d01b03891602178355611ec5565b6040805180820190915265ffffffffffff808a1682526001600160d01b03808a1660208085019182528d54600181018f555f8f81529190912094519151909216600160301b029216919091179101555b9450859350611bcd92505050565b50506040805180820190915265ffffffffffff80851682526001600160d01b0380851660208085019182528854600181018a555f8a815291822095519251909316600160301b029190931617920191909155905081611bcd565b5f81518084528060208401602086015e5f602082860101526020601f19601f83011685010191505092915050565b602081525f6106456020830184611f2d565b80356001600160a01b0381168114610e8d575f5ffd5b5f5f60408385031215611f94575f5ffd5b611f9d83611f6d565b946020939093013593505050565b5f5f5f60608486031215611fbd575f5ffd5b611fc684611f6d565b9250611fd460208501611f6d565b929592945050506040919091013590565b5f60208284031215611ff5575f5ffd5b61064582611f6d565b6001600160a01b0391909116815260200190565b60ff60f81b8816815260e060208201525f61203060e0830189611f2d565b82810360408401526120428189611f2d565b606084018890526001600160a01b038716608085015260a0840186905283810360c0850152845180825260208087019350909101905f5b81811015612097578351835260209384019390920191600101612079565b50909b9a5050505050505050505050565b5f602082840312156120b8575f5ffd5b5035919050565b60ff81168114610b8b575f5ffd5b5f5f5f5f5f5f60c087890312156120e2575f5ffd5b6120eb87611f6d565b955060208701359450604087013593506060870135612109816120bf565b9598949750929560808101359460a0909101359350915050565b5f5f5f5f5f5f5f60e0888a031215612139575f5ffd5b61214288611f6d565b965061215060208901611f6d565b95506040880135945060608801359350608088013561216e816120bf565b9699959850939692959460a0840135945060c09093013592915050565b5f5f6040838503121561219c575f5ffd5b6121a583611f6d565b91506121b360208401611f6d565b90509250929050565b5f5f604083850312156121cd575f5ffd5b6121d683611f6d565b9150602083013563ffffffff811681146121ee575f5ffd5b809150509250929050565b600181811c9082168061220d57607f821691505b60208210810361222b57634e487b7160e01b5f52602260045260245ffd5b50919050565b634e487b7160e01b5f52601160045260245ffd5b8181038181111561064857610648612231565b8082018082111561064857610648612231565b6001600160a01b039390931683526020830191909152604082015260600190565b5f6020828403121561229c575f5ffd5b8151611009816120bf565b634e487b7160e01b5f52601260045260245ffd5b634e487b7160e01b5f52602160045260245ffd5b634e487b7160e01b5f52603260045260245ffd5b6001600160d01b03828116828216039081111561064857610648612231565b6001600160d01b03818116838216019081111561064857610648612231565b5f8261233b57634e487b7160e01b5f52601260045260245ffd5b50049056fea164736f6c634300081c000a", "linkReferences": {}, "deployedLinkReferences": {}, "immutableReferences": { "3415": [ { "length": 32, - "start": 1988 + "start": 2115 }, { "length": 32, - "start": 2985 + "start": 3117 }, { "length": 32, - "start": 3290 + "start": 3422 }, { "length": 32, - "start": 3463 + "start": 3595 } ], "6684": [ { "length": 32, - "start": 3694 + "start": 3826 } ], "6686": [ { "length": 32, - "start": 3652 + "start": 3784 } ], "6688": [ { "length": 32, - "start": 3610 + "start": 3742 } ], "6690": [ { "length": 32, - "start": 3775 + "start": 3907 } ], "6692": [ { "length": 32, - "start": 3815 + "start": 3947 } ], "6695": [ { "length": 32, - "start": 4463 + "start": 4602 } ], "6698": [ { "length": 32, - "start": 4508 + "start": 4647 } ] }, "inputSourceName": "project/contracts/token/EnclaveTicketToken.sol", - "buildInfoId": "solc-0_8_28-e60a5d7c133605edcf61acdd5ba43ab44ee0928e" + "buildInfoId": "solc-0_8_28-2c25095d1e3a91525c0c4b9447ccae8788bab93f" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/verifier/DkgPkVerifier.sol/DkgPkVerifier.json b/packages/enclave-contracts/artifacts/contracts/verifier/DkgPkVerifier.sol/DkgPkVerifier.json index 272ae5f3f1..49bba848aa 100644 --- a/packages/enclave-contracts/artifacts/contracts/verifier/DkgPkVerifier.sol/DkgPkVerifier.json +++ b/packages/enclave-contracts/artifacts/contracts/verifier/DkgPkVerifier.sol/DkgPkVerifier.json @@ -102,7 +102,7 @@ } }, "immutableReferences": { - "32281": [ + "33764": [ { "length": 32, "start": 91 @@ -164,13 +164,13 @@ "start": 11964 } ], - "32283": [ + "33766": [ { "length": 32, "start": 398 } ], - "32285": [ + "33768": [ { "length": 32, "start": 432 @@ -182,5 +182,5 @@ ] }, "inputSourceName": "project/contracts/verifier/DkgPkVerifier.sol", - "buildInfoId": "solc-0_8_28-e60a5d7c133605edcf61acdd5ba43ab44ee0928e" + "buildInfoId": "solc-0_8_28-2c25095d1e3a91525c0c4b9447ccae8788bab93f" } \ No newline at end of file diff --git a/packages/enclave-contracts/artifacts/contracts/verifier/DkgPkVerifier.sol/ZKTranscriptLib.json b/packages/enclave-contracts/artifacts/contracts/verifier/DkgPkVerifier.sol/ZKTranscriptLib.json index 4bc1877086..77f4e0d705 100644 --- a/packages/enclave-contracts/artifacts/contracts/verifier/DkgPkVerifier.sol/ZKTranscriptLib.json +++ b/packages/enclave-contracts/artifacts/contracts/verifier/DkgPkVerifier.sol/ZKTranscriptLib.json @@ -396,5 +396,5 @@ "deployedLinkReferences": {}, "immutableReferences": {}, "inputSourceName": "project/contracts/verifier/DkgPkVerifier.sol", - "buildInfoId": "solc-0_8_28-e60a5d7c133605edcf61acdd5ba43ab44ee0928e" + "buildInfoId": "solc-0_8_28-2c25095d1e3a91525c0c4b9447ccae8788bab93f" } \ No newline at end of file diff --git a/packages/enclave-contracts/contracts/E3RefundManager.sol b/packages/enclave-contracts/contracts/E3RefundManager.sol index 5e9001b403..e3ed6a7e1a 100644 --- a/packages/enclave-contracts/contracts/E3RefundManager.sol +++ b/packages/enclave-contracts/contracts/E3RefundManager.sol @@ -39,11 +39,19 @@ contract E3RefundManager is IE3RefundManager, OwnableUpgradeable { /// @notice Work value allocation configuration WorkValueAllocation internal _workAllocation; /// @notice Maps E3 ID to refund distribution - mapping(uint256 e3Id => RefundDistribution) internal _distributions; + mapping(uint256 e3Id => RefundDistribution distribution) + internal _distributions; /// @notice Tracks claims per E3 per address - mapping(uint256 e3Id => mapping(address => bool)) internal _claimed; + mapping(uint256 e3Id => mapping(address claimer => bool hasClaimed)) + internal _claimed; + /// @notice Tracks number of claims made per E3 (for routeSlashedFunds guard) + mapping(uint256 e3Id => uint256 count) internal _claimCount; + /// @notice Tracks number of honest node claims made per E3 (for dust fix) + mapping(uint256 e3Id => uint256 count) internal _honestNodeClaimCount; + /// @notice Tracks total amount paid to honest nodes per E3 (for dust fix) + mapping(uint256 e3Id => uint256 amount) internal _totalHonestNodePaid; /// @notice Maps E3 ID to honest node addresses - mapping(uint256 e3Id => address[]) internal _honestNodes; + mapping(uint256 e3Id => address[] nodes) internal _honestNodes; //////////////////////////////////////////////////////////// // // // Modifiers // @@ -103,10 +111,12 @@ contract E3RefundManager is IE3RefundManager, OwnableUpgradeable { function calculateRefund( uint256 e3Id, uint256 originalPayment, - address[] calldata honestNodes + address[] calldata honestNodes, + IERC20 paymentToken ) external onlyEnclave { require(!_distributions[e3Id].calculated, "Already calculated"); require(originalPayment > 0, "No payment"); + require(address(paymentToken) != address(0), "Invalid fee token"); // Calculate work value based on stage IEnclave.E3Stage failedAt = _getFailedAtStage(e3Id); @@ -121,14 +131,15 @@ contract E3RefundManager is IE3RefundManager, OwnableUpgradeable { honestNodeAmount - requesterAmount; - // Store distribution + // Store distribution with the actual token used for this E3 _distributions[e3Id] = RefundDistribution({ requesterAmount: requesterAmount, honestNodeAmount: honestNodeAmount, protocolAmount: protocolAmount, totalSlashed: 0, honestNodeCount: honestNodes.length, - calculated: true + calculated: true, + feeToken: paymentToken }); // Store honest nodes @@ -138,7 +149,7 @@ contract E3RefundManager is IE3RefundManager, OwnableUpgradeable { // Transfer protocol fee to treasury immediately if (protocolAmount > 0) { - feeToken.safeTransfer(treasury, protocolAmount); + paymentToken.safeTransfer(treasury, protocolAmount); } emit RefundDistributionCalculated( @@ -229,6 +240,12 @@ contract E3RefundManager is IE3RefundManager, OwnableUpgradeable { RefundDistribution storage dist = _distributions[e3Id]; if (!dist.calculated) revert RefundNotCalculated(e3Id); + // Guard against pre-upgrade records where feeToken was not yet stored + require( + address(dist.feeToken) != address(0), + "feeToken not initialized" + ); + address requester = enclave.getRequester(e3Id); if (msg.sender != requester) revert NotRequester(e3Id, msg.sender); @@ -238,8 +255,10 @@ contract E3RefundManager is IE3RefundManager, OwnableUpgradeable { if (amount == 0) revert NoRefundAvailable(e3Id); _claimed[e3Id][msg.sender] = true; + _claimCount[e3Id]++; - feeToken.safeTransfer(msg.sender, amount); + // Use the per-E3 fee token (not the global one, which may have been rotated) + dist.feeToken.safeTransfer(msg.sender, amount); emit RefundClaimed(e3Id, msg.sender, amount, "REQUESTER"); } @@ -250,6 +269,13 @@ contract E3RefundManager is IE3RefundManager, OwnableUpgradeable { ) external returns (uint256 amount) { RefundDistribution storage dist = _distributions[e3Id]; require(dist.calculated, RefundNotCalculated(e3Id)); + + // Guard against pre-upgrade records where feeToken was not yet stored + require( + address(dist.feeToken) != address(0), + "feeToken not initialized" + ); + require(!_claimed[e3Id][msg.sender], AlreadyClaimed(e3Id, msg.sender)); // Check if caller is honest node @@ -261,20 +287,26 @@ contract E3RefundManager is IE3RefundManager, OwnableUpgradeable { require(isHonest, NotHonestNode(e3Id, msg.sender)); require(dist.honestNodeCount > 0, NoRefundAvailable(e3Id)); - amount = dist.honestNodeAmount / dist.honestNodeCount; + uint256 perNodeAmount = dist.honestNodeAmount / dist.honestNodeCount; + + _honestNodeClaimCount[e3Id]++; + if (_honestNodeClaimCount[e3Id] == dist.honestNodeCount) { + // Last claimer gets whatever remains (includes dust) + amount = dist.honestNodeAmount - _totalHonestNodePaid[e3Id]; + } else { + amount = perNodeAmount; + } + _totalHonestNodePaid[e3Id] += amount; require(amount > 0, NoRefundAvailable(e3Id)); _claimed[e3Id][msg.sender] = true; + _claimCount[e3Id]++; - // Distribute reward through bonding registry - feeToken.approve(address(bondingRegistry), amount); - - address[] memory nodeArray = new address[](1); - nodeArray[0] = msg.sender; - uint256[] memory amountArray = new uint256[](1); - amountArray[0] = amount; - - bondingRegistry.distributeRewards(feeToken, nodeArray, amountArray); + // Transfer directly to the honest node. Using distributeRewards would require + // this contract to be an authorized distributor in BondingRegistry, and the node + // must be registered. Direct transfer is simpler and more reliable for refunds. + IERC20 token = dist.feeToken; + token.safeTransfer(msg.sender, amount); emit RefundClaimed(e3Id, msg.sender, amount, "HONEST_NODE"); } @@ -286,9 +318,10 @@ contract E3RefundManager is IE3RefundManager, OwnableUpgradeable { ) external onlyEnclave { RefundDistribution storage dist = _distributions[e3Id]; require(dist.calculated, "Not calculated"); + require(_claimCount[e3Id] == 0, "Claims already started"); + require(amount > 0, "Zero amount"); // Add slashed funds to distribution - // Note: slashing should be finalized before claims are made. // 50% to requester, 50% to honest nodes for non-participation uint256 toRequester = amount / 2; uint256 toHonestNodes = amount - toRequester; diff --git a/packages/enclave-contracts/contracts/Enclave.sol b/packages/enclave-contracts/contracts/Enclave.sol index 16206aaf22..d5e8abbbd6 100644 --- a/packages/enclave-contracts/contracts/Enclave.sol +++ b/packages/enclave-contracts/contracts/Enclave.sol @@ -8,6 +8,7 @@ pragma solidity >=0.8.27; import { IEnclave, E3, IE3Program } from "./interfaces/IEnclave.sol"; import { ICiphernodeRegistry } from "./interfaces/ICiphernodeRegistry.sol"; import { IBondingRegistry } from "./interfaces/IBondingRegistry.sol"; +import { ISlashingManager } from "./interfaces/ISlashingManager.sol"; import { IE3RefundManager } from "./interfaces/IE3RefundManager.sol"; import { IDecryptionVerifier } from "./interfaces/IDecryptionVerifier.sol"; import { @@ -23,6 +24,7 @@ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; * @notice Main contract for managing Encrypted Execution Environments (E3) * @dev Coordinates E3 lifecycle including request, activation, input publishing, and output verification */ +// solhint-disable-next-line max-states-count contract Enclave is IEnclave, OwnableUpgradeable { using SafeERC20 for IERC20; @@ -44,6 +46,10 @@ contract Enclave is IEnclave, OwnableUpgradeable { /// @dev Manages refund calculation and claiming for failed E3s. IE3RefundManager public e3RefundManager; + /// @notice Slashing Manager contract for fault attribution. + /// @dev Used to check which operators have been slashed for E3s. + ISlashingManager public slashingManager; + /// @notice Address of the ERC20 token used for E3 fees. /// @dev All E3 request fees must be paid in this token. IERC20 public feeToken; @@ -78,16 +84,19 @@ contract Enclave is IEnclave, OwnableUpgradeable { mapping(uint256 e3Id => uint256 e3Payment) public e3Payments; /// @notice Maps E3 ID to its current stage - mapping(uint256 e3Id => E3Stage) internal _e3Stages; + mapping(uint256 e3Id => E3Stage stage) internal _e3Stages; /// @notice Maps E3 ID to its deadlines - mapping(uint256 e3Id => E3Deadlines) internal _e3Deadlines; + mapping(uint256 e3Id => E3Deadlines deadlines) internal _e3Deadlines; /// @notice Maps E3 ID to failure reason (if failed) - mapping(uint256 e3Id => FailureReason) internal _e3FailureReasons; + mapping(uint256 e3Id => FailureReason reason) internal _e3FailureReasons; /// @notice Maps E3 ID to requester address - mapping(uint256 e3Id => address) internal _e3Requesters; + mapping(uint256 e3Id => address requester) internal _e3Requesters; + + /// @notice Maps E3 ID to the fee token used at request time + mapping(uint256 e3Id => IERC20 token) internal _e3FeeTokens; /// @notice Global timeout configuration E3TimeoutConfig internal _timeoutConfig; @@ -206,6 +215,16 @@ contract Enclave is IEnclave, OwnableUpgradeable { _; } + /// @notice Restricts function to CiphernodeRegistry or SlashingManager + modifier onlyCiphernodeRegistryOrSlashingManager() { + require( + msg.sender == address(ciphernodeRegistry) || + msg.sender == address(slashingManager), + "Only Registry or SlashingMgr" + ); + _; + } + //////////////////////////////////////////////////////////// // // // Initialization // @@ -336,6 +355,9 @@ contract Enclave is IEnclave, OwnableUpgradeable { feeToken.safeTransferFrom(msg.sender, address(this), e3Fee); + // Store the fee token used for this E3 (survives global token rotations) + _e3FeeTokens[e3Id] = feeToken; + require( ciphernodeRegistry.requestCommittee( e3Id, @@ -366,6 +388,12 @@ contract Enclave is IEnclave, OwnableUpgradeable { ) external returns (bool success) { E3 memory e3 = getE3(e3Id); + E3Stage current = _e3Stages[e3Id]; + require( + current == E3Stage.KeyPublished, + InvalidStage(e3Id, E3Stage.KeyPublished, current) + ); + E3Deadlines memory deadlines = _e3Deadlines[e3Id]; // You cannot post outputs after the compute deadline @@ -454,39 +482,57 @@ contract Enclave is IEnclave, OwnableUpgradeable { // // //////////////////////////////////////////////////////////// - /// @notice Distributes rewards to committee members after successful E3 completion. - /// @dev Divides the E3 payment equally among all committee members and transfers via bonding registry. - /// @dev Emits RewardsDistributed event upon successful distribution. + /// @notice Distributes rewards to active committee members after successful E3 completion. + /// @dev Uses active committee nodes (excluding expelled members). + /// Divides the E3 payment equally among active members and transfers via bonding registry. + /// If no active members remain (e.g., all expelled), refunds the requester to prevent fund lockup. + /// Any division dust is sent to the last member rather than being lost. /// @param e3Id The ID of the E3 for which to distribute rewards. function _distributeRewards(uint256 e3Id) internal { - address[] memory committeeNodes = ciphernodeRegistry.getCommitteeNodes( - e3Id - ); - uint256 committeeLength = committeeNodes.length; - uint256[] memory amounts = new uint256[](committeeLength); - - // TODO: do we need to pay different amounts to different nodes? - // For now, we'll pay the same amount to all nodes. - uint256 amount = e3Payments[e3Id] / committeeLength; - for (uint256 i = 0; i < committeeLength; i++) { - amounts[i] = amount; - } + address[] memory activeNodes = ciphernodeRegistry + .getActiveCommitteeNodes(e3Id); + uint256 activeLength = activeNodes.length; uint256 totalAmount = e3Payments[e3Id]; e3Payments[e3Id] = 0; + if (totalAmount == 0) return; + + // Use the per-E3 fee token (not the global one, which may have been rotated) + IERC20 paymentToken = _e3FeeTokens[e3Id]; - feeToken.approve(address(bondingRegistry), totalAmount); + if (activeLength == 0) { + address requester = _e3Requesters[e3Id]; + if (requester != address(0)) { + paymentToken.safeTransfer(requester, totalAmount); + } + return; + } + + uint256[] memory amounts = new uint256[](activeLength); - bondingRegistry.distributeRewards(feeToken, committeeNodes, amounts); + // Distribute equally among active (non-expelled) committee members + uint256 amount = totalAmount / activeLength; + uint256 distributed = 0; + for (uint256 i = 0; i < activeLength; i++) { + amounts[i] = amount; + distributed += amount; + } + uint256 dust = totalAmount - distributed; + if (dust > 0) { + amounts[activeLength - 1] += dust; + } - // TODO: decide where does dust go? Treasury maybe? - feeToken.approve(address(bondingRegistry), 0); + paymentToken.forceApprove(address(bondingRegistry), totalAmount); - emit RewardsDistributed(e3Id, committeeNodes, amounts); + bondingRegistry.distributeRewards(paymentToken, activeNodes, amounts); + + paymentToken.forceApprove(address(bondingRegistry), 0); + + emit RewardsDistributed(e3Id, activeNodes, amounts); } /// @notice Retrieves the honest committee nodes for a given E3. - /// @dev Determines honest nodes based on failure reason and committee publication status. + /// @dev Uses active committee view from the registry (which excludes expelled/slashed members). /// @param e3Id The ID of the E3. /// @return honestNodes An array of addresses of honest committee nodes. function _getHonestNodes( @@ -502,12 +548,11 @@ contract Enclave is IEnclave, OwnableUpgradeable { return new address[](0); } - // Try to get published committee nodes - try ciphernodeRegistry.getCommitteeNodes(e3Id) returns ( + // Use active committee nodes (already filtered by expulsion) + try ciphernodeRegistry.getActiveCommitteeNodes(e3Id) returns ( address[] memory nodes ) { - // TODO: Implement fault attribution to filter honest from faulting nodes - return nodes; // Assume all are honest for now + return nodes; } catch { return new address[](0); // Committee not published (DKG failed) } @@ -621,6 +666,21 @@ contract Enclave is IEnclave, OwnableUpgradeable { emit AllowedE3ProgramsParamsSet(_e3ProgramsParams); } + /// @notice Removes previously allowed E3 program parameter sets + /// @param _e3ProgramsParams Array of ABI encoded parameter sets to remove + function removeE3ProgramsParams( + bytes[] memory _e3ProgramsParams + ) public onlyOwner { + uint256 length = _e3ProgramsParams.length; + for (uint256 i; i < length; ) { + delete e3ProgramsParams[_e3ProgramsParams[i]]; + unchecked { + ++i; + } + } + emit E3ProgramsParamsRemoved(_e3ProgramsParams); + } + /// @notice Sets the E3 Refund Manager contract address /// @param _e3RefundManager The new E3 Refund Manager contract address function setE3RefundManager( @@ -634,8 +694,22 @@ contract Enclave is IEnclave, OwnableUpgradeable { emit E3RefundManagerSet(address(_e3RefundManager)); } + /// @notice Sets the Slashing Manager contract address + /// @param _slashingManager The new Slashing Manager contract address + function setSlashingManager( + ISlashingManager _slashingManager + ) public onlyOwner { + require( + address(_slashingManager) != address(0), + "Invalid SlashingManager address" + ); + slashingManager = _slashingManager; + emit SlashingManagerSet(address(_slashingManager)); + } + /// @notice Process a failed E3 and calculate refunds - /// @dev Can be called by anyone once E3 is in failed state + /// @dev Can be called by anyone once E3 is in failed state. + /// Uses the per-E3 feeToken stored at request time (survives global token rotation). /// @param e3Id The ID of the failed E3 function processE3Failure(uint256 e3Id) external { E3Stage stage = _e3Stages[e3Id]; @@ -647,8 +721,15 @@ contract Enclave is IEnclave, OwnableUpgradeable { address[] memory honestNodes = _getHonestNodes(e3Id); - feeToken.safeTransfer(address(e3RefundManager), payment); - e3RefundManager.calculateRefund(e3Id, payment, honestNodes); + IERC20 paymentToken = _e3FeeTokens[e3Id]; + + paymentToken.safeTransfer(address(e3RefundManager), payment); + e3RefundManager.calculateRefund( + e3Id, + payment, + honestNodes, + paymentToken + ); emit E3FailureProcessed(e3Id, payment, honestNodes.length); } @@ -701,8 +782,11 @@ contract Enclave is IEnclave, OwnableUpgradeable { function onE3Failed( uint256 e3Id, uint8 reason - ) external onlyCiphernodeRegistry { - require(reason > 0 && reason <= 12, "Invalid failure reason"); + ) external onlyCiphernodeRegistryOrSlashingManager { + require( + reason > 0 && reason <= uint8(FailureReason.VerificationFailed), + "Invalid failure reason" + ); // Mark E3 as failed with the given reason _markE3FailedWithReason(e3Id, FailureReason(reason)); } @@ -861,7 +945,6 @@ contract Enclave is IEnclave, OwnableUpgradeable { require(config.dkgWindow > 0, "Invalid DKG window"); require(config.computeWindow > 0, "Invalid compute window"); require(config.decryptionWindow > 0, "Invalid decryption window"); - require(config.gracePeriod > 0, "Invalid grace period"); _timeoutConfig = config; diff --git a/packages/enclave-contracts/contracts/interfaces/IBondingRegistry.sol b/packages/enclave-contracts/contracts/interfaces/IBondingRegistry.sol index a343b2f747..d910cdb5ff 100644 --- a/packages/enclave-contracts/contracts/interfaces/IBondingRegistry.sol +++ b/packages/enclave-contracts/contracts/interfaces/IBondingRegistry.sol @@ -350,7 +350,7 @@ interface IBondingRegistry { * @param rewardToken Reward token contract * @param operators Addresses of the operators to distribute rewards to * @param amounts Amounts of rewards to distribute to each operator - * @dev Only callable by contract owner + * @dev Only callable by authorized distributors. */ function distributeRewards( IERC20 rewardToken, @@ -439,6 +439,13 @@ interface IBondingRegistry { */ function setRewardDistributor(address newRewardDistributor) external; + /** + * @notice Revoke reward distributor authorization + * @param distributor Address to revoke + * @dev Only callable by contract owner + */ + function revokeRewardDistributor(address distributor) external; + /** * @notice Withdraw slashed funds to treasury * @param ticketAmount Amount of slashed ticket balance to withdraw diff --git a/packages/enclave-contracts/contracts/interfaces/ICiphernodeRegistry.sol b/packages/enclave-contracts/contracts/interfaces/ICiphernodeRegistry.sol index 4cfa7b1c8f..69b12a8752 100644 --- a/packages/enclave-contracts/contracts/interfaces/ICiphernodeRegistry.sol +++ b/packages/enclave-contracts/contracts/interfaces/ICiphernodeRegistry.sol @@ -41,6 +41,8 @@ interface ICiphernodeRegistry { address[] committee; mapping(address node => bool submitted) submitted; mapping(address node => uint256 score) scoreOf; + mapping(address node => bool active) active; + uint256 activeCount; } /// @notice This event MUST be emitted when a committee is selected for an E3. @@ -98,6 +100,30 @@ interface ICiphernodeRegistry { /// @param active True if committee is now active, false if completed. event CommitteeActivationChanged(uint256 indexed e3Id, bool active); + /// @notice This event MUST be emitted when a committee member is expelled due to slashing. + /// @param e3Id ID of the E3 for which the member was expelled. + /// @param node Address of the expelled committee member. + /// @param reason Hash of the slash reason that caused the expulsion. + /// @param activeCountAfter Number of active committee members remaining after expulsion. + event CommitteeMemberExpelled( + uint256 indexed e3Id, + address indexed node, + bytes32 reason, + uint256 activeCountAfter + ); + + /// @notice This event MUST be emitted when committee viability changes after an expulsion. + /// @param e3Id ID of the E3. + /// @param activeCount Current number of active committee members. + /// @param thresholdM The minimum threshold (M) required. + /// @param viable Whether the committee is still viable (activeCount >= M). + event CommitteeViabilityUpdated( + uint256 indexed e3Id, + uint256 activeCount, + uint256 thresholdM, + bool viable + ); + /// @notice This event MUST be emitted when `enclave` is set. /// @param enclave Address of the enclave contract. event EnclaveSet(address indexed enclave); @@ -249,4 +275,49 @@ interface ICiphernodeRegistry { /// @param e3Id ID of the E3 computation /// @return committeeDeadline The committee deadline timestamp function getCommitteeDeadline(uint256 e3Id) external view returns (uint256); + + /// @notice Expel a committee member from a specific E3 committee due to slashing + /// @dev Only callable by SlashingManager. Idempotent (re-expelling same member is no-op). + /// Returns viability data so the caller can decide whether to fail the E3 — + /// eliminating the need for separate getActiveCommitteeCount/getCommitteeThreshold calls. + /// @param e3Id ID of the E3 computation + /// @param node Address of the committee member to expel + /// @param reason Hash of the slash reason + /// @return activeCount Number of active committee members after expulsion + /// @return thresholdM The minimum threshold (M) required for viability + function expelCommitteeMember( + uint256 e3Id, + address node, + bytes32 reason + ) external returns (uint256 activeCount, uint32 thresholdM); + + /// @notice Check if a committee member is still active for a specific E3 + /// @param e3Id ID of the E3 computation + /// @param node Address of the committee member to check + /// @return active Whether the member is still active in the committee + function isCommitteeMemberActive( + uint256 e3Id, + address node + ) external view returns (bool active); + + /// @notice Get active (non-expelled) committee nodes for an E3 + /// @param e3Id ID of the E3 computation + /// @return nodes Array of active committee member addresses + function getActiveCommitteeNodes( + uint256 e3Id + ) external view returns (address[] memory nodes); + + /// @notice Get the count of active committee members for an E3 + /// @param e3Id ID of the E3 computation + /// @return count Number of active committee members + function getActiveCommitteeCount( + uint256 e3Id + ) external view returns (uint256 count); + + /// @notice Get the threshold configuration for an E3 committee + /// @param e3Id ID of the E3 computation + /// @return threshold The [M, N] threshold array + function getCommitteeThreshold( + uint256 e3Id + ) external view returns (uint32[2] memory threshold); } diff --git a/packages/enclave-contracts/contracts/interfaces/ICircuitVerifier.sol b/packages/enclave-contracts/contracts/interfaces/ICircuitVerifier.sol new file mode 100644 index 0000000000..ffca72f19b --- /dev/null +++ b/packages/enclave-contracts/contracts/interfaces/ICircuitVerifier.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. +pragma solidity >=0.8.27; + +/** + * @title ICircuitVerifier + * @notice Interface for on-chain ZK circuit verifiers (e.g., DkgPkVerifier, Honk verifiers) + * @dev Standard interface matching the verification pattern used by Honk-generated verifiers. + * Set the circuit verifier address directly as the proofVerifier in a SlashPolicy. + */ +interface ICircuitVerifier { + /// @notice Verify a ZK proof against public inputs + /// @param _proof The raw proof bytes + /// @param _publicInputs The public inputs to verify against + /// @return True if the proof is valid + function verify( + bytes calldata _proof, + bytes32[] calldata _publicInputs + ) external returns (bool); +} diff --git a/packages/enclave-contracts/contracts/interfaces/IE3RefundManager.sol b/packages/enclave-contracts/contracts/interfaces/IE3RefundManager.sol index aa2062cb47..44a079494b 100644 --- a/packages/enclave-contracts/contracts/interfaces/IE3RefundManager.sol +++ b/packages/enclave-contracts/contracts/interfaces/IE3RefundManager.sol @@ -5,6 +5,7 @@ // or FITNESS FOR A PARTICULAR PURPOSE. pragma solidity >=0.8.27; import { IEnclave } from "./IEnclave.sol"; +import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; /** * @title IE3RefundManager @@ -32,6 +33,7 @@ interface IE3RefundManager { uint256 totalSlashed; // Slashed funds added uint256 honestNodeCount; // Number of honest nodes bool calculated; // Whether distribution is calculated + IERC20 feeToken; // The fee token used for this E3's payment (stored per-E3 to survive token rotations) } //////////////////////////////////////////////////////////// // // @@ -86,10 +88,12 @@ interface IE3RefundManager { /// @param e3Id The failed E3 ID /// @param originalPayment The original payment amount /// @param honestNodes Array of honest node addresses + /// @param paymentToken The fee token that was used for this E3's payment function calculateRefund( uint256 e3Id, uint256 originalPayment, - address[] calldata honestNodes + address[] calldata honestNodes, + IERC20 paymentToken ) external; /// @notice Requester claims their refund diff --git a/packages/enclave-contracts/contracts/interfaces/IEnclave.sol b/packages/enclave-contracts/contracts/interfaces/IEnclave.sol index 67a0a81382..f96d76a2a0 100644 --- a/packages/enclave-contracts/contracts/interfaces/IEnclave.sol +++ b/packages/enclave-contracts/contracts/interfaces/IEnclave.sol @@ -59,7 +59,6 @@ interface IEnclave { uint256 dkgWindow; uint256 computeWindow; uint256 decryptionWindow; - uint256 gracePeriod; } /// @notice Deadlines for each E3 @@ -153,10 +152,18 @@ interface IEnclave { /// @param e3ProgramParams Array of encoded encryption scheme parameters (e.g, for BFV) event AllowedE3ProgramsParamsSet(bytes[] e3ProgramParams); + /// @notice Emitted when E3 program parameter sets are removed. + /// @param e3ProgramParams Array of removed encryption scheme parameters. + event E3ProgramsParamsRemoved(bytes[] e3ProgramParams); + /// @notice Emitted when E3RefundManager contract is set. /// @param e3RefundManager The address of the E3RefundManager contract. event E3RefundManagerSet(address indexed e3RefundManager); + /// @notice Emitted when the SlashingManager contract is set. + /// @param slashingManager The address of the SlashingManager contract. + event SlashingManagerSet(address indexed slashingManager); + /// @notice Emitted when a failed E3 is processed for refunds. /// @param e3Id The ID of the failed E3. /// @param paymentAmount The original payment amount being refunded. @@ -307,6 +314,11 @@ interface IEnclave { /// @param _e3ProgramsParams Array of ABI encoded parameter sets to allow. function setE3ProgramsParams(bytes[] memory _e3ProgramsParams) external; + /// @notice Removes previously allowed E3 program parameter sets. + /// @dev This function revokes specific parameter sets that should no longer be allowed. + /// @param _e3ProgramsParams Array of ABI encoded parameter sets to remove. + function removeE3ProgramsParams(bytes[] memory _e3ProgramsParams) external; + //////////////////////////////////////////////////////////// // // // Get Functions // diff --git a/packages/enclave-contracts/contracts/interfaces/ISlashVerifier.sol b/packages/enclave-contracts/contracts/interfaces/ISlashVerifier.sol deleted file mode 100644 index a2ac4b860f..0000000000 --- a/packages/enclave-contracts/contracts/interfaces/ISlashVerifier.sol +++ /dev/null @@ -1,23 +0,0 @@ -// SPDX-License-Identifier: LGPL-3.0-only -// -// This file is provided WITHOUT ANY WARRANTY; -// without even the implied warranty of MERCHANTABILITY -// or FITNESS FOR A PARTICULAR PURPOSE. -pragma solidity >=0.8.27; - -/** - * @title ISlashVerifier - * @notice Interface for verifying slash proofs - * @dev Slash verifiers implement cryptographic or logical verification of slash proposals - */ -interface ISlashVerifier { - /// @notice Verify a slash proof - /// @dev This function is called by the SlashingManager contract during slash proposal to verify proof validity - /// @param proposalId ID of the slash proposal - /// @param proof ABI encoded proof data supporting the slash - /// @return success Whether the proof was successfully verified - function verify( - uint256 proposalId, - bytes memory proof - ) external view returns (bool success); -} diff --git a/packages/enclave-contracts/contracts/interfaces/ISlashingManager.sol b/packages/enclave-contracts/contracts/interfaces/ISlashingManager.sol index d5711fc77b..9cf3c4089d 100644 --- a/packages/enclave-contracts/contracts/interfaces/ISlashingManager.sol +++ b/packages/enclave-contracts/contracts/interfaces/ISlashingManager.sol @@ -11,7 +11,9 @@ import { IBondingRegistry } from "./IBondingRegistry.sol"; /** * @title ISlashingManager * @notice Interface for managing slashing proposals, appeals, and execution - * @dev Maintains policy table and handles slash workflows with appeals + * @dev Maintains policy table and handles slash workflows with two lanes: + * Lane A (proof-based): permissionless, atomic, no appeals + * Lane B (evidence-based): SLASHER_ROLE required, appeal window */ interface ISlashingManager { // ====================== @@ -28,6 +30,8 @@ interface ISlashingManager { * @param banNode Whether executing this slash will permanently ban the node * @param appealWindow Time window in seconds for operators to appeal (0 = immediate execution, no appeals) * @param enabled Whether this slash type is currently active and can be proposed + * @param affectsCommittee Whether executing this slash triggers committee expulsion for the target E3 + * @param failureReason The FailureReason enum value to use when committee drops below threshold (0 = no E3 failure) */ struct SlashPolicy { uint256 ticketPenalty; @@ -37,11 +41,14 @@ interface ISlashingManager { bool banNode; uint256 appealWindow; bool enabled; + bool affectsCommittee; + uint8 failureReason; } /** * @notice Slash proposal details tracking the full lifecycle of a slash * @dev Stores all state needed for proposal, appeal, and execution workflows + * @param e3Id ID of the E3 computation this slash relates to (0 for non-E3 slashes) * @param operator Address of the ciphernode operator being slashed * @param reason Hash of the slash reason (maps to SlashPolicy configuration) * @param ticketAmount Amount of ticket collateral to slash (copied from policy at proposal time) @@ -57,6 +64,7 @@ interface ISlashingManager { * @param proofVerified Whether the proof was successfully verified by the proof verifier contract */ struct SlashProposal { + uint256 e3Id; address operator; bytes32 reason; uint256 ticketAmount; @@ -70,6 +78,12 @@ interface ISlashingManager { address proposer; bytes32 proofHash; bool proofVerified; + /// @dev Snapshotted from SlashPolicy at proposal time to prevent execution drift + bool banNode; + /// @dev Snapshotted from SlashPolicy at proposal time to prevent execution drift + bool affectsCommittee; + /// @dev Snapshotted from SlashPolicy at proposal time to prevent execution drift + uint8 failureReason; } // ====================== @@ -94,6 +108,21 @@ interface ISlashingManager { /// @notice Thrown when provided proof fails verification error InvalidProof(); + /// @notice The ZK proof verified successfully — the operator's submission was valid, not a fault + error ProofIsValid(); + + /// @notice Thrown when the recovered signer does not match the operator being slashed + error SignerIsNotOperator(); + + /// @notice Thrown when the operator is not a member of the committee for this E3 + error OperatorNotInCommittee(); + + /// @notice Thrown when the verifier address in signed evidence doesn't match the policy's current verifier + error VerifierMismatch(); + + /// @notice Thrown when the verifier staticcall fails (e.g., contract doesn't exist, reverts, or runs out of gas) + error VerifierCallFailed(); + /// @notice Thrown when attempting to execute a slash whose appeal was upheld error AppealUpheld(); @@ -127,6 +156,12 @@ interface ISlashingManager { /// @notice Thrown when a policy requires proof but no verifier contract is configured error VerifierNotSet(); + /// @notice Thrown when the same evidence bundle has already been used in a proposal + error DuplicateEvidence(); + + /// @notice Thrown when the chainId in the signed proof payload does not match the current chain + error ChainIdMismatch(); + // ====================== // Events // ====================== @@ -141,6 +176,7 @@ interface ISlashingManager { /** * @notice Emitted when a new slash proposal is created * @param proposalId Unique ID of the created proposal + * @param e3Id ID of the E3 computation related to this slash * @param operator Address of the ciphernode operator being slashed * @param reason Hash of the slash reason * @param ticketAmount Amount of ticket collateral to be slashed @@ -150,8 +186,9 @@ interface ISlashingManager { */ event SlashProposed( uint256 indexed proposalId, + uint256 indexed e3Id, address indexed operator, - bytes32 indexed reason, + bytes32 reason, uint256 ticketAmount, uint256 licenseAmount, uint256 executableAt, @@ -161,6 +198,7 @@ interface ISlashingManager { /** * @notice Emitted when a slash proposal is executed and penalties are applied * @param proposalId ID of the executed proposal + * @param e3Id ID of the E3 committee associated with this slash * @param operator Address of the slashed operator * @param reason Hash of the slash reason * @param ticketAmount Amount of ticket collateral slashed @@ -169,6 +207,7 @@ interface ISlashingManager { */ event SlashExecuted( uint256 indexed proposalId, + uint256 e3Id, address indexed operator, bytes32 indexed reason, uint256 ticketAmount, @@ -295,7 +334,7 @@ interface ISlashingManager { /** * @notice Grants SLASHER_ROLE to an address - * @dev Only callable by DEFAULT_ADMIN_ROLE. Slashers can propose and execute slashes + * @dev Only callable by DEFAULT_ADMIN_ROLE. Slashers can propose and execute evidence-based slashes * @param slasher Address to grant slashing permissions (must be non-zero) */ function addSlasher(address slasher) external; @@ -307,58 +346,58 @@ interface ISlashingManager { */ function removeSlasher(address slasher) external; - /** - * @notice Grants VERIFIER_ROLE to an address - * @dev Only callable by DEFAULT_ADMIN_ROLE. Verifiers can validate proof-based slashes - * @param verifier Address to grant verification permissions (must be non-zero) - */ - function addVerifier(address verifier) external; - - /** - * @notice Revokes VERIFIER_ROLE from an address - * @dev Only callable by DEFAULT_ADMIN_ROLE - * @param verifier Address to revoke verification permissions from - */ - function removeVerifier(address verifier) external; - // ====================== // Slashing Functions // ====================== /** - * @notice Creates a new slash proposal against an operator - * @dev Only callable by SLASHER_ROLE. Validates policy and proof if required + * @notice Creates a new slash proposal with cryptographic proof (Lane A - permissionless) + * @dev Anyone can call this for proof-based slashes. Requires the operator's ECDSA signature + * over the proof payload to prevent arbitrary slashing. + * Evidence format: + * abi.encode(bytes zkProof, bytes32[] publicInputs, + * bytes signature, uint256 chainId, uint256 proofType, address verifier) + * The operator must have signed: keccak256(abi.encode(PROOF_PAYLOAD_TYPEHASH, chainId, e3Id, + * proofType, keccak256(zkProof), keccak256(abi.encodePacked(publicInputs)))) + * Verifications performed: + * 1. Verifier address in evidence matches the policy's current proofVerifier + * 2. Signature recovery confirms the operator authored the bad proof + * 3. Committee membership check confirms the operator was in the E3's committee + * 4. ZK proof re-verification confirms the proof is indeed invalid (fault) + * @param e3Id ID of the E3 computation this slash relates to * @param operator Address of the ciphernode operator to slash (must be non-zero) - * @param reason Hash of the slash reason (must have an enabled policy configured) - * @param proof Proof data to be verified (required if policy.requiresProof is true, can be empty otherwise) + * @param reason Hash of the slash reason (must have an enabled proof-required policy) + * @param proof Evidence data: abi.encode(zkProof, publicInputs, signature, chainId, proofType, verifier) * @return proposalId Sequential ID of the created proposal - * Requirements: - * - operator must not be zero address - * - reason must have an enabled policy configured - * - If policy requires proof, proof must be non-empty and pass verification - * - Caller must have SLASHER_ROLE */ function proposeSlash( + uint256 e3Id, address operator, bytes32 reason, bytes calldata proof ) external returns (uint256 proposalId); + /** + * @notice Creates a new slash proposal with evidence (Lane B - SLASHER_ROLE required) + * @dev Only callable by SLASHER_ROLE. Evidence-based slashes have appeal windows. + * @param e3Id ID of the E3 computation this slash relates to + * @param operator Address of the ciphernode operator to slash (must be non-zero) + * @param reason Hash of the slash reason (must have an enabled non-proof policy) + * @param evidence Evidence data supporting the slash proposal + * @return proposalId Sequential ID of the created proposal + */ + function proposeSlashEvidence( + uint256 e3Id, + address operator, + bytes32 reason, + bytes calldata evidence + ) external returns (uint256 proposalId); + /** * @notice Executes a slash proposal and applies penalties to the operator - * @dev Only callable by SLASHER_ROLE. Validates execution conditions and applies slashing + * @dev For evidence-based slashes, validates appeal window has expired. + * Proof-based slashes are executed atomically in proposeSlash. * @param proposalId ID of the proposal to execute (must exist and not be already executed) - * Requirements: - * - Proposal must exist and not be already executed - * - For proof-required slashes: proof must be verified - * - For evidence-based slashes: appeal window must have expired - * - If appeal was filed and resolved, appeal must not have been upheld - * - Caller must have SLASHER_ROLE - * Effects: - * - Marks proposal as executed - * - Slashes ticket balance if ticketAmount > 0 - * - Slashes license bond if licenseAmount > 0 - * - Bans node if policy.banNode is true */ function executeSlash(uint256 proposalId) external; @@ -367,32 +406,19 @@ interface ISlashingManager { // ====================== /** - * @notice Allows an operator to file an appeal against a slash proposal + * @notice Allows an operator to file an appeal against an evidence-based slash proposal * @dev Only the operator being slashed can file an appeal, and only within the appeal window * @param proposalId ID of the proposal to appeal (must exist) * @param evidence String containing evidence and arguments supporting the appeal - * Requirements: - * - Proposal must exist - * - Caller must be the operator being slashed - * - Current timestamp must be before proposal.executableAt (within appeal window) - * - Proposal must not already have an appeal filed */ function fileAppeal(uint256 proposalId, string calldata evidence) external; /** * @notice Resolves an appeal by accepting or rejecting it * @dev Only callable by GOVERNANCE_ROLE. If appeal is upheld, the slash cannot be executed - * @param proposalId ID of the proposal with the appeal to resolve (must exist and have an appeal) - * @param appealUpheld True to uphold the appeal (cancel the slash), false to deny the appeal - * (allow slash to proceed) + * @param proposalId ID of the proposal with the appeal to resolve + * @param appealUpheld True to uphold the appeal (cancel the slash), false to deny * @param resolution String explaining the governance decision - * Requirements: - * - Proposal must exist and have an appeal filed - * - Appeal must not already be resolved - * - Caller must have GOVERNANCE_ROLE - * Effects: - * - Marks appeal as resolved - * - Sets appealUpheld flag (true = slash cancelled, false = slash can proceed) */ function resolveAppeal( uint256 proposalId, @@ -410,13 +436,6 @@ interface ISlashingManager { * @param node Address of the node to ban (must be non-zero) * @param status Whether to ban the node * @param reason Hash of the reason for banning - * Requirements: - * - node must not be zero address - * - Caller must have GOVERNANCE_ROLE - * Effects: - * - Sets banned[node] to status - * - Emits NodeBanned event if status is true - * - Emits NodeUnbanned event if status is false */ function updateBanStatus( address node, diff --git a/packages/enclave-contracts/contracts/registry/BondingRegistry.sol b/packages/enclave-contracts/contracts/registry/BondingRegistry.sol index 41006ce38b..53eff07658 100644 --- a/packages/enclave-contracts/contracts/registry/BondingRegistry.sol +++ b/packages/enclave-contracts/contracts/registry/BondingRegistry.sol @@ -9,6 +9,9 @@ pragma solidity >=0.8.27; import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; +import { + ReentrancyGuardUpgradeable +} from "@openzeppelin/contracts-upgradeable/utils/ReentrancyGuardUpgradeable.sol"; import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import { SafeERC20 @@ -26,7 +29,12 @@ import { EnclaveTicketToken } from "../token/EnclaveTicketToken.sol"; * @notice Implementation of the bonding registry managing operator ticket balances and license bonds * @dev Handles deposits, withdrawals, slashing, exits, and integrates with registry and slashing manager */ -contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { +// solhint-disable-next-line max-states-count +contract BondingRegistry is + IBondingRegistry, + OwnableUpgradeable, + ReentrancyGuardUpgradeable +{ using SafeERC20 for IERC20; using ExitQueueLib for ExitQueueLib.ExitQueueState; @@ -62,8 +70,11 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { /// @notice Address authorized to perform slashing operations address public slashingManager; - /// @notice Address authorized to distribute rewards to operators - address public rewardDistributor; + /// @notice Addresses authorized to distribute rewards to operators + /// @dev Multiple contracts (Enclave, E3RefundManager) need to distribute rewards. + /// Each authorized distributor must approve this contract for the reward token. + mapping(address distributor => bool authorized) + public authorizedDistributors; /// @notice Treasury address that receives slashed funds address public slashedFundsTreasury; @@ -173,6 +184,7 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { uint64 _exitDelay ) public initializer { __Ownable_init(msg.sender); + __ReentrancyGuard_init(); setTicketToken(_ticketToken); setLicenseToken(_licenseToken); setRegistry(_registry); @@ -304,7 +316,7 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { /// @inheritdoc IBondingRegistry function deregisterOperator( uint256[] calldata siblingNodes - ) external noExitInProgress(msg.sender) { + ) external noExitInProgress(msg.sender) nonReentrant { Operator storage op = operators[msg.sender]; require(op.registered, NotRegistered()); @@ -352,7 +364,7 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { /// @inheritdoc IBondingRegistry function addTicketBalance( uint256 amount - ) external noExitInProgress(msg.sender) { + ) external noExitInProgress(msg.sender) nonReentrant { require(amount != 0, ZeroAmount()); require(operators[msg.sender].registered, NotRegistered()); @@ -371,7 +383,7 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { /// @inheritdoc IBondingRegistry function removeTicketBalance( uint256 amount - ) external noExitInProgress(msg.sender) { + ) external noExitInProgress(msg.sender) nonReentrant { require(amount != 0, ZeroAmount()); require(operators[msg.sender].registered, NotRegistered()); require( @@ -393,7 +405,9 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { } /// @inheritdoc IBondingRegistry - function bondLicense(uint256 amount) external noExitInProgress(msg.sender) { + function bondLicense( + uint256 amount + ) external noExitInProgress(msg.sender) nonReentrant { require(amount != 0, ZeroAmount()); uint256 balanceBefore = licenseToken.balanceOf(address(this)); @@ -416,7 +430,7 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { /// @inheritdoc IBondingRegistry function unbondLicense( uint256 amount - ) external noExitInProgress(msg.sender) { + ) external noExitInProgress(msg.sender) nonReentrant { require(amount != 0, ZeroAmount()); require( operators[msg.sender].licenseBond >= amount, @@ -444,7 +458,7 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { function claimExits( uint256 maxTicketAmount, uint256 maxLicenseAmount - ) external { + ) external nonReentrant { (uint256 ticketClaim, uint256 licenseClaim) = _exits.claimAssets( msg.sender, maxTicketAmount, @@ -573,15 +587,15 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { IERC20 rewardToken, address[] calldata recipients, uint256[] calldata amounts - ) external { - require(msg.sender == rewardDistributor, OnlyRewardDistributor()); + ) external nonReentrant { + require(authorizedDistributors[msg.sender], OnlyRewardDistributor()); require(recipients.length == amounts.length, ArrayLengthMismatch()); uint256 len = recipients.length; for (uint256 i = 0; i < len; i++) { - if (amounts[i] > 0 && operators[recipients[i]].registered) { + if (amounts[i] > 0) { rewardToken.safeTransferFrom( - rewardDistributor, + msg.sender, recipients[i], amounts[i] ); @@ -679,20 +693,27 @@ contract BondingRegistry is IBondingRegistry, OwnableUpgradeable { slashingManager = newSlashingManager; } - /// @notice Sets the reward distributor address - /// @dev Only callable by owner - /// @param newRewardDistributor Address of the reward distributor + /// @notice Authorizes an address to distribute rewards + /// @dev Only callable by owner. Supports multiple authorized distributors (Enclave + E3RefundManager) + /// @param newRewardDistributor Address to authorize as reward distributor function setRewardDistributor( address newRewardDistributor ) public onlyOwner { - rewardDistributor = newRewardDistributor; + authorizedDistributors[newRewardDistributor] = true; + } + + /// @notice Revokes reward distributor authorization + /// @dev Only callable by owner + /// @param distributor Address to revoke + function revokeRewardDistributor(address distributor) public onlyOwner { + authorizedDistributors[distributor] = false; } /// @inheritdoc IBondingRegistry function withdrawSlashedFunds( uint256 ticketAmount, uint256 licenseAmount - ) public onlyOwner { + ) public onlyOwner nonReentrant { require(ticketAmount <= slashedTicketBalance, InsufficientBalance()); require(licenseAmount <= slashedLicenseBond, InsufficientBalance()); diff --git a/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol b/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol index 298ab77f9e..fc20835afd 100644 --- a/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol +++ b/packages/enclave-contracts/contracts/registry/CiphernodeRegistryOwnable.sol @@ -8,6 +8,7 @@ pragma solidity >=0.8.27; import { ICiphernodeRegistry } from "../interfaces/ICiphernodeRegistry.sol"; import { IBondingRegistry } from "../interfaces/IBondingRegistry.sol"; import { IEnclave } from "../interfaces/IEnclave.sol"; +import { ISlashingManager } from "../interfaces/ISlashingManager.sol"; import { OwnableUpgradeable } from "@openzeppelin/contracts-upgradeable/access/OwnableUpgradeable.sol"; @@ -66,6 +67,9 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { /// @notice Maps E3 ID to its committee data mapping(uint256 e3Id => Committee committee) internal committees; + /// @notice Address of the slashing manager authorized to expel committee members + ISlashingManager public slashingManager; + //////////////////////////////////////////////////////////// // // // Errors // @@ -143,6 +147,9 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { /// @notice Caller is not authorized error Unauthorized(); + /// @notice Caller is not the slashing manager + error NotSlashingManager(); + /// @notice Not enough registered ciphernodes to meet threshold /// @param requested The requested committee size (N) /// @param available The number of registered ciphernodes @@ -175,6 +182,12 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { _; } + /// @dev Restricts function access to only the slashing manager + modifier onlySlashingManager() { + require(msg.sender == address(slashingManager), NotSlashingManager()); + _; + } + //////////////////////////////////////////////////////////// // // // Initialization // @@ -213,6 +226,11 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { //////////////////////////////////////////////////////////// /// @inheritdoc ICiphernodeRegistry + /// @dev Uses numActiveOperators() which checks registered + minimum bond + minimum tickets. + /// Between request time and ticket submission, operators may become inactive by losing + /// bond or tickets. The check at request time may be stale by the time submitTicket + /// is called. This is appropriately conservative — it prevents requesting committees + /// when not enough operators are active even at request time. function requestCommittee( uint256 e3Id, uint256 seed, @@ -266,6 +284,10 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { // TODO: Currently we trust the owner to publish the correct committee. // TODO: Need a Proof that the public key is generated from the committee + // SECURITY: Without DKG correctness proofs, a malicious owner could publish a key they + // control, enabling decryption of all E3 results. This is a centralization assumption + // accepted for the current phase. DKG proof verification must be added before + // decentralizing the owner role. c.publicKey = publicKeyHash; publicKeyHashes[e3Id] = publicKeyHash; // Progress E3 to KeyPublished stage @@ -354,7 +376,7 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { require(c.initialized, CommitteeNotRequested()); require(!c.finalized, CommitteeAlreadyFinalized()); require( - block.timestamp >= c.committeeDeadline, + block.timestamp > c.committeeDeadline, SubmissionWindowNotClosed() ); c.finalized = true; @@ -375,6 +397,16 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { } c.committee = c.topNodes; + // Initialize active committee tracking in Committee struct + uint256 committeeLen = c.committee.length; + for (uint256 i = 0; i < committeeLen; ) { + c.active[c.committee[i]] = true; + unchecked { + ++i; + } + } + c.activeCount = committeeLen; + enclave.onCommitteeFinalized(e3Id); emit CommitteeFinalized(e3Id, c.topNodes); return true; @@ -406,6 +438,16 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { emit BondingRegistrySet(address(_bondingRegistry)); } + /// @notice Sets the slashing manager contract address + /// @dev Only callable by owner + /// @param _slashingManager Address of the slashing manager contract + function setSlashingManager( + ISlashingManager _slashingManager + ) public onlyOwner { + require(address(_slashingManager) != address(0), ZeroAddress()); + slashingManager = _slashingManager; + } + /// @inheritdoc ICiphernodeRegistry function setSortitionSubmissionWindow( uint256 _sortitionSubmissionWindow @@ -496,6 +538,87 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { return c.committeeDeadline; } + //////////////////////////////////////////////////////////// + // // + // Committee Expulsion Functions // + // // + //////////////////////////////////////////////////////////// + + /// @inheritdoc ICiphernodeRegistry + function expelCommitteeMember( + uint256 e3Id, + address node, + bytes32 reason + ) + external + onlySlashingManager + returns (uint256 activeCount, uint32 thresholdM) + { + Committee storage c = committees[e3Id]; + require(c.finalized, CommitteeNotFinalized()); + thresholdM = c.threshold[0]; + + // Idempotent: if already expelled, return current state + if (!c.active[node]) { + activeCount = c.activeCount; + return (activeCount, thresholdM); + } + + c.active[node] = false; + c.activeCount--; + + activeCount = c.activeCount; + emit CommitteeMemberExpelled(e3Id, node, reason, activeCount); + + // Emit viability update + bool viable = activeCount >= thresholdM; + emit CommitteeViabilityUpdated(e3Id, activeCount, thresholdM, viable); + } + + /// @inheritdoc ICiphernodeRegistry + function isCommitteeMemberActive( + uint256 e3Id, + address node + ) external view returns (bool) { + return committees[e3Id].active[node]; + } + + /// @inheritdoc ICiphernodeRegistry + function getActiveCommitteeNodes( + uint256 e3Id + ) external view returns (address[] memory) { + Committee storage c = committees[e3Id]; + uint256 total = c.committee.length; + uint256 actCount = c.activeCount; + + address[] memory activeNodes = new address[](actCount); + uint256 idx = 0; + for (uint256 i = 0; i < total; ) { + if (c.active[c.committee[i]]) { + activeNodes[idx] = c.committee[i]; + idx++; + } + unchecked { + ++i; + } + } + return activeNodes; + } + + /// @inheritdoc ICiphernodeRegistry + function getActiveCommitteeCount( + uint256 e3Id + ) external view returns (uint256) { + return committees[e3Id].activeCount; + } + + /// @inheritdoc ICiphernodeRegistry + function getCommitteeThreshold( + uint256 e3Id + ) external view returns (uint32[2] memory) { + return committees[e3Id].threshold; + } + //////////////////////////////////////////////////////////// // // // Internal Functions // @@ -521,7 +644,10 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { } /// @notice Validates that a node is eligible to submit a ticket - /// @dev Uses snapshot of ticket balance at E3 request block for deterministic validation + /// @dev Uses snapshot of ticket balance at (requestBlock - 1) for deterministic validation. + /// The -1 offset prevents same-block manipulation attacks where an operator could deposit + /// tickets and submit in the same transaction. Deposits in the request block itself are + /// excluded. This is conservative but not fully settled — see TODO below. /// @param node Address of the ciphernode /// @param ticketNumber The ticket number being submitted /// @param e3Id ID of the E3 computation @@ -554,7 +680,11 @@ contract CiphernodeRegistryOwnable is ICiphernodeRegistry, OwnableUpgradeable { } /// @notice Inserts a node into the top-N list - Smallest scores - /// @dev If the node is not in the top-N, it is added to the top-N. + /// @dev O(N) linear scan per insertion to find the worst score. For a committee of size N + /// with S total submissions, total gas is O(N * S). With N=20 and S=1000, this is ~20K + /// iterations at ~200 gas each (≈ 4M gas total), which is acceptable for current + /// parameters. Will not scale to N > ~50 without switching to a heap or sorted + /// data structure. /// @param c Committee storage reference /// @param node Address of the node /// @param score Score of the node diff --git a/packages/enclave-contracts/contracts/slashing/SlashingManager.sol b/packages/enclave-contracts/contracts/slashing/SlashingManager.sol index 7001ef127d..7cbfd2c680 100644 --- a/packages/enclave-contracts/contracts/slashing/SlashingManager.sol +++ b/packages/enclave-contracts/contracts/slashing/SlashingManager.sol @@ -9,26 +9,32 @@ pragma solidity >=0.8.27; import { AccessControl } from "@openzeppelin/contracts/access/AccessControl.sol"; +import { ECDSA } from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import { + MessageHashUtils +} from "@openzeppelin/contracts/utils/cryptography/MessageHashUtils.sol"; import { ISlashingManager } from "../interfaces/ISlashingManager.sol"; import { IBondingRegistry } from "../interfaces/IBondingRegistry.sol"; -import { ISlashVerifier } from "../interfaces/ISlashVerifier.sol"; +import { ICiphernodeRegistry } from "../interfaces/ICiphernodeRegistry.sol"; +import { IEnclave } from "../interfaces/IEnclave.sol"; +import { ICircuitVerifier } from "../interfaces/ICircuitVerifier.sol"; /** * @title SlashingManager - * @notice Implementation of slashing management with proposal, appeal, and execution workflows - * @dev Role-based access control for slashers, verifiers, and governance with configurable slash policies + * @notice Implementation of slashing management with two-lane architecture: + * Lane A (proof-based): permissionless, atomic propose+execute, no appeals + * Lane B (evidence-based): SLASHER_ROLE required, appeal window, separate execute + * @dev Role-based access control for slashers and governance with configurable slash policies. + * Integrates with CiphernodeRegistry for committee expulsion and Enclave for E3 failure. */ contract SlashingManager is ISlashingManager, AccessControl { // ====================== // Constants & Roles // ====================== - /// @notice Role identifier for accounts authorized to propose and execute slashes + /// @notice Role identifier for accounts authorized to propose evidence-based slashes bytes32 public constant SLASHER_ROLE = keccak256("SLASHER_ROLE"); - /// @notice Role identifier for accounts authorized to verify cryptographic proofs in slash proposals - bytes32 public constant VERIFIER_ROLE = keccak256("VERIFIER_ROLE"); - /// @notice Role identifier for governance accounts that can configure policies, resolve appeals, and manage bans bytes32 public constant GOVERNANCE_ROLE = keccak256("GOVERNANCE_ROLE"); @@ -37,45 +43,54 @@ contract SlashingManager is ISlashingManager, AccessControl { // ====================== /// @notice Reference to the bonding registry contract where slash penalties are executed - /// @dev Used to call slashTicketBalance() and slashLicenseBond() when executing slashes IBondingRegistry public bondingRegistry; + /// @notice Reference to the ciphernode registry for committee expulsion + ICiphernodeRegistry public ciphernodeRegistry; + + /// @notice Reference to the Enclave contract for E3 failure signaling + IEnclave public enclave; + /// @notice Mapping from slash reason hash to its configured policy - /// @dev Stores penalty amounts, proof requirements, and appeal settings for each slash type mapping(bytes32 reason => SlashPolicy policy) public slashPolicies; /// @notice Internal storage for all slash proposals indexed by proposal ID - /// @dev Sequentially indexed starting from 0, accessed via getSlashProposal() mapping(uint256 proposalId => SlashProposal proposal) internal _proposals; /// @notice Counter for total number of slash proposals ever created - /// @dev Also serves as the next proposal ID to be assigned uint256 public totalProposals; /// @notice Mapping tracking which nodes are currently banned from the network - /// @dev Set to true when a node is banned (either via executeSlash or banNode), false when unbanned mapping(address node => bool banned) public banned; + /// @notice Evidence replay protection: tracks consumed evidence keys + /// @dev Key is keccak256(abi.encode(e3Id, operator, keccak256(proof))) — reason-independent + /// to prevent the same proof/evidence from being used to slash under multiple reasons. + mapping(bytes32 evidenceKey => bool consumed) public evidenceConsumed; + + // ====================== + // Constants + // ====================== + + /// @notice EIP-712 style typehash for the operator's signed proof payload. + /// @dev Must match `ProofPayload::typehash()` in `crates/events/src/enclave_event/signed_proof.rs`. + /// Prevents cross-chain, cross-E3, and cross-proof-type replay of signed proofs. + bytes32 public constant PROOF_PAYLOAD_TYPEHASH = + keccak256( + "ProofPayload(uint256 chainId,uint256 e3Id,uint256 proofType,bytes zkProof,bytes publicSignals)" + ); + // ====================== // Modifiers // ====================== /// @notice Restricts function access to accounts with SLASHER_ROLE - /// @dev Reverts with Unauthorized() if caller lacks the role modifier onlySlasher() { if (!hasRole(SLASHER_ROLE, msg.sender)) revert Unauthorized(); _; } - /// @notice Restricts function access to accounts with VERIFIER_ROLE - /// @dev Reverts with Unauthorized() if caller lacks the role - modifier onlyVerifier() { - if (!hasRole(VERIFIER_ROLE, msg.sender)) revert Unauthorized(); - _; - } - /// @notice Restricts function access to accounts with GOVERNANCE_ROLE - /// @dev Reverts with Unauthorized() if caller lacks the role modifier onlyGovernance() { if (!hasRole(GOVERNANCE_ROLE, msg.sender)) revert Unauthorized(); _; @@ -86,19 +101,26 @@ contract SlashingManager is ISlashingManager, AccessControl { // ====================== /** - * @notice Initializes the SlashingManager contract with admin and bonding registry - * @dev Sets up initial role assignments and bonding registry reference + * @notice Initializes the SlashingManager contract * @param admin Address to receive DEFAULT_ADMIN_ROLE and GOVERNANCE_ROLE - * @param _bondingRegistry Address of the bonding registry contract for executing slashes - * Requirements: - * - admin must not be zero address - * - _bondingRegistry must not be zero address + * @param _bondingRegistry Address of the bonding registry contract + * @param _ciphernodeRegistry Address of the ciphernode registry contract + * @param _enclave Address of the Enclave contract */ - constructor(address admin, address _bondingRegistry) { + constructor( + address admin, + address _bondingRegistry, + address _ciphernodeRegistry, + address _enclave + ) { require(admin != address(0), ZeroAddress()); require(_bondingRegistry != address(0), ZeroAddress()); + require(_ciphernodeRegistry != address(0), ZeroAddress()); + require(_enclave != address(0), ZeroAddress()); bondingRegistry = IBondingRegistry(_bondingRegistry); + ciphernodeRegistry = ICiphernodeRegistry(_ciphernodeRegistry); + enclave = IEnclave(_enclave); _grantRole(DEFAULT_ADMIN_ROLE, admin); _grantRole(GOVERNANCE_ROLE, admin); @@ -146,7 +168,6 @@ contract SlashingManager is ISlashingManager, AccessControl { if (policy.requiresProof) { require(policy.proofVerifier != address(0), VerifierNotSet()); - // TODO: Should we allow appeal window for proof required? require(policy.appealWindow == 0, InvalidPolicy()); } else { require(policy.appealWindow > 0, InvalidPolicy()); @@ -164,6 +185,24 @@ contract SlashingManager is ISlashingManager, AccessControl { bondingRegistry = IBondingRegistry(newBondingRegistry); } + /// @notice Updates the ciphernode registry contract address + /// @param newCiphernodeRegistry Address of the new ICiphernodeRegistry contract + function setCiphernodeRegistry( + address newCiphernodeRegistry + ) external onlyRole(DEFAULT_ADMIN_ROLE) { + require(newCiphernodeRegistry != address(0), ZeroAddress()); + ciphernodeRegistry = ICiphernodeRegistry(newCiphernodeRegistry); + } + + /// @notice Updates the Enclave contract address + /// @param newEnclave Address of the new IEnclave contract + function setEnclave( + address newEnclave + ) external onlyRole(DEFAULT_ADMIN_ROLE) { + require(newEnclave != address(0), ZeroAddress()); + enclave = IEnclave(newEnclave); + } + /// @inheritdoc ISlashingManager function addSlasher(address slasher) external onlyRole(DEFAULT_ADMIN_ROLE) { require(slasher != address(0), ZeroAddress()); @@ -177,69 +216,132 @@ contract SlashingManager is ISlashingManager, AccessControl { _revokeRole(SLASHER_ROLE, slasher); } - /// @inheritdoc ISlashingManager - function addVerifier( - address verifier - ) external onlyRole(DEFAULT_ADMIN_ROLE) { - require(verifier != address(0), ZeroAddress()); - _grantRole(VERIFIER_ROLE, verifier); - } - - /// @inheritdoc ISlashingManager - function removeVerifier( - address verifier - ) external onlyRole(DEFAULT_ADMIN_ROLE) { - _revokeRole(VERIFIER_ROLE, verifier); - } - // ====================== // Slashing Functions // ====================== /// @inheritdoc ISlashingManager + /// @dev Lane A: Permissionless proof-based slash. Anyone can call. + /// Atomically proposes, verifies operator signature + ZK proof, and executes slash. + /// Evidence format: + /// `abi.encode(bytes zkProof, bytes32[] publicInputs, + /// bytes signature, + /// uint256 chainId, + /// uint256 proofType, + /// address verifier)` + /// The operator must have signed: + /// `keccak256(abi.encode(PROOF_PAYLOAD_TYPEHASH, + /// chainId, + /// e3Id, + /// proofType, + /// keccak256(zkProof), + /// keccak256(abi.encodePacked(publicInputs))))` + /// This prevents: + /// - Arbitrary proof submission (attacker can't forge operator's signature) + /// - Cross-E3 replay (e3Id is in the signed message)` + /// - Cross-chain replay (chainId is in the signed message)` + /// - Verifier-upgrade attacks (verifier in evidence must match policy's current verifier)` function proposeSlash( + uint256 e3Id, address operator, bytes32 reason, bytes calldata proof - ) - external - // TODO: Do we need an onlySlasher modifier? - // Can anyone propose a slash? - onlySlasher - returns (uint256 proposalId) - { + ) external returns (uint256 proposalId) { require(operator != address(0), ZeroAddress()); SlashPolicy memory policy = slashPolicies[reason]; require(policy.enabled, SlashReasonDisabled()); + require(policy.requiresProof, InvalidPolicy()); + require(proof.length != 0, ProofRequired()); + + // Evidence replay protection — reason-independent to prevent cross-reason replay (M-05) + bytes32 evidenceKey = keccak256( + abi.encode(e3Id, operator, keccak256(proof)) + ); + require(!evidenceConsumed[evidenceKey], DuplicateEvidence()); + evidenceConsumed[evidenceKey] = true; + + // Verify evidence: signature, committee membership, and ZK proof + _verifyProofEvidence(proof, e3Id, operator, policy.proofVerifier); + // Create proposal proposalId = totalProposals; totalProposals = proposalId + 1; - uint256 executableAt = block.timestamp + policy.appealWindow; SlashProposal storage p = _proposals[proposalId]; - + p.e3Id = e3Id; p.operator = operator; p.reason = reason; p.ticketAmount = policy.ticketPenalty; p.licenseAmount = policy.licensePenalty; p.proposedAt = block.timestamp; - p.executableAt = executableAt; + p.executableAt = block.timestamp; p.proposer = msg.sender; p.proofHash = keccak256(proof); + p.proofVerified = true; + // Snapshot behavioral flags from policy at proposal time + p.banNode = policy.banNode; + p.affectsCommittee = policy.affectsCommittee; + p.failureReason = policy.failureReason; - if (policy.requiresProof) { - require(proof.length != 0, ProofRequired()); - bool ok = ISlashVerifier(policy.proofVerifier).verify( - proposalId, - proof - ); - require(ok, InvalidProof()); - p.proofVerified = true; - } + emit SlashProposed( + proposalId, + e3Id, + operator, + reason, + policy.ticketPenalty, + policy.licensePenalty, + block.timestamp, + msg.sender + ); + + _executeSlash(proposalId); + } + + /// @inheritdoc ISlashingManager + /// @dev Lane B: Evidence-based slash with appeal window. SLASHER_ROLE required. + function proposeSlashEvidence( + uint256 e3Id, + address operator, + bytes32 reason, + bytes calldata evidence + ) external onlySlasher returns (uint256 proposalId) { + require(operator != address(0), ZeroAddress()); + + SlashPolicy memory policy = slashPolicies[reason]; + require(policy.enabled, SlashReasonDisabled()); + require(!policy.requiresProof, InvalidPolicy()); + + // Evidence replay protection — reason-independent to prevent cross-reason replay (M-05) + bytes32 evidenceKey = keccak256( + abi.encode(e3Id, operator, keccak256(evidence)) + ); + require(!evidenceConsumed[evidenceKey], DuplicateEvidence()); + evidenceConsumed[evidenceKey] = true; + + proposalId = totalProposals; + totalProposals = proposalId + 1; + + uint256 executableAt = block.timestamp + policy.appealWindow; + SlashProposal storage p = _proposals[proposalId]; + p.e3Id = e3Id; + p.operator = operator; + p.reason = reason; + p.ticketAmount = policy.ticketPenalty; + p.licenseAmount = policy.licensePenalty; + p.proposedAt = block.timestamp; + p.executableAt = executableAt; + p.proposer = msg.sender; + p.proofHash = keccak256(evidence); + // Snapshot behavioral flags from policy at proposal time + // to prevent execution drift if policy is modified during appeal window + p.banNode = policy.banNode; + p.affectsCommittee = policy.affectsCommittee; + p.failureReason = policy.failureReason; emit SlashProposed( proposalId, + e3Id, operator, reason, policy.ticketPenalty, @@ -250,28 +352,126 @@ contract SlashingManager is ISlashingManager, AccessControl { } /// @inheritdoc ISlashingManager + /// @dev Only for evidence-based slashes (Lane B). Proof-based slashes execute atomically. function executeSlash(uint256 proposalId) external { require(proposalId < totalProposals, InvalidProposal()); SlashProposal storage p = _proposals[proposalId]; - - // Has already been executed? require(!p.executed, AlreadyExecuted()); - p.executed = true; - SlashPolicy memory policy = slashPolicies[p.reason]; + // Use snapshotted requiresProof state: proof-based slashes are already executed atomically in proposeSlash + require(!p.proofVerified, InvalidPolicy()); - if (policy.requiresProof) { - // Appeal window is 0 by policy validation, so we dont check for appeal gating - require(p.proofVerified, InvalidProof()); - } else { - // Evidence mode with appeals - require(block.timestamp >= p.executableAt, AppealWindowActive()); - if (p.appealed) { - require(p.resolved, AppealPending()); - require(!p.appealUpheld, AppealUpheld()); // approved = appeal upheld => cancel slash, return? + // Evidence mode: check appeal window + require(block.timestamp >= p.executableAt, AppealWindowActive()); + if (p.appealed) { + require(p.resolved, AppealPending()); + require(!p.appealUpheld, AppealUpheld()); + } + + _executeSlash(proposalId); + } + + // ====================== + // Internal Execution + // ====================== + + /// @dev Verifies the operator is/was a committee member for the given E3. + function _verifyCommitteeMembership( + uint256 e3Id, + address operator + ) internal view { + address[] memory committeeNodes = ciphernodeRegistry.getCommitteeNodes( + e3Id + ); + bool isMember = false; + for (uint256 i = 0; i < committeeNodes.length; i++) { + if (committeeNodes[i] == operator) { + isMember = true; + break; } } + require(isMember, OperatorNotInCommittee()); + } + + /// @dev Decodes evidence, verifies operator signature, committee membership, + /// and that the ZK proof is invalid (fault confirmed). + /// Evidence format: + /// `abi.encode(bytes zkProof, bytes32[] publicInputs, + /// bytes signature, + /// uint256 chainId, + /// uint256 proofType, + /// address verifier)` + function _verifyProofEvidence( + bytes calldata proof, + uint256 e3Id, + address operator, + address policyVerifier + ) internal view { + ( + bytes memory zkProof, + bytes32[] memory publicInputs, + bytes memory signature, + uint256 chainId, + uint256 proofType, + address signedVerifier + ) = abi.decode( + proof, + (bytes, bytes32[], bytes, uint256, uint256, address) + ); + + // 1. Verify verifier in evidence matches policy's current verifier. + require(signedVerifier == policyVerifier, VerifierMismatch()); + + // 1b. Verify chainId matches current chain to prevent cross-chain replay. + require(chainId == block.chainid, ChainIdMismatch()); + + // 2. Verify the operator signed this exact proof payload. + bytes32 messageHash = keccak256( + abi.encode( + PROOF_PAYLOAD_TYPEHASH, + chainId, + e3Id, + proofType, + keccak256(zkProof), + keccak256(abi.encodePacked(publicInputs)) + ) + ); + bytes32 ethSignedHash = MessageHashUtils.toEthSignedMessageHash( + messageHash + ); + address recoveredSigner = ECDSA.recover(ethSignedHash, signature); + require(recoveredSigner == operator, SignerIsNotOperator()); + + // 3. Verify committee membership. + _verifyCommitteeMembership(e3Id, operator); + // 4. Re-verify the ZK proof on-chain (INVERTED: must FAIL to confirm fault). + // The staticcall MUST succeed — if the verifier reverts or doesn't exist, + // we cannot determine fault and must not slash (M-04 fix). + (bool callSuccess, bytes memory returnData) = policyVerifier.staticcall( + abi.encodeCall(ICircuitVerifier.verify, (zkProof, publicInputs)) + ); + require(callSuccess, VerifierCallFailed()); + require(returnData.length >= 32, VerifierCallFailed()); + bool proofValid = abi.decode(returnData, (bool)); + if (proofValid) revert ProofIsValid(); + } + + /** + * @notice Internal function that executes a slash and handles committee expulsion + * @dev For Lane B (delayed execution), the operator may have deregistered during the appeal + * window. BondingRegistry.slashTicketBalance and slashLicenseBond use Math.min(requested, + * available), so zero-balance operators receive a zero slash gracefully. The exit queue's + * slashPendingAssets(includeLockedAssets=true) covers operators mid-exit. If the operator + * has already claimed their exit, funds are gone and the slash amount becomes 0. This is + * an accepted tradeoff for the appeal window design. + * @param proposalId ID of the proposal to execute + */ + function _executeSlash(uint256 proposalId) internal { + SlashProposal storage p = _proposals[proposalId]; + p.executed = true; + + // Execute financial penalties if (p.ticketAmount > 0) { bondingRegistry.slashTicketBalance( p.operator, @@ -288,18 +488,32 @@ contract SlashingManager is ISlashingManager, AccessControl { ); } - if (policy.banNode) { + // Ban node if snapshotted policy requires it + if (p.banNode) { banned[p.operator] = true; - emit NodeBanUpdated(p.operator, true, p.reason, msg.sender); + emit NodeBanUpdated(p.operator, true, p.reason, address(this)); + } + + // Committee expulsion for E3-scoped slashes (uses snapshotted behavioral flags) + // expelCommitteeMember returns (activeCount, thresholdM) — one call instead of three + if (p.affectsCommittee) { + (uint256 activeCount, uint32 thresholdM) = ciphernodeRegistry + .expelCommitteeMember(p.e3Id, p.operator, p.reason); + + // If active count drops below M, fail the E3 + if (activeCount < thresholdM && p.failureReason > 0) { + try enclave.onE3Failed(p.e3Id, p.failureReason) {} catch {} + } } emit SlashExecuted( proposalId, + p.e3Id, p.operator, p.reason, p.ticketAmount, p.licenseAmount, - p.executed + true ); } @@ -308,17 +522,22 @@ contract SlashingManager is ISlashingManager, AccessControl { // ====================== /// @inheritdoc ISlashingManager + /// @dev Only the accused operator can file an appeal. No delegate, multi-sig, or representative + /// patterns exist. If the operator has lost access to their key or been banned, they cannot + /// appeal. Consider adding an appealDelegate mapping for production to allow a designated + /// representative to appeal on behalf of the operator. function fileAppeal(uint256 proposalId, string calldata evidence) external { require(proposalId < totalProposals, InvalidProposal()); - // TODO: Should we reject the appeal if the proposal has a cryptographic proof? SlashProposal storage p = _proposals[proposalId]; // Only the accused can appeal require(msg.sender == p.operator, Unauthorized()); - // Only in the window + // Only within the appeal window require(block.timestamp < p.executableAt, AppealWindowExpired()); // Only once require(!p.appealed, AlreadyAppealed()); + // Cannot appeal proof-verified slashes (they have no appeal window) + require(!p.proofVerified, InvalidProposal()); p.appealed = true; @@ -338,7 +557,7 @@ contract SlashingManager is ISlashingManager, AccessControl { require(!p.resolved, AlreadyResolved()); p.resolved = true; - p.appealUpheld = appealUpheld; // true => cancel slash, false => slash stands + p.appealUpheld = appealUpheld; emit AppealResolved( proposalId, diff --git a/packages/enclave-contracts/contracts/test/MockCiphernodeRegistry.sol b/packages/enclave-contracts/contracts/test/MockCiphernodeRegistry.sol index db92a079df..a16cdc38cf 100644 --- a/packages/enclave-contracts/contracts/test/MockCiphernodeRegistry.sol +++ b/packages/enclave-contracts/contracts/test/MockCiphernodeRegistry.sol @@ -10,6 +10,20 @@ import { IEnclave } from "../interfaces/IEnclave.sol"; import { IBondingRegistry } from "../interfaces/IBondingRegistry.sol"; contract MockCiphernodeRegistry is ICiphernodeRegistry { + /// @notice Configurable committee members per E3 for testing + mapping(uint256 e3Id => address[] nodes) private _committeeNodes; + + /// @notice Set committee members for an E3 (test helper) + function setCommitteeNodes( + uint256 e3Id, + address[] calldata nodes + ) external { + delete _committeeNodes[e3Id]; + for (uint256 i = 0; i < nodes.length; i++) { + _committeeNodes[e3Id].push(nodes[i]); + } + } + function requestCommittee( uint256, uint256, @@ -52,10 +66,9 @@ contract MockCiphernodeRegistry is ICiphernodeRegistry { ) external pure {} // solhint-disable-line no-empty-blocks function getCommitteeNodes( - uint256 - ) external pure returns (address[] memory) { - address[] memory nodes = new address[](0); - return nodes; + uint256 e3Id + ) external view returns (address[] memory) { + return _committeeNodes[e3Id]; } function root() external pure returns (uint256) { @@ -94,6 +107,38 @@ contract MockCiphernodeRegistry is ICiphernodeRegistry { function isOpen(uint256) external pure returns (bool) { return false; } + + // solhint-disable-next-line no-empty-blocks + function expelCommitteeMember( + uint256, + address, + bytes32 + ) external pure returns (uint256, uint32) { + return (0, 0); + } + + function isCommitteeMemberActive( + uint256, + address + ) external pure returns (bool) { + return true; + } + + function getActiveCommitteeNodes( + uint256 + ) external pure returns (address[] memory) { + return new address[](0); + } + + function getActiveCommitteeCount(uint256) external pure returns (uint256) { + return 0; + } + + function getCommitteeThreshold( + uint256 + ) external pure returns (uint32[2] memory) { + return [uint32(0), uint32(0)]; + } } contract MockCiphernodeRegistryEmptyKey is ICiphernodeRegistry { @@ -179,4 +224,36 @@ contract MockCiphernodeRegistryEmptyKey is ICiphernodeRegistry { function isOpen(uint256) external pure returns (bool) { return false; } + + // solhint-disable-next-line no-empty-blocks + function expelCommitteeMember( + uint256, + address, + bytes32 + ) external pure returns (uint256, uint32) { + return (0, 0); + } + + function isCommitteeMemberActive( + uint256, + address + ) external pure returns (bool) { + return true; + } + + function getActiveCommitteeNodes( + uint256 + ) external pure returns (address[] memory) { + return new address[](0); + } + + function getActiveCommitteeCount(uint256) external pure returns (uint256) { + return 0; + } + + function getCommitteeThreshold( + uint256 + ) external pure returns (uint32[2] memory) { + return [uint32(0), uint32(0)]; + } } diff --git a/packages/enclave-contracts/contracts/test/MockSlashingVerifier.sol b/packages/enclave-contracts/contracts/test/MockSlashingVerifier.sol index 9a134afbea..5097d5eca8 100644 --- a/packages/enclave-contracts/contracts/test/MockSlashingVerifier.sol +++ b/packages/enclave-contracts/contracts/test/MockSlashingVerifier.sol @@ -5,15 +5,22 @@ // or FITNESS FOR A PARTICULAR PURPOSE. pragma solidity >=0.8.27; -import { ISlashVerifier } from "../interfaces/ISlashVerifier.sol"; +import { ICircuitVerifier } from "../interfaces/ICircuitVerifier.sol"; -contract MockSlashingVerifier is ISlashVerifier { - function verify( - uint256, - bytes memory data - ) external pure returns (bool success) { - data; +/// @notice Mock circuit verifier for testing. Returns configurable result. +/// @dev Default returnValue = false means proof is invalid = fault confirmed (slash proceeds). +/// Set returnValue = true to simulate a valid proof = no fault (ProofIsValid revert). +contract MockCircuitVerifier is ICircuitVerifier { + bool public returnValue; + + function setReturnValue(bool _returnValue) external { + returnValue = _returnValue; + } - if (data.length > 0) success = true; + function verify( + bytes calldata, + bytes32[] calldata + ) external view returns (bool) { + return returnValue; } } diff --git a/packages/enclave-contracts/contracts/token/EnclaveTicketToken.sol b/packages/enclave-contracts/contracts/token/EnclaveTicketToken.sol index 9d932d6a66..b7be111ef8 100644 --- a/packages/enclave-contracts/contracts/token/EnclaveTicketToken.sol +++ b/packages/enclave-contracts/contracts/token/EnclaveTicketToken.sol @@ -57,6 +57,10 @@ contract EnclaveTicketToken is /// @dev Only this contract can call restricted functions like depositFor, withdrawTo, burnTickets, and payout address public registry; + /// @notice Tracks slashed funds available for payout (L-12 defense-in-depth) + /// @dev Incremented by burnTickets, decremented by payout. Prevents payout exceeding slashed amount. + uint256 public payableBalance; + /// @notice Restricts function access to only the registry contract /// @dev Reverts with NotRegistry if caller is not the registry address modifier onlyRegistry() { @@ -101,6 +105,7 @@ contract EnclaveTicketToken is * @dev Only callable by the registry contract. Transfers underlying tokens from the registry to * this contract and mints an equivalent amount of ticket tokens. Automatically delegates * voting power to the operator on their first deposit to enable voting power tracking. + * Combined with delegate() reverting DelegationLocked(), operators permanently self-delegate. * @param operator Address to receive the minted ticket tokens * @param amount Number of underlying tokens to deposit and ticket tokens to mint * @return success True if the deposit and minting succeeded @@ -172,6 +177,7 @@ contract EnclaveTicketToken is address operator, uint256 amount ) external onlyRegistry { + payableBalance += amount; _burn(operator, amount); } @@ -182,9 +188,18 @@ contract EnclaveTicketToken is * @param amount Amount of ticket tokens to payout. */ function payout(address to, uint256 amount) external onlyRegistry { + require(amount <= payableBalance, "Exceeds payable balance"); + payableBalance -= amount; SafeERC20.safeTransfer(IERC20(address(underlying())), to, amount); } + /** + * @dev Override approve to revert — ticket tokens are non-transferable. + */ + function approve(address, uint256) public pure override returns (bool) { + revert TransferNotAllowed(); + } + /** * @dev Override ERC20Votes update hook to prevent transfers between users. */ diff --git a/packages/enclave-contracts/contracts/token/EnclaveToken.sol b/packages/enclave-contracts/contracts/token/EnclaveToken.sol index f1e5534008..0443d2d218 100644 --- a/packages/enclave-contracts/contracts/token/EnclaveToken.sol +++ b/packages/enclave-contracts/contracts/token/EnclaveToken.sol @@ -60,7 +60,6 @@ contract EnclaveToken is bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); /// @notice Tracks the cumulative amount of tokens minted since deployment - /// @dev Incremented with each mint operation to enforce MAX_SUPPLY cap uint256 public totalMinted; /// @notice Mapping of addresses permitted to transfer tokens when restrictions are active @@ -158,6 +157,7 @@ contract EnclaveToken is for (uint256 i = 0; i < len; i++) { address recipient = recipients[i]; uint256 amount = amounts[i]; + if (recipient == address(0)) revert ZeroAddress(); if (amount == 0) revert ZeroAmount(); if (amount > MAX_SUPPLY - minted) revert ExceedsTotalSupply(); @@ -222,6 +222,7 @@ contract EnclaveToken is * @dev Overrides ERC20 and ERC20Votes to add transfer restriction logic. Reverts if transfers * are restricted and neither sender nor receiver is whitelisted. Minting (from == 0) and * burning (to == 0) are always allowed regardless of restrictions. + * * @param from Address sending tokens (zero address for minting) * @param to Address receiving tokens (zero address for burning) * @param value Amount of tokens being transferred diff --git a/packages/enclave-contracts/deployed_contracts.json b/packages/enclave-contracts/deployed_contracts.json index 703194cdfb..2e5973e418 100644 --- a/packages/enclave-contracts/deployed_contracts.json +++ b/packages/enclave-contracts/deployed_contracts.json @@ -132,5 +132,141 @@ "blockNumber": 10279540, "address": "0xB886C067e9C1D2B31461F4DFd29f557B5714297d" } + }, + "localhost": { + "PoseidonT3": { + "blockNumber": 3, + "address": "0x3333333C0A88F9BE4fd23ed0536F9B6c427e3B93" + }, + "MockUSDC": { + "constructorArgs": { + "initialSupply": "1000000" + }, + "blockNumber": 4, + "address": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" + }, + "EnclaveToken": { + "constructorArgs": { + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + }, + "blockNumber": 5, + "address": "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9" + }, + "EnclaveTicketToken": { + "constructorArgs": { + "baseToken": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", + "registry": "0x0000000000000000000000000000000000000001", + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + }, + "blockNumber": 7, + "address": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707" + }, + "SlashingManager": { + "constructorArgs": { + "admin": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "bondingRegistry": "0x0000000000000000000000000000000000000001", + "ciphernodeRegistry": "0x0000000000000000000000000000000000000001", + "enclave": "0x0000000000000000000000000000000000000001" + }, + "blockNumber": 8, + "address": "0x0165878A594ca255338adfa4d48449f69242Eb8F" + }, + "BondingRegistry": { + "constructorArgs": { + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "ticketToken": "0x5FC8d32690cc91D4c39d9d3abcBD16989F875707", + "licenseToken": "0xCf7Ed3AccA5a467e9e704C703E8D87F634fB0Fc9", + "registry": "0x0000000000000000000000000000000000000001", + "slashedFundsTreasury": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "ticketPrice": "10000000", + "licenseRequiredBond": "100000000000000000000", + "minTicketBalance": "1", + "exitDelay": "604800" + }, + "proxyRecords": { + "initData": "0x7333fa82000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb922660000000000000000000000005fc8d32690cc91d4c39d9d3abcbd16989f875707000000000000000000000000cf7ed3acca5a467e9e704c703e8d87f634fb0fc90000000000000000000000000000000000000000000000000000000000000001000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb9226600000000000000000000000000000000000000000000000000000000009896800000000000000000000000000000000000000000000000056bc75e2d6310000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000093a80", + "initialOwner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "proxyAddress": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6", + "proxyAdminAddress": "0x94099942864EA81cCF197E9D71ac53310b1468D8", + "implementationAddress": "0xa513E6E4b8f2a923D98304ec87F64353C4D5C853" + }, + "blockNumber": 8, + "address": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" + }, + "CiphernodeRegistryOwnable": { + "constructorArgs": { + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "enclaveAddress": "0x0000000000000000000000000000000000000001", + "submissionWindow": "10" + }, + "proxyRecords": { + "initData": "0x1794bb3c000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb922660000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000a", + "initialOwner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "proxyAddress": "0x610178dA211FEF7D417bC0e6FeD39F05609AD788", + "proxyAdminAddress": "0x6F1216D1BFe15c98520CA1434FC1d9D57AC95321", + "implementationAddress": "0x8A791620dd6260079BF849Dc5567aDC3F2FdC318" + }, + "blockNumber": 11, + "address": "0x610178dA211FEF7D417bC0e6FeD39F05609AD788" + }, + "Enclave": { + "constructorArgs": { + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "registry": "0x610178dA211FEF7D417bC0e6FeD39F05609AD788", + "bondingRegistry": "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6", + "e3RefundManager": "0x0000000000000000000000000000000000000001", + "feeToken": "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0", + "maxDuration": "2592000", + "timeoutConfig": "{\"committeeFormationWindow\":3600,\"dkgWindow\":7200,\"computeWindow\":86400,\"decryptionWindow\":3600,\"gracePeriod\":600}", + "params": [ + "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000ffffee0010000000000000000000000000000000000000000000000000000000ffffc400100000000000000000000000000000000000000000000000000000000000000013300000000000000000000000000000000000000000000000000000000000000" + ] + }, + "proxyRecords": { + "initData": "0x69c5b347000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266000000000000000000000000610178da211fef7d417bc0e6fed39f05609ad7880000000000000000000000002279b7a0a67db372996a5fab50d91eaa73d2ebe600000000000000000000000000000000000000000000000000000000000000010000000000000000000000009fe46736679d2d9a65f0992f2272de9f3c7fa6e00000000000000000000000000000000000000000000000000000000000278d000000000000000000000000000000000000000000000000000000000000001c2000000000000000000000000000000000000000000000000000000000000151800000000000000000000000000000000000000000000000000000000000000e1000000000000000000000000000000000000000000000000000000000000002580000000000000000000000000000000000000000000000000000000000000160000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000140000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000064000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000ffffee0010000000000000000000000000000000000000000000000000000000ffffc400100000000000000000000000000000000000000000000000000000000000000013300000000000000000000000000000000000000000000000000000000000000", + "initialOwner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "proxyAddress": "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0", + "proxyAdminAddress": "0x1F708C24a0D3A740cD47cC0444E9480899f3dA7D", + "implementationAddress": "0xB7f8BC63BbcaD18155201308C8f3540b07f84F5e" + }, + "blockNumber": 13, + "address": "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0" + }, + "E3RefundManager": { + "constructorArgs": { + "owner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "enclave": "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0", + "treasury": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + }, + "proxyRecords": { + "initData": "0xc0c53b8b000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266000000000000000000000000a51c1fc2f0d1a1b8494ed1fe312d7c3a78ed91c0000000000000000000000000f39fd6e51aad88f6f4ce6ab8827279cfffb92266", + "initialOwner": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "proxyAddress": "0x9A676e781A523b5d0C0e43731313A708CB607508", + "proxyAdminAddress": "0x8e80FFe6Dc044F4A766Afd6e5a8732Fe0977A493", + "implementationAddress": "0x0DCd1Bf9A1b36cE34237eEaFef220932846BCD82" + }, + "blockNumber": 15, + "address": "0x9A676e781A523b5d0C0e43731313A708CB607508" + }, + "MockComputeProvider": { + "blockNumber": 29, + "address": "0x09635F643e140090A9A8Dcd712eD6285858ceBef" + }, + "MockDecryptionVerifier": { + "blockNumber": 30, + "address": "0xc5a5C42992dECbae36851359345FE25997F5C42d" + }, + "MockE3Program": { + "blockNumber": 31, + "address": "0x67d269191c92Caf3cD7723F116c85e6E9bf55933" + }, + "ZKTranscriptLib": { + "blockNumber": 34, + "address": "0x84eA74d481Ee0A5332c457a4d796187F6Ba67fEB" + }, + "DkgPkVerifier": { + "blockNumber": 35, + "address": "0x9E545E3C0baAB3E08CdfD552C960A1050f373042" + } } } \ No newline at end of file diff --git a/packages/enclave-contracts/ignition/modules/enclave.ts b/packages/enclave-contracts/ignition/modules/enclave.ts index 9f216e7544..e6990e143f 100644 --- a/packages/enclave-contracts/ignition/modules/enclave.ts +++ b/packages/enclave-contracts/ignition/modules/enclave.ts @@ -18,7 +18,6 @@ export default buildModule("Enclave", (m) => { dkgWindow: 7200, computeWindow: 86400, decryptionWindow: 3600, - gracePeriod: 600, }); const enclaveImpl = m.contract("Enclave", []); diff --git a/packages/enclave-contracts/ignition/modules/mockSlashingVerifier.ts b/packages/enclave-contracts/ignition/modules/mockSlashingVerifier.ts index 3c4b663b6a..17a6ce70d6 100644 --- a/packages/enclave-contracts/ignition/modules/mockSlashingVerifier.ts +++ b/packages/enclave-contracts/ignition/modules/mockSlashingVerifier.ts @@ -5,8 +5,8 @@ // or FITNESS FOR A PARTICULAR PURPOSE. import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; -export default buildModule("MockSlashingVerifier", (m) => { - const mockSlashingVerifier = m.contract("MockSlashingVerifier"); +export default buildModule("MockCircuitVerifier", (m) => { + const mockCircuitVerifier = m.contract("MockCircuitVerifier"); - return { mockSlashingVerifier }; + return { mockCircuitVerifier }; }) as any; diff --git a/packages/enclave-contracts/ignition/modules/slashingManager.ts b/packages/enclave-contracts/ignition/modules/slashingManager.ts index 0d5919900e..e44ad80287 100644 --- a/packages/enclave-contracts/ignition/modules/slashingManager.ts +++ b/packages/enclave-contracts/ignition/modules/slashingManager.ts @@ -3,17 +3,19 @@ // This file is provided WITHOUT ANY WARRANTY; // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. - -/* eslint-disable @typescript-eslint/no-explicit-any */ import { buildModule } from "@nomicfoundation/hardhat-ignition/modules"; export default buildModule("SlashingManager", (m) => { const bondingRegistry = m.getParameter("bondingRegistry"); + const ciphernodeRegistry = m.getParameter("ciphernodeRegistry"); + const enclave = m.getParameter("enclave"); const admin = m.getParameter("admin"); const slashingManager = m.contract("SlashingManager", [ admin, bondingRegistry, + ciphernodeRegistry, + enclave, ]); return { slashingManager }; diff --git a/packages/enclave-contracts/scripts/deployAndSave/enclave.ts b/packages/enclave-contracts/scripts/deployAndSave/enclave.ts index ff60f4217c..b8aa4f0dfd 100644 --- a/packages/enclave-contracts/scripts/deployAndSave/enclave.ts +++ b/packages/enclave-contracts/scripts/deployAndSave/enclave.ts @@ -21,7 +21,6 @@ export interface E3TimeoutConfig { dkgWindow: number; computeWindow: number; decryptionWindow: number; - gracePeriod: number; } /** diff --git a/packages/enclave-contracts/scripts/deployAndSave/slashingManager.ts b/packages/enclave-contracts/scripts/deployAndSave/slashingManager.ts index 47b7e179e6..fdd28469b8 100644 --- a/packages/enclave-contracts/scripts/deployAndSave/slashingManager.ts +++ b/packages/enclave-contracts/scripts/deployAndSave/slashingManager.ts @@ -17,6 +17,8 @@ import { readDeploymentArgs, storeDeploymentArgs } from "../utils"; export interface SlashingManagerArgs { admin?: string; bondingRegistry?: string; + ciphernodeRegistry?: string; + enclave?: string; hre: HardhatRuntimeEnvironment; } @@ -28,6 +30,8 @@ export interface SlashingManagerArgs { export const deployAndSaveSlashingManager = async ({ admin, bondingRegistry, + ciphernodeRegistry, + enclave, hre, }: SlashingManagerArgs): Promise<{ slashingManager: SlashingManager; @@ -41,8 +45,13 @@ export const deployAndSaveSlashingManager = async ({ if ( !admin || !bondingRegistry || + !ciphernodeRegistry || + !enclave || (preDeployedArgs?.constructorArgs?.admin === admin && - preDeployedArgs?.constructorArgs?.bondingRegistry === bondingRegistry) + preDeployedArgs?.constructorArgs?.bondingRegistry === bondingRegistry && + preDeployedArgs?.constructorArgs?.ciphernodeRegistry === + ciphernodeRegistry && + preDeployedArgs?.constructorArgs?.enclave === enclave) ) { if (!preDeployedArgs?.address) { throw new Error( @@ -61,6 +70,8 @@ export const deployAndSaveSlashingManager = async ({ const slashingManager = await slashingManagerFactory.deploy( admin, bondingRegistry, + ciphernodeRegistry, + enclave, ); await slashingManager.waitForDeployment(); @@ -74,6 +85,8 @@ export const deployAndSaveSlashingManager = async ({ constructorArgs: { admin, bondingRegistry, + ciphernodeRegistry, + enclave, }, blockNumber, address: slashingManagerAddress, diff --git a/packages/enclave-contracts/scripts/deployEnclave.ts b/packages/enclave-contracts/scripts/deployEnclave.ts index 5c9670f0be..8da5a2668d 100644 --- a/packages/enclave-contracts/scripts/deployEnclave.ts +++ b/packages/enclave-contracts/scripts/deployEnclave.ts @@ -26,7 +26,6 @@ const DEFAULT_TIMEOUT_CONFIG = { dkgWindow: 7200, computeWindow: 86400, decryptionWindow: 3600, - gracePeriod: 600, }; /** @@ -102,6 +101,8 @@ export const deployEnclave = async (withMocks?: boolean) => { const { slashingManager } = await deployAndSaveSlashingManager({ admin: ownerAddress, bondingRegistry: addressOne, + ciphernodeRegistry: addressOne, + enclave: addressOne, hre, }); const slashingManagerAddress = await slashingManager.getAddress(); @@ -189,9 +190,18 @@ export const deployEnclave = async (withMocks?: boolean) => { console.log("Setting BondingRegistry address in SlashingManager..."); await slashingManager.setBondingRegistry(bondingRegistryAddress); + console.log("Setting CiphernodeRegistry address in SlashingManager..."); + await slashingManager.setCiphernodeRegistry(ciphernodeRegistryAddress); + + console.log("Setting Enclave address in SlashingManager..."); + await slashingManager.setEnclave(enclaveAddress); + console.log("Setting SlashingManager address in BondingRegistry..."); await bondingRegistry.setSlashingManager(slashingManagerAddress); + console.log("Setting SlashingManager address in CiphernodeRegistry..."); + await ciphernodeRegistry.setSlashingManager(slashingManagerAddress); + console.log("Setting Enclave as reward distributor in BondingRegistry..."); await bondingRegistry.setRewardDistributor(enclaveAddress); diff --git a/packages/enclave-contracts/scripts/utils.ts b/packages/enclave-contracts/scripts/utils.ts index a86684fe5d..53ca1ab1d4 100644 --- a/packages/enclave-contracts/scripts/utils.ts +++ b/packages/enclave-contracts/scripts/utils.ts @@ -39,6 +39,7 @@ export interface EnclaveConfig { enclave?: { address: string; deploy_block: number }; ciphernode_registry?: { address: string; deploy_block: number }; bonding_registry?: { address: string; deploy_block: number }; + slashing_manager?: { address: string; deploy_block: number }; fee_token?: { address: string; deploy_block: number }; }; }>; diff --git a/packages/enclave-contracts/test/E3Lifecycle/E3Integration.spec.ts b/packages/enclave-contracts/test/E3Lifecycle/E3Integration.spec.ts index 6b44dc185f..e42635d1cb 100644 --- a/packages/enclave-contracts/test/E3Lifecycle/E3Integration.spec.ts +++ b/packages/enclave-contracts/test/E3Lifecycle/E3Integration.spec.ts @@ -56,7 +56,6 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { dkgWindow: ONE_DAY, computeWindow: THREE_DAYS, decryptionWindow: ONE_DAY, - gracePeriod: ONE_HOUR, }; const abiCoder = ethers.AbiCoder.defaultAbiCoder(); @@ -128,6 +127,8 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { SlashingManager: { admin: ownerAddress, bondingRegistry: addressOne, // Will be updated + ciphernodeRegistry: addressOne, // Will be updated + enclave: addressOne, // Will be updated }, }, }, @@ -253,7 +254,14 @@ describe("E3 Integration - Refund/Timeout Mechanism", function () { await slashingManagerContract.slashingManager.setBondingRegistry( await bondingRegistry.getAddress(), ); + await slashingManagerContract.slashingManager.setCiphernodeRegistry( + ciphernodeRegistryAddress, + ); + await slashingManagerContract.slashingManager.setEnclave(enclaveAddress); await registry.setBondingRegistry(await bondingRegistry.getAddress()); + await registry.setSlashingManager( + await slashingManagerContract.slashingManager.getAddress(), + ); // Update ticket token registry await ticketTokenContract.enclaveTicketToken.setRegistry( diff --git a/packages/enclave-contracts/test/Enclave.spec.ts b/packages/enclave-contracts/test/Enclave.spec.ts index 4b6ac074e5..54969c83e6 100644 --- a/packages/enclave-contracts/test/Enclave.spec.ts +++ b/packages/enclave-contracts/test/Enclave.spec.ts @@ -43,7 +43,6 @@ describe("Enclave", function () { dkgWindow: 3600, // 1 hour computeWindow: 3600, // 1 hour decryptionWindow: 3600, // 1 hour - gracePeriod: 300, // 5 minutes }; const inputWindowDuration = 300; @@ -185,6 +184,8 @@ describe("Enclave", function () { SlashingManager: { admin: ownerAddress, bondingRegistry: addressOne, + ciphernodeRegistry: addressOne, + enclave: addressOne, }, }, }, @@ -931,11 +932,8 @@ describe("Enclave", function () { await enclave.publishCiphertextOutput(e3Id, data, proof); await expect(enclave.publishCiphertextOutput(e3Id, data, proof)) - .to.be.revertedWithCustomError( - enclave, - "CiphertextOutputAlreadyPublished", - ) - .withArgs(e3Id); + .to.be.revertedWithCustomError(enclave, "InvalidStage") + .withArgs(e3Id, 3, 4); }); it("reverts if committee duties are over", async function () { const { diff --git a/packages/enclave-contracts/test/Registry/BondingRegistry.spec.ts b/packages/enclave-contracts/test/Registry/BondingRegistry.spec.ts index b53d785db1..479f1e7269 100644 --- a/packages/enclave-contracts/test/Registry/BondingRegistry.spec.ts +++ b/packages/enclave-contracts/test/Registry/BondingRegistry.spec.ts @@ -97,6 +97,8 @@ describe("BondingRegistry", function () { SlashingManager: { admin: ownerAddress, bondingRegistry: AddressOne, + ciphernodeRegistry: AddressOne, + enclave: AddressOne, }, }, }, diff --git a/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts b/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts index 9a2db8f877..570395f84f 100644 --- a/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts +++ b/packages/enclave-contracts/test/Registry/CiphernodeRegistryOwnable.spec.ts @@ -134,6 +134,8 @@ describe("CiphernodeRegistryOwnable", function () { SlashingManager: { admin: ownerAddress, bondingRegistry: AddressOne, + ciphernodeRegistry: AddressOne, + enclave: AddressOne, }, }, }, @@ -175,7 +177,6 @@ describe("CiphernodeRegistryOwnable", function () { dkgWindow: 3600, computeWindow: 3600, decryptionWindow: 3600, - gracePeriod: 300, }, }, }, diff --git a/packages/enclave-contracts/test/Slashing/CommitteeExpulsion.spec.ts b/packages/enclave-contracts/test/Slashing/CommitteeExpulsion.spec.ts new file mode 100644 index 0000000000..ba1918e6fd --- /dev/null +++ b/packages/enclave-contracts/test/Slashing/CommitteeExpulsion.spec.ts @@ -0,0 +1,957 @@ +// SPDX-License-Identifier: LGPL-3.0-only +// +// This file is provided WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY +// or FITNESS FOR A PARTICULAR PURPOSE. + +/** + * Tests for committee expulsion, viability checks, and E3 failure on threshold breach. + * + * Verifies: + * - Committee members are expelled via proposeSlash when affectsCommittee=true + * - The E3 continues as long as active members >= threshold M + * - The E3 fails when active members drop below threshold M + * - Rewards exclude expelled members + * - Idempotent expulsion (re-slashing same node doesn't double-count) + */ +import { expect } from "chai"; +import type { Signer } from "ethers"; +import { network } from "hardhat"; + +import BondingRegistryModule from "../../ignition/modules/bondingRegistry"; +import CiphernodeRegistryModule from "../../ignition/modules/ciphernodeRegistry"; +import EnclaveModule from "../../ignition/modules/enclave"; +import EnclaveTicketTokenModule from "../../ignition/modules/enclaveTicketToken"; +import EnclaveTokenModule from "../../ignition/modules/enclaveToken"; +import MockDecryptionVerifierModule from "../../ignition/modules/mockDecryptionVerifier"; +import MockE3ProgramModule from "../../ignition/modules/mockE3Program"; +import MockCircuitVerifierModule from "../../ignition/modules/mockSlashingVerifier"; +import MockStableTokenModule from "../../ignition/modules/mockStableToken"; +import SlashingManagerModule from "../../ignition/modules/slashingManager"; +import { + BondingRegistry__factory as BondingRegistryFactory, + CiphernodeRegistryOwnable__factory as CiphernodeRegistryOwnableFactory, + Enclave__factory as EnclaveFactory, + EnclaveToken__factory as EnclaveTokenFactory, + MockCircuitVerifier__factory as MockCircuitVerifierFactory, + MockDecryptionVerifier__factory as MockDecryptionVerifierFactory, + MockE3Program__factory as MockE3ProgramFactory, + MockUSDC__factory as MockUSDCFactory, + SlashingManager__factory as SlashingManagerFactory, +} from "../../types"; + +const { ethers, ignition, networkHelpers } = await network.connect(); +const { loadFixture, time } = networkHelpers; + +describe("Committee Expulsion & Fault Tolerance", function () { + const ONE_HOUR = 60 * 60; + const ONE_DAY = 24 * ONE_HOUR; + const THREE_DAYS = 3 * ONE_DAY; + const SEVEN_DAYS = 7 * ONE_DAY; + const THIRTY_DAYS = 30 * ONE_DAY; + const SORTITION_SUBMISSION_WINDOW = 10; + const addressOne = "0x0000000000000000000000000000000000000001"; + + const REASON_BAD_DKG = ethers.keccak256( + ethers.toUtf8Bytes("E3_BAD_DKG_PROOF"), + ); + const REASON_BAD_DECRYPTION = ethers.keccak256( + ethers.toUtf8Bytes("E3_BAD_DECRYPTION_PROOF"), + ); + + const abiCoder = ethers.AbiCoder.defaultAbiCoder(); + const polynomial_degree = ethers.toBigInt(2048); + const plaintext_modulus = ethers.toBigInt(1032193); + const moduli = [ethers.toBigInt("18014398492704769")]; + + const encodedE3ProgramParams = abiCoder.encode( + ["uint256", "uint256", "uint256[]"], + [polynomial_degree, plaintext_modulus, moduli], + ); + + const encryptionSchemeId = + "0x2c2a814a0495f913a3a312fc4771e37552bc14f8a2d4075a08122d356f0849c6"; + + const defaultTimeoutConfig = { + dkgWindow: ONE_DAY, + computeWindow: THREE_DAYS, + decryptionWindow: ONE_DAY, + }; + + // Must match the PROOF_PAYLOAD_TYPEHASH in SlashingManager.sol + const PROOF_PAYLOAD_TYPEHASH = ethers.keccak256( + ethers.toUtf8Bytes( + "ProofPayload(uint256 chainId,uint256 e3Id,uint256 proofType,bytes zkProof,bytes publicSignals)", + ), + ); + + /** + * Helper to create a signed proof evidence bundle. + * The operator signs the proof payload (matching SlashingManager._verifyProofEvidence), + * then the evidence is encoded in the 6-field format expected by proposeSlash(). + */ + async function signAndEncodeProof( + signer: Signer, + e3Id: number, + verifierAddress: string, + zkProof: string = "0x1234", + publicInputs: string[] = [ethers.ZeroHash], + chainId: number = 31337, + proofType: number = 0, + ): Promise { + const messageHash = ethers.keccak256( + abiCoder.encode( + ["bytes32", "uint256", "uint256", "uint256", "bytes32", "bytes32"], + [ + PROOF_PAYLOAD_TYPEHASH, + chainId, + e3Id, + proofType, + ethers.keccak256(zkProof), + ethers.keccak256( + ethers.solidityPacked(["bytes32[]"], [publicInputs]), + ), + ], + ), + ); + const signature = await signer.signMessage(ethers.getBytes(messageHash)); + return abiCoder.encode( + ["bytes", "bytes32[]", "bytes", "uint256", "uint256", "address"], + [zkProof, publicInputs, signature, chainId, proofType, verifierAddress], + ); + } + + const setup = async () => { + const [ + owner, + requester, + treasury, + operator1, + operator2, + operator3, + operator4, + ] = await ethers.getSigners(); + + const ownerAddress = await owner.getAddress(); + const treasuryAddress = await treasury.getAddress(); + const requesterAddress = await requester.getAddress(); + + // Deploy tokens + const usdcContract = await ignition.deploy(MockStableTokenModule, { + parameters: { MockUSDC: { initialSupply: 10000000 } }, + }); + const usdcToken = MockUSDCFactory.connect( + await usdcContract.mockUSDC.getAddress(), + owner, + ); + + const enclTokenContract = await ignition.deploy(EnclaveTokenModule, { + parameters: { EnclaveToken: { owner: ownerAddress } }, + }); + const enclToken = EnclaveTokenFactory.connect( + await enclTokenContract.enclaveToken.getAddress(), + owner, + ); + await enclToken.setTransferRestriction(false); + + const ticketTokenContract = await ignition.deploy( + EnclaveTicketTokenModule, + { + parameters: { + EnclaveTicketToken: { + baseToken: await usdcToken.getAddress(), + registry: addressOne, + owner: ownerAddress, + }, + }, + }, + ); + + const mockVerifierContract = await ignition.deploy( + MockCircuitVerifierModule, + ); + const mockVerifier = MockCircuitVerifierFactory.connect( + await mockVerifierContract.mockCircuitVerifier.getAddress(), + owner, + ); + + // Deploy slashing manager + const slashingManagerContract = await ignition.deploy( + SlashingManagerModule, + { + parameters: { + SlashingManager: { + admin: ownerAddress, + bondingRegistry: addressOne, + ciphernodeRegistry: addressOne, + enclave: addressOne, + }, + }, + }, + ); + const slashingManager = SlashingManagerFactory.connect( + await slashingManagerContract.slashingManager.getAddress(), + owner, + ); + + // Deploy bonding registry + const bondingRegistryContract = await ignition.deploy( + BondingRegistryModule, + { + parameters: { + BondingRegistry: { + owner: ownerAddress, + ticketToken: + await ticketTokenContract.enclaveTicketToken.getAddress(), + licenseToken: await enclToken.getAddress(), + registry: addressOne, + slashedFundsTreasury: treasuryAddress, + ticketPrice: ethers.parseUnits("10", 6), + licenseRequiredBond: ethers.parseEther("1000"), + minTicketBalance: 1, + exitDelay: SEVEN_DAYS, + }, + }, + }, + ); + const bondingRegistry = BondingRegistryFactory.connect( + await bondingRegistryContract.bondingRegistry.getAddress(), + owner, + ); + + // Deploy Enclave + const enclaveContract = await ignition.deploy(EnclaveModule, { + parameters: { + Enclave: { + params: encodedE3ProgramParams, + owner: ownerAddress, + maxDuration: THIRTY_DAYS, + registry: addressOne, + e3RefundManager: addressOne, + bondingRegistry: await bondingRegistry.getAddress(), + feeToken: await usdcToken.getAddress(), + timeoutConfig: defaultTimeoutConfig, + }, + }, + }); + const enclaveAddress = await enclaveContract.enclave.getAddress(); + const enclave = EnclaveFactory.connect(enclaveAddress, owner); + + // Deploy CiphernodeRegistry + const ciphernodeRegistryContract = await ignition.deploy( + CiphernodeRegistryModule, + { + parameters: { + CiphernodeRegistry: { + enclaveAddress: enclaveAddress, + owner: ownerAddress, + submissionWindow: SORTITION_SUBMISSION_WINDOW, + }, + }, + }, + ); + const registryAddress = + await ciphernodeRegistryContract.cipherNodeRegistry.getAddress(); + const registry = CiphernodeRegistryOwnableFactory.connect( + registryAddress, + owner, + ); + + // Deploy mock E3 program + const e3ProgramContract = await ignition.deploy(MockE3ProgramModule, { + parameters: { + MockE3Program: { + encryptionSchemeId: encryptionSchemeId, + }, + }, + }); + const e3Program = MockE3ProgramFactory.connect( + await e3ProgramContract.mockE3Program.getAddress(), + owner, + ); + + // Deploy mock decryption verifier + const decryptionVerifierContract = await ignition.deploy( + MockDecryptionVerifierModule, + ); + const decryptionVerifier = MockDecryptionVerifierFactory.connect( + await decryptionVerifierContract.mockDecryptionVerifier.getAddress(), + owner, + ); + + // Wire everything together + await enclave.setCiphernodeRegistry(registryAddress); + await enclave.enableE3Program(await e3Program.getAddress()); + await enclave.setDecryptionVerifier( + encryptionSchemeId, + await decryptionVerifier.getAddress(), + ); + await enclave.setSlashingManager(await slashingManager.getAddress()); + + await bondingRegistry.setRewardDistributor(enclaveAddress); + await bondingRegistry.setRegistry(registryAddress); + await bondingRegistry.setSlashingManager( + await slashingManager.getAddress(), + ); + + await slashingManager.setBondingRegistry( + await bondingRegistry.getAddress(), + ); + await slashingManager.setCiphernodeRegistry(registryAddress); + await slashingManager.setEnclave(enclaveAddress); + + await registry.setBondingRegistry(await bondingRegistry.getAddress()); + await registry.setSlashingManager(await slashingManager.getAddress()); + + await ticketTokenContract.enclaveTicketToken.setRegistry( + await bondingRegistry.getAddress(), + ); + + // Mint tokens to requester for E3 requests + await usdcToken.mint(requesterAddress, ethers.parseUnits("100000", 6)); + + // Helper: setup an operator (bond license, register, add tickets) + async function setupOperator(operator: Signer) { + const operatorAddress = await operator.getAddress(); + await enclToken.mintAllocation( + operatorAddress, + ethers.parseEther("10000"), + "Test allocation", + ); + await usdcToken.mint(operatorAddress, ethers.parseUnits("100000", 6)); + + await enclToken + .connect(operator) + .approve(await bondingRegistry.getAddress(), ethers.parseEther("2000")); + await bondingRegistry + .connect(operator) + .bondLicense(ethers.parseEther("1000")); + await bondingRegistry.connect(operator).registerOperator(); + + const ticketTokenAddress = await bondingRegistry.ticketToken(); + const ticketAmount = ethers.parseUnits("100", 6); + await usdcToken + .connect(operator) + .approve(ticketTokenAddress, ticketAmount); + await bondingRegistry.connect(operator).addTicketBalance(ticketAmount); + } + + // Helper: make an E3 request + async function makeRequest(threshold: [number, number] = [2, 3]) { + const startTime = (await time.latest()) + 100; + const requestParams = { + threshold: threshold, + inputWindow: [startTime + 100, startTime + ONE_DAY] as [number, number], + e3Program: await e3Program.getAddress(), + e3ProgramParams: encodedE3ProgramParams, + computeProviderParams: abiCoder.encode( + ["address"], + [await decryptionVerifier.getAddress()], + ), + customParams: abiCoder.encode( + ["address"], + ["0x1234567890123456789012345678901234567890"], + ), + }; + + const fee = await enclave.getE3Quote(requestParams); + await usdcToken.connect(requester).approve(enclaveAddress, fee); + await enclave.connect(requester).request(requestParams); + } + + // Helper: finalize a committee after sortition + async function finalizeCommitteeWithOperators( + e3Id: number, + operators: Signer[], + ) { + for (const op of operators) { + await registry.connect(op).submitTicket(e3Id, 1); + } + await time.increase(SORTITION_SUBMISSION_WINDOW + 1); + await registry.finalizeCommittee(e3Id); + + // Publish the committee key so getCommitteeNodes works + const nodes = await Promise.all(operators.map((op) => op.getAddress())); + const publicKey = ethers.toUtf8Bytes("fake-public-key"); + const publicKeyHash = ethers.keccak256(publicKey); + await registry.publishCommittee(e3Id, nodes, publicKey, publicKeyHash); + } + + // Set up committee-affecting slash policy + // MockCircuitVerifier returns false by default → proof invalid → fault confirmed + const committeeSlashPolicy = { + ticketPenalty: ethers.parseUnits("10", 6), + licensePenalty: ethers.parseEther("50"), + requiresProof: true, + proofVerifier: await mockVerifier.getAddress(), + banNode: false, + appealWindow: 0, + enabled: true, + affectsCommittee: true, + failureReason: 4, // FailureReason.DKGInvalidShares + }; + await slashingManager.setSlashPolicy(REASON_BAD_DKG, committeeSlashPolicy); + + const decryptionSlashPolicy = { + ticketPenalty: ethers.parseUnits("10", 6), + licensePenalty: ethers.parseEther("50"), + requiresProof: true, + proofVerifier: await mockVerifier.getAddress(), + banNode: false, + appealWindow: 0, + enabled: true, + affectsCommittee: true, + failureReason: 11, // FailureReason.DecryptionInvalidShares + }; + await slashingManager.setSlashPolicy( + REASON_BAD_DECRYPTION, + decryptionSlashPolicy, + ); + + return { + enclave, + registry, + slashingManager, + bondingRegistry, + mockVerifier, + usdcToken, + enclToken, + owner, + requester, + treasury, + operator1, + operator2, + operator3, + operator4, + setupOperator, + makeRequest, + finalizeCommitteeWithOperators, + }; + }; + + describe("committee expulsion via proposeSlash", function () { + it("should expel a committee member and emit CommitteeMemberExpelled", async function () { + const { + registry, + slashingManager, + mockVerifier, + operator1, + operator2, + operator3, + setupOperator, + makeRequest, + finalizeCommitteeWithOperators, + } = await loadFixture(setup); + + await setupOperator(operator1); + await setupOperator(operator2); + await setupOperator(operator3); + + // threshold [2, 3] means M=2, N=3 + await makeRequest([2, 3]); + await finalizeCommitteeWithOperators(0, [ + operator1, + operator2, + operator3, + ]); + + const op1Address = await operator1.getAddress(); + + // Verify member is active before slash + expect(await registry.isCommitteeMemberActive(0, op1Address)).to.be.true; + expect(await registry.getActiveCommitteeCount(0)).to.equal(3); + + // Submit slash proposal — MockCircuitVerifier returns false by default + // so fault is confirmed and slash is auto-executed + const proof = await signAndEncodeProof( + operator1, + 0, + await mockVerifier.getAddress(), + ); + const tx = await slashingManager.proposeSlash( + 0, + op1Address, + REASON_BAD_DKG, + proof, + ); + + // Should emit CommitteeMemberExpelled + await expect(tx) + .to.emit(registry, "CommitteeMemberExpelled") + .withArgs(0, op1Address, REASON_BAD_DKG, 2); + + // Should emit CommitteeViabilityUpdated + await expect(tx) + .to.emit(registry, "CommitteeViabilityUpdated") + .withArgs(0, 2, 2, true); // 2 >= 2 → viable + + // Verify member is no longer active + expect(await registry.isCommitteeMemberActive(0, op1Address)).to.be.false; + expect(await registry.getActiveCommitteeCount(0)).to.equal(2); + }); + + it("should keep E3 alive when active members >= threshold", async function () { + const { + enclave, + registry, + slashingManager, + mockVerifier, + operator1, + operator2, + operator3, + setupOperator, + makeRequest, + finalizeCommitteeWithOperators, + } = await loadFixture(setup); + + await setupOperator(operator1); + await setupOperator(operator2); + await setupOperator(operator3); + + await makeRequest([2, 3]); // M=2, N=3 + await finalizeCommitteeWithOperators(0, [ + operator1, + operator2, + operator3, + ]); + + // Slash one member — 3 active → 2 active, threshold is 2, still viable + const proof = await signAndEncodeProof( + operator1, + 0, + await mockVerifier.getAddress(), + ); + await slashingManager.proposeSlash( + 0, + await operator1.getAddress(), + REASON_BAD_DKG, + proof, + ); + + // E3 should NOT be failed — stage should still be Requested (1) + // or whatever stage it was at, not Failed + const stage = await enclave.getE3Stage(0); + expect(stage).to.not.equal(6); // 6 = E3Stage.Failed + + // Active committee still has enough members + expect(await registry.getActiveCommitteeCount(0)).to.equal(2); + const threshold = await registry.getCommitteeThreshold(0); + expect(threshold[0]).to.equal(2); // M=2 + }); + + it("should fail E3 when active members drop below threshold", async function () { + const { + enclave, + slashingManager, + mockVerifier, + operator1, + operator2, + operator3, + setupOperator, + makeRequest, + finalizeCommitteeWithOperators, + } = await loadFixture(setup); + + await setupOperator(operator1); + await setupOperator(operator2); + await setupOperator(operator3); + + await makeRequest([2, 3]); // M=2, N=3 + await finalizeCommitteeWithOperators(0, [ + operator1, + operator2, + operator3, + ]); + + // Slash first member — 3 → 2 active, still >= 2 + const proof1 = await signAndEncodeProof( + operator1, + 0, + await mockVerifier.getAddress(), + "0x1111", + ); + await slashingManager.proposeSlash( + 0, + await operator1.getAddress(), + REASON_BAD_DKG, + proof1, + ); + + let stage = await enclave.getE3Stage(0); + expect(stage).to.not.equal(6); // Not failed yet + + // Slash second member — 2 → 1 active, below threshold M=2 + const proof2 = await signAndEncodeProof( + operator2, + 0, + await mockVerifier.getAddress(), + "0x2222", + ); + const tx = await slashingManager.proposeSlash( + 0, + await operator2.getAddress(), + REASON_BAD_DKG, + proof2, + ); + + // Should emit E3Failed event + await expect(tx).to.emit(enclave, "E3Failed"); + + // E3 should now be Failed + stage = await enclave.getE3Stage(0); + expect(stage).to.equal(6); // E3Stage.Failed + + // Failure reason should be DKGInvalidShares (4) + const reason = await enclave.getFailureReason(0); + expect(reason).to.equal(4); + }); + + it("should handle idempotent expulsion (re-slashing same node)", async function () { + const { + registry, + slashingManager, + mockVerifier, + operator1, + operator2, + operator3, + setupOperator, + makeRequest, + finalizeCommitteeWithOperators, + } = await loadFixture(setup); + + await setupOperator(operator1); + await setupOperator(operator2); + await setupOperator(operator3); + + await makeRequest([2, 3]); + await finalizeCommitteeWithOperators(0, [ + operator1, + operator2, + operator3, + ]); + + // Slash operator1 once + const proof1 = await signAndEncodeProof( + operator1, + 0, + await mockVerifier.getAddress(), + "0xaaaa", + ); + await slashingManager.proposeSlash( + 0, + await operator1.getAddress(), + REASON_BAD_DKG, + proof1, + ); + expect(await registry.getActiveCommitteeCount(0)).to.equal(2); + + // Slash operator1 again with different proof (different evidence key) + const proof2 = await signAndEncodeProof( + operator1, + 0, + await mockVerifier.getAddress(), + "0xbbbb", + ); + await slashingManager.proposeSlash( + 0, + await operator1.getAddress(), + REASON_BAD_DKG, + proof2, + ); + + // Active count should still be 2 (idempotent expulsion) + expect(await registry.getActiveCommitteeCount(0)).to.equal(2); + }); + + it("should exclude expelled members from getActiveCommitteeNodes", async function () { + const { + registry, + slashingManager, + mockVerifier, + operator1, + operator2, + operator3, + setupOperator, + makeRequest, + finalizeCommitteeWithOperators, + } = await loadFixture(setup); + + await setupOperator(operator1); + await setupOperator(operator2); + await setupOperator(operator3); + + await makeRequest([2, 3]); + await finalizeCommitteeWithOperators(0, [ + operator1, + operator2, + operator3, + ]); + + // Before expulsion: all 3 should be in active nodes + const nodesBefore = await registry.getActiveCommitteeNodes(0); + expect(nodesBefore.length).to.equal(3); + expect(nodesBefore).to.include(await operator1.getAddress()); + + // Expel operator1 + const proof = await signAndEncodeProof( + operator1, + 0, + await mockVerifier.getAddress(), + ); + await slashingManager.proposeSlash( + 0, + await operator1.getAddress(), + REASON_BAD_DKG, + proof, + ); + + // After expulsion: only 2 should be active + const nodesAfter = await registry.getActiveCommitteeNodes(0); + expect(nodesAfter.length).to.equal(2); + expect(nodesAfter).to.not.include(await operator1.getAddress()); + expect(nodesAfter).to.include(await operator2.getAddress()); + expect(nodesAfter).to.include(await operator3.getAddress()); + }); + }); + + describe("E3 continues above threshold", function () { + it("should allow multiple expulsions while staying above threshold", async function () { + const { + enclave, + registry, + slashingManager, + mockVerifier, + operator1, + operator2, + operator3, + operator4, + setupOperator, + makeRequest, + finalizeCommitteeWithOperators, + } = await loadFixture(setup); + + await setupOperator(operator1); + await setupOperator(operator2); + await setupOperator(operator3); + await setupOperator(operator4); + + await makeRequest([2, 4]); // M=2, N=4 + await finalizeCommitteeWithOperators(0, [ + operator1, + operator2, + operator3, + operator4, + ]); + + expect(await registry.getActiveCommitteeCount(0)).to.equal(4); + + // Expel 2 out of 4 — still have 2 >= M=2 + const proof1 = await signAndEncodeProof( + operator1, + 0, + await mockVerifier.getAddress(), + "0x1111", + ); + await slashingManager.proposeSlash( + 0, + await operator1.getAddress(), + REASON_BAD_DKG, + proof1, + ); + expect(await registry.getActiveCommitteeCount(0)).to.equal(3); + + const proof2 = await signAndEncodeProof( + operator2, + 0, + await mockVerifier.getAddress(), + "0x2222", + ); + await slashingManager.proposeSlash( + 0, + await operator2.getAddress(), + REASON_BAD_DKG, + proof2, + ); + expect(await registry.getActiveCommitteeCount(0)).to.equal(2); + + // E3 should NOT be failed + const stage = await enclave.getE3Stage(0); + expect(stage).to.not.equal(6); + }); + }); + + describe("E3 fails below threshold", function () { + it("should fail E3 exactly at the threshold breach", async function () { + const { + enclave, + registry, + slashingManager, + mockVerifier, + operator1, + operator2, + setupOperator, + makeRequest, + finalizeCommitteeWithOperators, + } = await loadFixture(setup); + + await setupOperator(operator1); + await setupOperator(operator2); + + await makeRequest([2, 2]); // M=2, N=2 — no room for error + await finalizeCommitteeWithOperators(0, [operator1, operator2]); + + // Expel one member: 2 → 1 < M=2 → E3 fails immediately + const proof = await signAndEncodeProof( + operator1, + 0, + await mockVerifier.getAddress(), + ); + const tx = await slashingManager.proposeSlash( + 0, + await operator1.getAddress(), + REASON_BAD_DKG, + proof, + ); + + await expect(tx).to.emit(enclave, "E3Failed"); + + // Should emit CommitteeViabilityUpdated(viable=false) + await expect(tx) + .to.emit(registry, "CommitteeViabilityUpdated") + .withArgs(0, 1, 2, false); + + const stage = await enclave.getE3Stage(0); + expect(stage).to.equal(6); // Failed + }); + + it("should not fail E3 twice on multiple sub-threshold expulsions", async function () { + const { + enclave, + slashingManager, + mockVerifier, + operator1, + operator2, + operator3, + setupOperator, + makeRequest, + finalizeCommitteeWithOperators, + } = await loadFixture(setup); + + await setupOperator(operator1); + await setupOperator(operator2); + await setupOperator(operator3); + + await makeRequest([2, 3]); // M=2, N=3 + await finalizeCommitteeWithOperators(0, [ + operator1, + operator2, + operator3, + ]); + + // Expel operator1 — still viable (2 >= 2) + const proof1 = await signAndEncodeProof( + operator1, + 0, + await mockVerifier.getAddress(), + "0x1111", + ); + await slashingManager.proposeSlash( + 0, + await operator1.getAddress(), + REASON_BAD_DKG, + proof1, + ); + + // Expel operator2 — now below threshold (1 < 2), E3 fails + const proof2 = await signAndEncodeProof( + operator2, + 0, + await mockVerifier.getAddress(), + "0x2222", + ); + await slashingManager.proposeSlash( + 0, + await operator2.getAddress(), + REASON_BAD_DKG, + proof2, + ); + + // E3 is now Failed + const stage = await enclave.getE3Stage(0); + expect(stage).to.equal(6); + + // Try to expel operator3 — E3 already failed, but onE3Failed is wrapped + // in try-catch so financial penalties are still applied + const proof3 = await signAndEncodeProof( + operator3, + 0, + await mockVerifier.getAddress(), + "0x3333", + ); + + // The third slash should succeed — penalties are applied even though E3 is already Failed. + // The onE3Failed call silently fails (try-catch) since E3 is already in Failed state. + await expect( + slashingManager.proposeSlash( + 0, + await operator3.getAddress(), + REASON_BAD_DKG, + proof3, + ), + ).to.emit(slashingManager, "SlashExecuted"); + + // E3 stage should still be Failed + const stageAfter = await enclave.getE3Stage(0); + expect(stageAfter).to.equal(6); + }); + }); + + describe("slash execution events", function () { + it("should emit SlashExecuted on proof-based committee slash", async function () { + const { + slashingManager, + mockVerifier, + operator1, + operator2, + operator3, + setupOperator, + makeRequest, + finalizeCommitteeWithOperators, + } = await loadFixture(setup); + + await setupOperator(operator1); + await setupOperator(operator2); + await setupOperator(operator3); + + await makeRequest([2, 3]); + await finalizeCommitteeWithOperators(0, [ + operator1, + operator2, + operator3, + ]); + + const proof = await signAndEncodeProof( + operator1, + 0, + await mockVerifier.getAddress(), + ); + const op1Addr = await operator1.getAddress(); + const tx = await slashingManager.proposeSlash( + 0, + op1Addr, + REASON_BAD_DKG, + proof, + ); + + await expect(tx).to.emit(slashingManager, "SlashExecuted").withArgs( + 0, // proposalId + 0, // e3Id + op1Addr, + REASON_BAD_DKG, + ethers.parseUnits("10", 6), // ticketPenalty + ethers.parseEther("50"), // licensePenalty + true, // executed + ); + }); + }); +}); diff --git a/packages/enclave-contracts/test/Slashing/SlashingManager.spec.ts b/packages/enclave-contracts/test/Slashing/SlashingManager.spec.ts index 20b29055e1..f96574724a 100644 --- a/packages/enclave-contracts/test/Slashing/SlashingManager.spec.ts +++ b/packages/enclave-contracts/test/Slashing/SlashingManager.spec.ts @@ -9,19 +9,21 @@ import { network } from "hardhat"; import BondingRegistryModule from "../../ignition/modules/bondingRegistry"; import EnclaveTicketTokenModule from "../../ignition/modules/enclaveTicketToken"; import EnclaveTokenModule from "../../ignition/modules/enclaveToken"; -import MockSlashingVerifierModule from "../../ignition/modules/mockSlashingVerifier"; +import MockCiphernodeRegistryModule from "../../ignition/modules/mockCiphernodeRegistry"; +import MockCircuitVerifierModule from "../../ignition/modules/mockSlashingVerifier"; import MockStableTokenModule from "../../ignition/modules/mockStableToken"; import SlashingManagerModule from "../../ignition/modules/slashingManager"; import { BondingRegistry__factory as BondingRegistryFactory, EnclaveTicketToken__factory as EnclaveTicketTokenFactory, EnclaveToken__factory as EnclaveTokenFactory, - MockSlashingVerifier__factory as MockSlashingVerifierFactory, + MockCiphernodeRegistry__factory as MockCiphernodeRegistryFactory, + MockCircuitVerifier__factory as MockCircuitVerifierFactory, MockUSDC__factory as MockUSDCFactory, SlashingManager__factory as SlashingManagerFactory, } from "../../types"; +import type { MockCircuitVerifier } from "../../types"; import type { SlashingManager } from "../../types/contracts/slashing/SlashingManager"; -import type { MockSlashingVerifier } from "../../types/contracts/test/MockSlashingVerifier"; const { ethers, networkHelpers, ignition } = await network.connect(); const { loadFixture, time } = networkHelpers; @@ -32,7 +34,6 @@ describe("SlashingManager", function () { const REASON_DOUBLE_SIGN = ethers.encodeBytes32String("doubleSign"); const SLASHER_ROLE = ethers.keccak256(ethers.toUtf8Bytes("SLASHER_ROLE")); - const VERIFIER_ROLE = ethers.keccak256(ethers.toUtf8Bytes("VERIFIER_ROLE")); const GOVERNANCE_ROLE = ethers.keccak256( ethers.toUtf8Bytes("GOVERNANCE_ROLE"), ); @@ -40,9 +41,76 @@ describe("SlashingManager", function () { const APPEAL_WINDOW = 7 * 24 * 60 * 60; + // Placeholder address for contracts not under test + const addressOne = "0x0000000000000000000000000000000000000001"; + + const abiCoder = ethers.AbiCoder.defaultAbiCoder(); + + // Must match the PROOF_PAYLOAD_TYPEHASH in SlashingManager.sol + const PROOF_PAYLOAD_TYPEHASH = ethers.keccak256( + ethers.toUtf8Bytes( + "ProofPayload(uint256 chainId,uint256 e3Id,uint256 proofType,bytes zkProof,bytes publicSignals)", + ), + ); + + /** + * Helper to create a signed proof evidence bundle. + * The operator signs the proof payload (matching Rust ProofPayload.digest()), + * then the evidence is encoded in the format expected by proposeSlash(). + * Returns abi.encode(zkProof, publicInputs, signature, chainId, proofType, verifier) + */ + async function signAndEncodeProof( + signer: any, + e3Id: number, + reason: string, + verifierAddress: string, + zkProof: string = "0x1234", + publicInputs: string[] = [ethers.ZeroHash], + chainId: number = 31337, // Hardhat default chain ID + proofType: number = 0, // T0PkBfv + ): Promise { + // Operator signs: keccak256(abi.encode(PROOF_PAYLOAD_TYPEHASH, chainId, e3Id, proofType, keccak256(zkProof), keccak256(publicSignals))) + const messageHash = ethers.keccak256( + abiCoder.encode( + ["bytes32", "uint256", "uint256", "uint256", "bytes32", "bytes32"], + [ + PROOF_PAYLOAD_TYPEHASH, + chainId, + e3Id, + proofType, + ethers.keccak256(zkProof), + ethers.keccak256( + ethers.solidityPacked(["bytes32[]"], [publicInputs]), + ), + ], + ), + ); + const signature = await signer.signMessage(ethers.getBytes(messageHash)); + // Evidence format: abi.encode(zkProof, publicInputs, signature, chainId, proofType, verifier) + return abiCoder.encode( + ["bytes", "bytes32[]", "bytes", "uint256", "uint256", "address"], + [zkProof, publicInputs, signature, chainId, proofType, verifierAddress], + ); + } + + /** + * Legacy helper for tests that check early failures (before abi.decode). + * This encodes a minimal 6-tuple with dummy values for basic validation tests. + */ + function encodeDummyProof( + zkProof: string = "0x1234", + publicInputs: string[] = [ethers.ZeroHash], + verifierAddress: string = ethers.ZeroAddress, + ): string { + return abiCoder.encode( + ["bytes", "bytes32[]", "bytes", "uint256", "uint256", "address"], + [zkProof, publicInputs, "0x00", 31337, 0, verifierAddress], + ); + } + async function setupPolicies( slashingManager: SlashingManager, - mockVerifier: MockSlashingVerifier, + mockVerifier: MockCircuitVerifier, ) { const proofPolicy = { ticketPenalty: ethers.parseUnits("50", 6), @@ -52,6 +120,8 @@ describe("SlashingManager", function () { banNode: false, appealWindow: 0, enabled: true, + affectsCommittee: false, + failureReason: 0, }; const evidencePolicy = { @@ -62,6 +132,8 @@ describe("SlashingManager", function () { banNode: false, appealWindow: APPEAL_WINDOW, enabled: true, + affectsCommittee: false, + failureReason: 0, }; const banPolicy = { @@ -72,6 +144,8 @@ describe("SlashingManager", function () { banNode: true, appealWindow: 0, enabled: true, + affectsCommittee: false, + failureReason: 0, }; await slashingManager.setSlashPolicy(REASON_MISBEHAVIOR, proofPolicy); @@ -80,7 +154,7 @@ describe("SlashingManager", function () { } async function setup() { - const [owner, slasher, verifier, operator, notTheOwner] = + const [owner, slasher, proposer, operator, notTheOwner] = await ethers.getSigners(); const ownerAddress = await owner.getAddress(); const operatorAddress = await operator.getAddress(); @@ -115,9 +189,16 @@ describe("SlashingManager", function () { ); const mockVerifierContract = await ignition.deploy( - MockSlashingVerifierModule, + MockCircuitVerifierModule, + ); + + const mockCiphernodeRegistryContract = await ignition.deploy( + MockCiphernodeRegistryModule, ); + const mockCiphernodeRegistryAddress = + await mockCiphernodeRegistryContract.mockCiphernodeRegistry.getAddress(); + const slashingManagerContract = await ignition.deploy( SlashingManagerModule, { @@ -125,6 +206,8 @@ describe("SlashingManager", function () { SlashingManager: { admin: ownerAddress, bondingRegistry: ownerAddress, + ciphernodeRegistry: mockCiphernodeRegistryAddress, + enclave: addressOne, }, }, }, @@ -162,8 +245,12 @@ describe("SlashingManager", function () { await ticketTokenContract.enclaveTicketToken.getAddress(), owner, ); - const mockVerifier = MockSlashingVerifierFactory.connect( - await mockVerifierContract.mockSlashingVerifier.getAddress(), + const mockVerifier = MockCircuitVerifierFactory.connect( + await mockVerifierContract.mockCircuitVerifier.getAddress(), + owner, + ); + const mockCiphernodeRegistry = MockCiphernodeRegistryFactory.connect( + mockCiphernodeRegistryAddress, owner, ); const slashingManager = SlashingManagerFactory.connect( @@ -192,12 +279,11 @@ describe("SlashingManager", function () { ); await slashingManager.addSlasher(await slasher.getAddress()); - await slashingManager.addVerifier(await verifier.getAddress()); return { owner, slasher, - verifier, + proposer, operator, operatorAddress, notTheOwner, @@ -207,6 +293,7 @@ describe("SlashingManager", function () { ticketToken, usdcToken, mockVerifier, + mockCiphernodeRegistry, }; } @@ -243,6 +330,8 @@ describe("SlashingManager", function () { SlashingManager: { admin: ethers.ZeroAddress, bondingRegistry: ethers.ZeroAddress, + ciphernodeRegistry: ethers.ZeroAddress, + enclave: ethers.ZeroAddress, }, }, }), @@ -251,7 +340,7 @@ describe("SlashingManager", function () { }); describe("setSlashPolicy()", function () { - it("should set a valid slash policy", async function () { + it("should set a valid proof-based slash policy", async function () { const { slashingManager, mockVerifier } = await loadFixture(setup); const policy = { @@ -262,6 +351,8 @@ describe("SlashingManager", function () { banNode: false, appealWindow: 0, enabled: true, + affectsCommittee: false, + failureReason: 0, }; await expect(slashingManager.setSlashPolicy(REASON_MISBEHAVIOR, policy)) @@ -276,7 +367,7 @@ describe("SlashingManager", function () { expect(storedPolicy.enabled).to.equal(policy.enabled); }); - it("should set a policy without proof requirement", async function () { + it("should set an evidence-based policy (no proof required)", async function () { const { slashingManager } = await loadFixture(setup); const policy = { @@ -287,6 +378,8 @@ describe("SlashingManager", function () { banNode: false, appealWindow: APPEAL_WINDOW, enabled: true, + affectsCommittee: false, + failureReason: 0, }; await expect(slashingManager.setSlashPolicy(REASON_INACTIVITY, policy)) @@ -305,6 +398,8 @@ describe("SlashingManager", function () { banNode: false, appealWindow: APPEAL_WINDOW, enabled: true, + affectsCommittee: false, + failureReason: 0, }; await expect( @@ -328,6 +423,8 @@ describe("SlashingManager", function () { banNode: false, appealWindow: APPEAL_WINDOW, enabled: true, + affectsCommittee: false, + failureReason: 0, }; await expect( @@ -346,6 +443,8 @@ describe("SlashingManager", function () { banNode: false, appealWindow: APPEAL_WINDOW, enabled: false, + affectsCommittee: false, + failureReason: 0, }; await expect( @@ -364,6 +463,8 @@ describe("SlashingManager", function () { banNode: false, appealWindow: APPEAL_WINDOW, enabled: true, + affectsCommittee: false, + failureReason: 0, }; await expect( @@ -382,6 +483,8 @@ describe("SlashingManager", function () { banNode: false, appealWindow: 0, enabled: true, + affectsCommittee: false, + failureReason: 0, }; await expect( @@ -400,6 +503,8 @@ describe("SlashingManager", function () { banNode: false, appealWindow: APPEAL_WINDOW, enabled: true, + affectsCommittee: false, + failureReason: 0, }; await expect( @@ -418,6 +523,8 @@ describe("SlashingManager", function () { banNode: false, appealWindow: 0, enabled: true, + affectsCommittee: false, + failureReason: 0, }; await expect( @@ -447,26 +554,6 @@ describe("SlashingManager", function () { ).to.be.false; }); - it("should add and remove verifier role", async function () { - const { slashingManager, notTheOwner } = await loadFixture(setup); - - await slashingManager.addVerifier(await notTheOwner.getAddress()); - expect( - await slashingManager.hasRole( - VERIFIER_ROLE, - await notTheOwner.getAddress(), - ), - ).to.be.true; - - await slashingManager.removeVerifier(await notTheOwner.getAddress()); - expect( - await slashingManager.hasRole( - VERIFIER_ROLE, - await notTheOwner.getAddress(), - ), - ).to.be.false; - }); - it("should revert if non-admin tries to add slasher", async function () { const { slashingManager, notTheOwner } = await loadFixture(setup); @@ -474,7 +561,10 @@ describe("SlashingManager", function () { slashingManager .connect(notTheOwner) .addSlasher(await notTheOwner.getAddress()), - ).to.be.revert(ethers); + ).to.be.revertedWithCustomError( + slashingManager, + "AccessControlUnauthorizedAccount", + ); }); it("should revert if zero address is added as slasher", async function () { @@ -486,129 +576,213 @@ describe("SlashingManager", function () { }); }); - describe("proposeSlash()", function () { - it("should propose slash with proof", async function () { - const { slashingManager, slasher, operatorAddress, mockVerifier } = - await loadFixture(setup); + describe("proposeSlash() — Lane A (proof-based, permissionless)", function () { + it("should propose and auto-execute slash with signed proof from operator", async function () { + const { + slashingManager, + proposer, + operator, + operatorAddress, + mockVerifier, + mockCiphernodeRegistry, + } = await loadFixture(setup); + // MockCircuitVerifier default returnValue=false → proof invalid → fault confirmed + const verifierAddress = await mockVerifier.getAddress(); const proofPolicy = { ticketPenalty: ethers.parseUnits("50", 6), licensePenalty: ethers.parseEther("100"), requiresProof: true, - proofVerifier: await mockVerifier.getAddress(), + proofVerifier: verifierAddress, banNode: false, appealWindow: 0, enabled: true, + affectsCommittee: false, + failureReason: 0, }; await slashingManager.setSlashPolicy(REASON_MISBEHAVIOR, proofPolicy); - const proof = ethers.toUtf8Bytes("Valid proof data"); - const currentTime = await time.latest(); + // Set up committee membership for operator + const e3Id = 0; + await mockCiphernodeRegistry.setCommitteeNodes(e3Id, [operatorAddress]); + + // Operator signs the bad proof + const proof = await signAndEncodeProof( + operator, + e3Id, + REASON_MISBEHAVIOR, + verifierAddress, + ); + // Anyone can submit the signed evidence (permissionless for Lane A) await expect( slashingManager - .connect(slasher) - .proposeSlash(operatorAddress, REASON_MISBEHAVIOR, proof), - ) - .to.emit(slashingManager, "SlashProposed") - .withArgs( - 0, - operatorAddress, - REASON_MISBEHAVIOR, - ethers.parseUnits("50", 6), - ethers.parseEther("100"), - currentTime + 1, - await slasher.getAddress(), - ); + .connect(proposer) + .proposeSlash(e3Id, operatorAddress, REASON_MISBEHAVIOR, proof), + ).to.emit(slashingManager, "SlashProposed"); + // Proof-based slashes auto-execute const proposal = await slashingManager.getSlashProposal(0); expect(proposal.operator).to.equal(operatorAddress); expect(proposal.reason).to.equal(REASON_MISBEHAVIOR); expect(proposal.proofVerified).to.be.true; - expect(proposal.proposer).to.equal(await slasher.getAddress()); + expect(proposal.executed).to.be.true; + expect(proposal.proposer).to.equal(await proposer.getAddress()); }); - it("should propose slash without proof (evidence-based)", async function () { - const { slashingManager, slasher, operatorAddress } = - await loadFixture(setup); + it("should revert if circuit verifier says proof is valid (no fault)", async function () { + const { + slashingManager, + proposer, + operator, + operatorAddress, + mockVerifier, + mockCiphernodeRegistry, + } = await loadFixture(setup); - const evidencePolicy = { - ticketPenalty: ethers.parseUnits("20", 6), - licensePenalty: ethers.parseEther("50"), - requiresProof: false, - proofVerifier: ethers.ZeroAddress, + const verifierAddress = await mockVerifier.getAddress(); + const proofPolicy = { + ticketPenalty: ethers.parseUnits("50", 6), + licensePenalty: ethers.parseEther("100"), + requiresProof: true, + proofVerifier: verifierAddress, banNode: false, - appealWindow: APPEAL_WINDOW, + appealWindow: 0, enabled: true, + affectsCommittee: false, + failureReason: 0, }; - await slashingManager.setSlashPolicy(REASON_INACTIVITY, evidencePolicy); + await slashingManager.setSlashPolicy(REASON_MISBEHAVIOR, proofPolicy); + + // Set mock verifier to return true → proof is valid → NOT a fault + await mockVerifier.setReturnValue(true); - const proof = ethers.toUtf8Bytes(""); - const currentTime = await time.latest(); + await mockCiphernodeRegistry.setCommitteeNodes(0, [operatorAddress]); + const proof = await signAndEncodeProof( + operator, + 0, + REASON_MISBEHAVIOR, + verifierAddress, + ); await expect( slashingManager - .connect(slasher) - .proposeSlash(operatorAddress, REASON_INACTIVITY, proof), - ) - .to.emit(slashingManager, "SlashProposed") - .withArgs( - 0, - operatorAddress, - REASON_INACTIVITY, - ethers.parseUnits("20", 6), - ethers.parseEther("50"), - currentTime + APPEAL_WINDOW + 1, - await slasher.getAddress(), - ); + .connect(proposer) + .proposeSlash(0, operatorAddress, REASON_MISBEHAVIOR, proof), + ).to.be.revertedWithCustomError(slashingManager, "ProofIsValid"); + }); - const proposal = await slashingManager.getSlashProposal(0); - expect(proposal.proofVerified).to.be.false; - expect(proposal.executableAt).to.be.greaterThan( - currentTime + APPEAL_WINDOW, + it("should revert if signer is not the operator (V-001 fix)", async function () { + const { + slashingManager, + proposer, + operatorAddress, + mockVerifier, + mockCiphernodeRegistry, + } = await loadFixture(setup); + + const verifierAddress = await mockVerifier.getAddress(); + const proofPolicy = { + ticketPenalty: ethers.parseUnits("50", 6), + licensePenalty: ethers.parseEther("100"), + requiresProof: true, + proofVerifier: verifierAddress, + banNode: false, + appealWindow: 0, + enabled: true, + affectsCommittee: false, + failureReason: 0, + }; + await slashingManager.setSlashPolicy(REASON_MISBEHAVIOR, proofPolicy); + + await mockCiphernodeRegistry.setCommitteeNodes(0, [operatorAddress]); + + // Proposer signs the proof (NOT the operator) — should be rejected + const proof = await signAndEncodeProof( + proposer, + 0, + REASON_MISBEHAVIOR, + verifierAddress, ); + await expect( + slashingManager + .connect(proposer) + .proposeSlash(0, operatorAddress, REASON_MISBEHAVIOR, proof), + ).to.be.revertedWithCustomError(slashingManager, "SignerIsNotOperator"); }); - it("should revert if caller is not slasher", async function () { - const { slashingManager, notTheOwner, operatorAddress } = - await loadFixture(setup); + it("should revert if operator is not in committee (V-001 fix)", async function () { + const { + slashingManager, + proposer, + operator, + operatorAddress, + mockVerifier, + } = await loadFixture(setup); + + const verifierAddress = await mockVerifier.getAddress(); + const proofPolicy = { + ticketPenalty: ethers.parseUnits("50", 6), + licensePenalty: ethers.parseEther("100"), + requiresProof: true, + proofVerifier: verifierAddress, + banNode: false, + appealWindow: 0, + enabled: true, + affectsCommittee: false, + failureReason: 0, + }; + await slashingManager.setSlashPolicy(REASON_MISBEHAVIOR, proofPolicy); - const proof = ethers.toUtf8Bytes("Some proof"); + // Do NOT add operator to committee — empty committee for this E3 + const proof = await signAndEncodeProof( + operator, + 0, + REASON_MISBEHAVIOR, + verifierAddress, + ); await expect( slashingManager - .connect(notTheOwner) - .proposeSlash(operatorAddress, REASON_MISBEHAVIOR, proof), - ).to.be.revertedWithCustomError(slashingManager, "Unauthorized"); + .connect(proposer) + .proposeSlash(0, operatorAddress, REASON_MISBEHAVIOR, proof), + ).to.be.revertedWithCustomError( + slashingManager, + "OperatorNotInCommittee", + ); }); it("should revert if operator is zero address", async function () { - const { slashingManager, slasher } = await loadFixture(setup); + const { slashingManager, proposer, mockVerifier } = + await loadFixture(setup); + + await setupPolicies(slashingManager, mockVerifier); - const proof = ethers.toUtf8Bytes("Some proof"); + // Any non-empty proof triggers ZeroAddress check before decode + const proof = encodeDummyProof(); await expect( slashingManager - .connect(slasher) - .proposeSlash(ethers.ZeroAddress, REASON_MISBEHAVIOR, proof), + .connect(proposer) + .proposeSlash(0, ethers.ZeroAddress, REASON_MISBEHAVIOR, proof), ).to.be.revertedWithCustomError(slashingManager, "ZeroAddress"); }); it("should revert if slash reason is disabled", async function () { - const { slashingManager, slasher, operatorAddress } = + const { slashingManager, proposer, operatorAddress } = await loadFixture(setup); - const proof = ethers.toUtf8Bytes("Some proof"); + const proof = encodeDummyProof(); await expect( slashingManager - .connect(slasher) - .proposeSlash(operatorAddress, REASON_DOUBLE_SIGN, proof), + .connect(proposer) + .proposeSlash(0, operatorAddress, REASON_DOUBLE_SIGN, proof), ).to.be.revertedWithCustomError(slashingManager, "SlashReasonDisabled"); }); - it("should revert if proof required but not provided", async function () { - const { slashingManager, slasher, operatorAddress, mockVerifier } = + it("should revert if proof is empty", async function () { + const { slashingManager, proposer, operatorAddress, mockVerifier } = await loadFixture(setup); const proofPolicy = { @@ -619,101 +793,210 @@ describe("SlashingManager", function () { banNode: false, appealWindow: 0, enabled: true, + affectsCommittee: false, + failureReason: 0, }; await slashingManager.setSlashPolicy(REASON_MISBEHAVIOR, proofPolicy); - const emptyProof = ethers.toUtf8Bytes(""); - await expect( slashingManager - .connect(slasher) - .proposeSlash(operatorAddress, REASON_MISBEHAVIOR, emptyProof), + .connect(proposer) + .proposeSlash(0, operatorAddress, REASON_MISBEHAVIOR, "0x"), ).to.be.revertedWithCustomError(slashingManager, "ProofRequired"); }); - it("should increment totalProposals", async function () { - const { slashingManager, slasher, operatorAddress, mockVerifier } = - await loadFixture(setup); + it("should reject duplicate evidence", async function () { + const { + slashingManager, + proposer, + operator, + operatorAddress, + mockVerifier, + mockCiphernodeRegistry, + } = await loadFixture(setup); + const verifierAddress = await mockVerifier.getAddress(); const proofPolicy = { ticketPenalty: ethers.parseUnits("50", 6), licensePenalty: ethers.parseEther("100"), requiresProof: true, - proofVerifier: await mockVerifier.getAddress(), + proofVerifier: verifierAddress, banNode: false, appealWindow: 0, enabled: true, - }; - const evidencePolicy = { - ticketPenalty: ethers.parseUnits("20", 6), - licensePenalty: ethers.parseEther("50"), - requiresProof: false, - proofVerifier: ethers.ZeroAddress, - banNode: false, - appealWindow: APPEAL_WINDOW, - enabled: true, + affectsCommittee: false, + failureReason: 0, }; await slashingManager.setSlashPolicy(REASON_MISBEHAVIOR, proofPolicy); - await slashingManager.setSlashPolicy(REASON_INACTIVITY, evidencePolicy); + await mockCiphernodeRegistry.setCommitteeNodes(0, [operatorAddress]); + + const proof = await signAndEncodeProof( + operator, + 0, + REASON_MISBEHAVIOR, + verifierAddress, + ); + await slashingManager + .connect(proposer) + .proposeSlash(0, operatorAddress, REASON_MISBEHAVIOR, proof); + + // Same proof for same e3Id/operator/reason should be rejected + await expect( + slashingManager + .connect(proposer) + .proposeSlash(0, operatorAddress, REASON_MISBEHAVIOR, proof), + ).to.be.revertedWithCustomError(slashingManager, "DuplicateEvidence"); + }); + + it("should increment totalProposals", async function () { + const { + slashingManager, + proposer, + operator, + operatorAddress, + mockVerifier, + mockCiphernodeRegistry, + } = await loadFixture(setup); + + await setupPolicies(slashingManager, mockVerifier); + + const verifierAddress = await mockVerifier.getAddress(); + await mockCiphernodeRegistry.setCommitteeNodes(0, [operatorAddress]); + await mockCiphernodeRegistry.setCommitteeNodes(1, [operatorAddress]); expect(await slashingManager.totalProposals()).to.equal(0); - const proof = ethers.toUtf8Bytes("Valid proof"); + const proof1 = await signAndEncodeProof( + operator, + 0, + REASON_MISBEHAVIOR, + verifierAddress, + "0x1111", + ); await slashingManager - .connect(slasher) - .proposeSlash(operatorAddress, REASON_MISBEHAVIOR, proof); + .connect(proposer) + .proposeSlash(0, operatorAddress, REASON_MISBEHAVIOR, proof1); expect(await slashingManager.totalProposals()).to.equal(1); + const proof2 = await signAndEncodeProof( + operator, + 1, + REASON_MISBEHAVIOR, + verifierAddress, + "0x2222", + ); await slashingManager - .connect(slasher) - .proposeSlash( - operatorAddress, - REASON_INACTIVITY, - ethers.toUtf8Bytes(""), - ); + .connect(proposer) + .proposeSlash(1, operatorAddress, REASON_MISBEHAVIOR, proof2); expect(await slashingManager.totalProposals()).to.equal(2); }); + + it("should ban node when policy requires it", async function () { + const { + slashingManager, + proposer, + operator, + operatorAddress, + mockVerifier, + mockCiphernodeRegistry, + } = await loadFixture(setup); + + await setupPolicies(slashingManager, mockVerifier); + + const verifierAddress = await mockVerifier.getAddress(); + await mockCiphernodeRegistry.setCommitteeNodes(0, [operatorAddress]); + + expect(await slashingManager.isBanned(operatorAddress)).to.be.false; + + const proof = await signAndEncodeProof( + operator, + 0, + REASON_DOUBLE_SIGN, + verifierAddress, + "0x3333", + ); + await slashingManager + .connect(proposer) + .proposeSlash(0, operatorAddress, REASON_DOUBLE_SIGN, proof); + + // banNode=true → auto-executed → node is now banned + expect(await slashingManager.isBanned(operatorAddress)).to.be.true; + }); }); - describe("executeSlash()", function () { - it("should execute slash with proof immediately", async function () { + describe("proposeSlashEvidence() — Lane B (evidence-based, SLASHER_ROLE)", function () { + it("should propose evidence-based slash with appeal window", async function () { const { slashingManager, slasher, operatorAddress, mockVerifier } = await loadFixture(setup); - const proofPolicy = { - ticketPenalty: ethers.parseUnits("50", 6), - licensePenalty: ethers.parseEther("100"), - requiresProof: true, - proofVerifier: await mockVerifier.getAddress(), - banNode: false, - appealWindow: 0, - enabled: true, - }; - await slashingManager.setSlashPolicy(REASON_MISBEHAVIOR, proofPolicy); + await setupPolicies(slashingManager, mockVerifier); - const proof = ethers.toUtf8Bytes("Valid proof"); - await slashingManager - .connect(slasher) - .proposeSlash(operatorAddress, REASON_MISBEHAVIOR, proof); + const evidence = ethers.toUtf8Bytes("operator was inactive during E3"); + const e3Id = 0; - await expect(slashingManager.connect(slasher).executeSlash(0)) - .to.emit(slashingManager, "SlashExecuted") - .withArgs( - 0, - operatorAddress, - REASON_MISBEHAVIOR, - ethers.parseUnits("50", 6), - ethers.parseEther("100"), - true, - ); + await expect( + slashingManager + .connect(slasher) + .proposeSlashEvidence( + e3Id, + operatorAddress, + REASON_INACTIVITY, + evidence, + ), + ).to.emit(slashingManager, "SlashProposed"); const proposal = await slashingManager.getSlashProposal(0); - expect(proposal.executed).to.be.true; + expect(proposal.operator).to.equal(operatorAddress); + expect(proposal.reason).to.equal(REASON_INACTIVITY); + expect(proposal.proofVerified).to.be.false; + expect(proposal.executed).to.be.false; + expect(proposal.proposer).to.equal(await slasher.getAddress()); + expect(proposal.executableAt).to.be.gt(proposal.proposedAt); + }); + + it("should revert if caller is not slasher", async function () { + const { slashingManager, notTheOwner, operatorAddress, mockVerifier } = + await loadFixture(setup); + + await setupPolicies(slashingManager, mockVerifier); + + const evidence = ethers.toUtf8Bytes("evidence"); + + await expect( + slashingManager + .connect(notTheOwner) + .proposeSlashEvidence( + 0, + operatorAddress, + REASON_INACTIVITY, + evidence, + ), + ).to.be.revertedWithCustomError(slashingManager, "Unauthorized"); }); - it("should execute slash after appeal window expires", async function () { + it("should revert if operator is zero address", async function () { + const { slashingManager, slasher, mockVerifier } = + await loadFixture(setup); + + await setupPolicies(slashingManager, mockVerifier); + + await expect( + slashingManager + .connect(slasher) + .proposeSlashEvidence( + 0, + ethers.ZeroAddress, + REASON_INACTIVITY, + ethers.toUtf8Bytes(""), + ), + ).to.be.revertedWithCustomError(slashingManager, "ZeroAddress"); + }); + }); + + describe("executeSlash() — Lane B execution", function () { + it("should execute evidence-based slash after appeal window", async function () { const { slashingManager, slasher, operatorAddress, mockVerifier } = await loadFixture(setup); @@ -721,49 +1004,66 @@ describe("SlashingManager", function () { await slashingManager .connect(slasher) - .proposeSlash( + .proposeSlashEvidence( + 0, operatorAddress, REASON_INACTIVITY, - ethers.toUtf8Bytes(""), + ethers.toUtf8Bytes("evidence"), ); + // Should revert before appeal window expires await expect( - slashingManager.connect(slasher).executeSlash(0), + slashingManager.executeSlash(0), ).to.be.revertedWithCustomError(slashingManager, "AppealWindowActive"); + // Fast forward past appeal window await time.increase(APPEAL_WINDOW + 1); - await expect(slashingManager.connect(slasher).executeSlash(0)).to.emit( + await expect(slashingManager.executeSlash(0)).to.emit( slashingManager, "SlashExecuted", ); + + const proposal = await slashingManager.getSlashProposal(0); + expect(proposal.executed).to.be.true; }); - it("should ban node when policy requires it", async function () { - const { slashingManager, slasher, operatorAddress, mockVerifier } = - await loadFixture(setup); + it("should revert if proof-based slash tries to executeSlash separately", async function () { + const { + slashingManager, + proposer, + operator, + operatorAddress, + mockVerifier, + mockCiphernodeRegistry, + } = await loadFixture(setup); await setupPolicies(slashingManager, mockVerifier); + const verifierAddress = await mockVerifier.getAddress(); + await mockCiphernodeRegistry.setCommitteeNodes(0, [operatorAddress]); - const proof = ethers.toUtf8Bytes("Serious violation proof"); + // Proof-based slash auto-executes in proposeSlash + const proof = await signAndEncodeProof( + operator, + 0, + REASON_MISBEHAVIOR, + verifierAddress, + ); await slashingManager - .connect(slasher) - .proposeSlash(operatorAddress, REASON_DOUBLE_SIGN, proof); - - expect(await slashingManager.isBanned(operatorAddress)).to.be.false; + .connect(proposer) + .proposeSlash(0, operatorAddress, REASON_MISBEHAVIOR, proof); - await expect(slashingManager.connect(slasher).executeSlash(0)) - .to.emit(slashingManager, "NodeBanUpdated") - .withArgs(operatorAddress, true, REASON_DOUBLE_SIGN, slasher); - - expect(await slashingManager.isBanned(operatorAddress)).to.be.true; + // Should revert because already executed + await expect( + slashingManager.executeSlash(0), + ).to.be.revertedWithCustomError(slashingManager, "AlreadyExecuted"); }); it("should revert if proposal doesn't exist", async function () { - const { slashingManager, slasher } = await loadFixture(setup); + const { slashingManager } = await loadFixture(setup); await expect( - slashingManager.connect(slasher).executeSlash(999), + slashingManager.executeSlash(999), ).to.be.revertedWithCustomError(slashingManager, "InvalidProposal"); }); @@ -773,20 +1073,26 @@ describe("SlashingManager", function () { await setupPolicies(slashingManager, mockVerifier); - const proof = ethers.toUtf8Bytes("Valid proof"); await slashingManager .connect(slasher) - .proposeSlash(operatorAddress, REASON_MISBEHAVIOR, proof); - await slashingManager.connect(slasher).executeSlash(0); + .proposeSlashEvidence( + 0, + operatorAddress, + REASON_INACTIVITY, + ethers.toUtf8Bytes("evidence"), + ); + + await time.increase(APPEAL_WINDOW + 1); + await slashingManager.executeSlash(0); await expect( - slashingManager.connect(slasher).executeSlash(0), + slashingManager.executeSlash(0), ).to.be.revertedWithCustomError(slashingManager, "AlreadyExecuted"); }); }); describe("appeal system", function () { - it("should allow operator to file appeal", async function () { + it("should allow operator to file appeal on evidence-based slash", async function () { const { slashingManager, slasher, @@ -799,10 +1105,11 @@ describe("SlashingManager", function () { await slashingManager .connect(slasher) - .proposeSlash( + .proposeSlashEvidence( + 0, operatorAddress, REASON_INACTIVITY, - ethers.toUtf8Bytes(""), + ethers.toUtf8Bytes("evidence"), ); const evidence = "I was not inactive, here's the proof..."; @@ -828,10 +1135,11 @@ describe("SlashingManager", function () { await slashingManager .connect(slasher) - .proposeSlash( + .proposeSlashEvidence( + 0, operatorAddress, REASON_INACTIVITY, - ethers.toUtf8Bytes(""), + ethers.toUtf8Bytes("evidence"), ); await expect( @@ -852,10 +1160,11 @@ describe("SlashingManager", function () { await slashingManager .connect(slasher) - .proposeSlash( + .proposeSlashEvidence( + 0, operatorAddress, REASON_INACTIVITY, - ethers.toUtf8Bytes(""), + ethers.toUtf8Bytes("evidence"), ); await time.increase(APPEAL_WINDOW + 1); @@ -878,10 +1187,11 @@ describe("SlashingManager", function () { await slashingManager .connect(slasher) - .proposeSlash( + .proposeSlashEvidence( + 0, operatorAddress, REASON_INACTIVITY, - ethers.toUtf8Bytes(""), + ethers.toUtf8Bytes("evidence"), ); await slashingManager.connect(operator).fileAppeal(0, "First appeal"); @@ -891,6 +1201,37 @@ describe("SlashingManager", function () { ).to.be.revertedWithCustomError(slashingManager, "AlreadyAppealed"); }); + it("should revert if appealing proof-verified slash", async function () { + const { + slashingManager, + proposer, + operator, + operatorAddress, + mockVerifier, + mockCiphernodeRegistry, + } = await loadFixture(setup); + + await setupPolicies(slashingManager, mockVerifier); + const verifierAddress = await mockVerifier.getAddress(); + await mockCiphernodeRegistry.setCommitteeNodes(0, [operatorAddress]); + + // Proof-based slash auto-executes with proofVerified=true + const proof = await signAndEncodeProof( + operator, + 0, + REASON_MISBEHAVIOR, + verifierAddress, + ); + await slashingManager + .connect(proposer) + .proposeSlash(0, operatorAddress, REASON_MISBEHAVIOR, proof); + + // Cannot appeal proof-verified slashes — appeal window is 0 so it's already expired + await expect( + slashingManager.connect(operator).fileAppeal(0, "Cannot appeal proof"), + ).to.be.revertedWithCustomError(slashingManager, "AppealWindowExpired"); + }); + it("should allow governance to resolve appeal (approve)", async function () { const { slashingManager, @@ -905,10 +1246,11 @@ describe("SlashingManager", function () { await slashingManager .connect(slasher) - .proposeSlash( + .proposeSlashEvidence( + 0, operatorAddress, REASON_INACTIVITY, - ethers.toUtf8Bytes(""), + ethers.toUtf8Bytes("evidence"), ); await slashingManager.connect(operator).fileAppeal(0, "Evidence"); @@ -945,10 +1287,11 @@ describe("SlashingManager", function () { await slashingManager .connect(slasher) - .proposeSlash( + .proposeSlashEvidence( + 0, operatorAddress, REASON_INACTIVITY, - ethers.toUtf8Bytes(""), + ethers.toUtf8Bytes("evidence"), ); await slashingManager.connect(operator).fileAppeal(0, "Evidence"); @@ -974,17 +1317,18 @@ describe("SlashingManager", function () { await slashingManager .connect(slasher) - .proposeSlash( + .proposeSlashEvidence( + 0, operatorAddress, REASON_INACTIVITY, - ethers.toUtf8Bytes(""), + ethers.toUtf8Bytes("evidence"), ); await slashingManager.connect(operator).fileAppeal(0, "Evidence"); await time.increase(APPEAL_WINDOW + 1); await expect( - slashingManager.connect(slasher).executeSlash(0), + slashingManager.executeSlash(0), ).to.be.revertedWithCustomError(slashingManager, "AppealPending"); }); @@ -1002,10 +1346,11 @@ describe("SlashingManager", function () { await slashingManager .connect(slasher) - .proposeSlash( + .proposeSlashEvidence( + 0, operatorAddress, REASON_INACTIVITY, - ethers.toUtf8Bytes(""), + ethers.toUtf8Bytes("evidence"), ); await slashingManager.connect(operator).fileAppeal(0, "Evidence"); await slashingManager.connect(owner).resolveAppeal(0, true, "Approved"); @@ -1013,7 +1358,7 @@ describe("SlashingManager", function () { await time.increase(APPEAL_WINDOW + 1); await expect( - slashingManager.connect(slasher).executeSlash(0), + slashingManager.executeSlash(0), ).to.be.revertedWithCustomError(slashingManager, "AppealUpheld"); }); @@ -1031,17 +1376,18 @@ describe("SlashingManager", function () { await slashingManager .connect(slasher) - .proposeSlash( + .proposeSlashEvidence( + 0, operatorAddress, REASON_INACTIVITY, - ethers.toUtf8Bytes(""), + ethers.toUtf8Bytes("evidence"), ); await slashingManager.connect(operator).fileAppeal(0, "Evidence"); await slashingManager.connect(owner).resolveAppeal(0, false, "Denied"); await time.increase(APPEAL_WINDOW + 1); - await expect(slashingManager.connect(slasher).executeSlash(0)).to.emit( + await expect(slashingManager.executeSlash(0)).to.emit( slashingManager, "SlashExecuted", ); @@ -1127,6 +1473,8 @@ describe("SlashingManager", function () { banNode: true, appealWindow: 0, enabled: true, + affectsCommittee: false, + failureReason: 0, }; await slashingManager.setSlashPolicy(REASON_MISBEHAVIOR, policy); @@ -1140,27 +1488,44 @@ describe("SlashingManager", function () { expect(retrieved.banNode).to.equal(policy.banNode); expect(retrieved.appealWindow).to.equal(policy.appealWindow); expect(retrieved.enabled).to.equal(policy.enabled); + expect(retrieved.affectsCommittee).to.equal(policy.affectsCommittee); + expect(retrieved.failureReason).to.equal(policy.failureReason); }); it("should return correct slash proposal", async function () { - const { slashingManager, slasher, operatorAddress, mockVerifier } = - await loadFixture(setup); + const { + slashingManager, + proposer, + operator, + operatorAddress, + mockVerifier, + mockCiphernodeRegistry, + } = await loadFixture(setup); await setupPolicies(slashingManager, mockVerifier); + const verifierAddress = await mockVerifier.getAddress(); + await mockCiphernodeRegistry.setCommitteeNodes(0, [operatorAddress]); - const proof = ethers.toUtf8Bytes("test proof"); + const proof = await signAndEncodeProof( + operator, + 0, + REASON_MISBEHAVIOR, + verifierAddress, + "0x4444", + ); await slashingManager - .connect(slasher) - .proposeSlash(operatorAddress, REASON_MISBEHAVIOR, proof); + .connect(proposer) + .proposeSlash(0, operatorAddress, REASON_MISBEHAVIOR, proof); const proposal = await slashingManager.getSlashProposal(0); expect(proposal.operator).to.equal(operatorAddress); expect(proposal.reason).to.equal(REASON_MISBEHAVIOR); expect(proposal.ticketAmount).to.equal(ethers.parseUnits("50", 6)); expect(proposal.licenseAmount).to.equal(ethers.parseEther("100")); - expect(proposal.proposer).to.equal(await slasher.getAddress()); + expect(proposal.proposer).to.equal(await proposer.getAddress()); expect(proposal.proofHash).to.equal(ethers.keccak256(proof)); expect(proposal.proofVerified).to.be.true; + expect(proposal.executed).to.be.true; }); it("should revert for invalid proposal ID", async function () { diff --git a/templates/default/client/src/context/WizardContext.tsx b/templates/default/client/src/context/WizardContext.tsx index 2a239998ec..32c6fbbd21 100644 --- a/templates/default/client/src/context/WizardContext.tsx +++ b/templates/default/client/src/context/WizardContext.tsx @@ -13,6 +13,7 @@ import { getEnclaveSDKConfig } from '@/utils/sdk-config' // TYPES & ENUMS // ============================================================================ +// eslint-disable-next-line react-refresh/only-export-components export enum WizardStep { CONNECT_WALLET = 1, REQUEST_COMPUTATION = 2, @@ -61,6 +62,7 @@ interface WizardContextType { const WizardContext = createContext(undefined) +// eslint-disable-next-line react-refresh/only-export-components export const useWizard = () => { const context = useContext(WizardContext) if (!context) { @@ -104,6 +106,7 @@ export const WizardProvider: React.FC = ({ children }) => { }) // Auto-advance steps based on state. + /* eslint-disable react-hooks/set-state-in-effect */ useEffect(() => { if (!isConnected) { setCurrentStep(WizardStep.CONNECT_WALLET) @@ -111,6 +114,7 @@ export const WizardProvider: React.FC = ({ children }) => { setCurrentStep(WizardStep.REQUEST_COMPUTATION) } }, [isConnected, sdk.isInitialized, currentStep]) + /* eslint-enable react-hooks/set-state-in-effect */ const handleReset = useCallback(() => { setCurrentStep(WizardStep.CONNECT_WALLET) diff --git a/templates/default/client/src/pages/steps/RequestComputation.tsx b/templates/default/client/src/pages/steps/RequestComputation.tsx index 2b06cbc524..efbb0015a2 100644 --- a/templates/default/client/src/pages/steps/RequestComputation.tsx +++ b/templates/default/client/src/pages/steps/RequestComputation.tsx @@ -75,7 +75,7 @@ const RequestComputation: React.FC = () => { off(EnclaveEventType.E3_REQUESTED, handleE3Requested) off(RegistryEventType.COMMITTEE_PUBLISHED, handleCommitteePublished) } - }, [isInitialized, onEnclaveEvent, off, EnclaveEventType, RegistryEventType]) + }, [isInitialized, onEnclaveEvent, off, EnclaveEventType, RegistryEventType, setE3State]) // Auto-advance to next step when committee publishes useEffect(() => { diff --git a/templates/default/enclave.config.yaml b/templates/default/enclave.config.yaml index aaa84e54db..16d03bf0f6 100644 --- a/templates/default/enclave.config.yaml +++ b/templates/default/enclave.config.yaml @@ -3,7 +3,7 @@ chains: rpc_url: "ws://localhost:8545" contracts: e3_program: - address: "0xc3e53F4d16Ae77Db1c982e75a937B9f60FE63690" + address: "0xa82fF9aFd8f496c3d6ac40E2a0F282E47488CFc9" deploy_block: 1 # Set to actual deploy block enclave: address: "0xA51c1fc2f0D1a1b8494Ed1FE312d7C3a78Ed91C0" @@ -14,6 +14,9 @@ chains: bonding_registry: address: "0x2279B7A0a67DB372996a5FaB50D91eAA73d2eBe6" deploy_block: 1 # Set to actual deploy block + slashing_manager: + address: '0x0165878A594ca255338adfa4d48449f69242Eb8F' + deploy_block: 8 fee_token: address: "0x9fE46736679d2D9a65F0992F2272dE9f3c7fa6e0" deploy_block: 1 # Set to actual deploy block