diff --git a/.github/workflows/releases.yml b/.github/workflows/releases.yml index 09818371e3..8acf80a32b 100644 --- a/.github/workflows/releases.yml +++ b/.github/workflows/releases.yml @@ -403,8 +403,7 @@ jobs: download-circuits, build-nix-flake, ] - if: - always() && needs.validate-and-prepare.result == 'success' && needs.build-binaries.result == 'success' + if: always() && needs.validate-and-prepare.result == 'success' && needs.build-binaries.result == 'success' steps: - name: Checkout uses: actions/checkout@v6 diff --git a/crates/evm/src/actors/ciphernode_registry_sol.rs b/crates/evm/src/actors/ciphernode_registry_sol.rs index 523e84f683..aff0267893 100644 --- a/crates/evm/src/actors/ciphernode_registry_sol.rs +++ b/crates/evm/src/actors/ciphernode_registry_sol.rs @@ -5,6 +5,7 @@ // or FITNESS FOR A PARTICULAR PURPOSE. use crate::actors::evm_parser::EvmParser; +use crate::contracts::ICiphernodeRegistry; use crate::domain::ciphernode_registry_events::extractor; use crate::domain::error_decoder::{decode_error_from_str, format_evm_error}; use crate::helpers::{encode_zk_proof, send_tx_with_retry, EthProvider}; @@ -14,7 +15,6 @@ use alloy::{ primitives::{Address, Bytes, B256, U256}, providers::{Provider, WalletProvider}, rpc::types::TransactionReceipt, - sol, }; use anyhow::Result; use e3_events::{ @@ -26,13 +26,6 @@ use e3_utils::{ArcBytes, NotifySync, MAILBOX_LIMIT}; use std::collections::{HashMap, HashSet}; use tracing::{error, info}; -sol!( - #[sol(rpc)] - #[derive(Debug)] - ICiphernodeRegistry, - "../../packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json" -); - /// Connects to CiphernodeRegistry.sol converting EVM events to EnclaveEvents pub struct CiphernodeRegistrySolReader; @@ -539,14 +532,7 @@ pub async fn fetch_dkg_fold_attestation_verifier( provider: &P, registry_address: Address, ) -> Result> { - sol! { - #[sol(rpc)] - interface ICiphernodeRegistryDkgFoldView { - function dkgFoldAttestationVerifier() external view returns (address); - } - } - - let contract = ICiphernodeRegistryDkgFoldView::new(registry_address, provider); + let contract = ICiphernodeRegistry::new(registry_address, provider); let verifier = contract.dkgFoldAttestationVerifier().call().await?; if verifier == Address::ZERO { Ok(None) @@ -566,14 +552,7 @@ pub async fn fetch_accusation_vote_validity( provider: &P, registry_address: Address, ) -> Result> { - sol! { - #[sol(rpc)] - interface ICiphernodeRegistryAccusationVoteView { - function accusationVoteValidity() external view returns (uint256); - } - } - - let contract = ICiphernodeRegistryAccusationVoteView::new(registry_address, provider); + let contract = ICiphernodeRegistry::new(registry_address, provider); let validity = contract.accusationVoteValidity().call().await?; if validity.is_zero() { Ok(None) diff --git a/crates/evm/src/actors/enclave_sol_writer.rs b/crates/evm/src/actors/enclave_sol_writer.rs index 00e10c597b..e401fac66c 100644 --- a/crates/evm/src/actors/enclave_sol_writer.rs +++ b/crates/evm/src/actors/enclave_sol_writer.rs @@ -4,6 +4,7 @@ // without even the implied warranty of MERCHANTABILITY // or FITNESS FOR A PARTICULAR PURPOSE. +use crate::contracts::IEnclave; use crate::domain::error_decoder::format_evm_error; use crate::domain::plaintext_publication::validate_plaintext_output; use crate::helpers::{encode_zk_proof, EthProvider}; @@ -12,7 +13,6 @@ use actix::prelude::*; use alloy::{ primitives::Address, providers::{Provider, WalletProvider}, - sol, }; use alloy::{ primitives::{Bytes, U256}, @@ -33,12 +33,6 @@ use e3_utils::MAILBOX_LIMIT; use std::collections::{HashMap, HashSet}; use tracing::info; -sol!( - #[sol(rpc)] - IEnclave, - "../../packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json" -); - /// Consumes events from the event bus and calls EVM methods on the Enclave.sol contract pub struct EnclaveSolWriter

{ provider: EthProvider

, diff --git a/crates/evm/src/actors/slashing_manager_sol_writer.rs b/crates/evm/src/actors/slashing_manager_sol_writer.rs index f8c9296f17..93d11460da 100644 --- a/crates/evm/src/actors/slashing_manager_sol_writer.rs +++ b/crates/evm/src/actors/slashing_manager_sol_writer.rs @@ -9,6 +9,7 @@ //! `proposeSlashByDkgParty` when DKG anchors resolve, and falls back to //! operator-attributed `proposeSlash` otherwise. +use crate::contracts::{ICiphernodeRegistry, ISlashingManager}; use crate::domain::attestation_evidence::encode_attestation_evidence; use crate::domain::error_decoder::format_evm_error; use crate::domain::slash_submission::{should_submit_slash, submission_delay, submission_rank}; @@ -20,7 +21,6 @@ use alloy::{ primitives::{Address, Bytes, U256}, providers::{Provider, WalletProvider}, rpc::types::TransactionReceipt, - sol, }; use anyhow::Result; use e3_events::prelude::*; @@ -33,12 +33,6 @@ use e3_events::{AccusationQuorumReached, EType}; use e3_utils::NotifySync; use tracing::{info, warn}; -sol!( - #[sol(rpc)] - ISlashingManager, - "../../packages/enclave-contracts/artifacts/contracts/interfaces/ISlashingManager.sol/ISlashingManager.json" -); - /// Submits `AccusationQuorumReached` events as slash proposals on-chain. pub struct SlashingManagerSolWriter

{ provider: EthProvider

, @@ -261,24 +255,13 @@ async fn resolve_party_id_for_operator( e3_id: U256, operator: Address, ) -> Result> { - sol! { - #[sol(rpc)] - interface IRegistryDkgView { - function getDkgAnchors(uint256 e3Id) - external - view - returns (uint256[] memory partyIds, bytes32[] memory, bytes32[] memory); - function canonicalCommitteeNodeAt(uint256 e3Id, uint256 partyId) external view returns (address); - } - } - let slashing = ISlashingManager::new(contract_address, provider.provider()); let registry = slashing.ciphernodeRegistry().call().await?; if registry == Address::ZERO { return Ok(None); } - let registry_view = IRegistryDkgView::new(registry, provider.provider()); + let registry_view = ICiphernodeRegistry::new(registry, provider.provider()); let anchors = registry_view.getDkgAnchors(e3_id).call().await?; for pid in anchors.partyIds { let node = registry_view diff --git a/crates/evm/src/contracts.rs b/crates/evm/src/contracts.rs new file mode 100644 index 0000000000..599fc31ac5 --- /dev/null +++ b/crates/evm/src/contracts.rs @@ -0,0 +1,249 @@ +// 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. + +//! Minimal stable Solidity ABI definitions for on-chain contracts. +//! +//! Instead of bundling full contract ABIs from JSON artifacts, this module +//! defines only the functions, events, errors, and structs that the ciphernode +//! actually uses. Contract upgrades must keep these exact signatures stable. +//! +//! All contract types (interfaces, enums) are replaced with their ABI-level +//! counterparts: `address` for contract types, `uint8` for enums. + +use alloy::sol; + +// ── IEnclave ─────────────────────────────────────────────────────────────── + +sol! { + #[sol(rpc)] + #[derive(Debug)] + interface IEnclave { + struct E3 { + uint256 seed; + uint8 committeeSize; + uint256 requestBlock; + uint256[2] inputWindow; + bytes32 encryptionSchemeId; + address e3Program; + uint8 paramSet; + bytes customParams; + address decryptionVerifier; + address pkVerifier; + bytes32 committeePublicKey; + bytes32 ciphertextOutput; + bytes plaintextOutput; + address requester; + bool proofAggregationEnabled; + } + + // ── Write functions ───────────────────────────────────────────────── + function publishPlaintextOutput( + uint256 e3Id, + bytes calldata plaintextOutput, + bytes calldata proof + ) external returns (bool success); + + function processE3Failure(uint256 e3Id) external; + + // ── View functions ────────────────────────────────────────────────── + function getE3(uint256 e3Id) external view returns (E3 memory e3); + + // ── Events ────────────────────────────────────────────────────────── + event E3Requested(uint256 e3Id, E3 e3, address indexed e3Program); + event CiphertextOutputPublished(uint256 indexed e3Id, bytes ciphertextOutput); + event E3Failed(uint256 e3Id, uint8 failedAtStage, uint8 reason); + event E3StageChanged(uint256 e3Id, uint8 previousStage, uint8 newStage); + + // ── Errors (only those our called functions can revert with) ──────── + error CiphertextOutputNotPublished(uint256 e3Id); + error PlaintextOutputAlreadyPublished(uint256 e3Id); + error E3DoesNotExist(uint256 e3Id); + error InvalidStage(uint256 e3Id, uint8 expected, uint8 actual); + error ProofRequired(); + error InvalidOutput(bytes output); + error E3NotFailed(uint256 e3Id); + error NoPaymentToRefund(uint256 e3Id); + } +} + +// ── ISlashingManager ──────────────────────────────────────────────────────── + +sol! { + #[sol(rpc)] + #[derive(Debug)] + interface ISlashingManager { + // ── Write functions ───────────────────────────────────────────────── + function proposeSlash( + uint256 e3Id, + address operator, + bytes calldata proof + ) external returns (uint256 proposalId); + + function proposeSlashByDkgParty( + uint256 e3Id, + uint256 partyId, + bytes calldata proof + ) external returns (uint256 proposalId); + + // ── View functions ────────────────────────────────────────────────── + function ciphernodeRegistry() external view returns (address); + + // ── Events ────────────────────────────────────────────────────────── + event SlashExecuted( + uint256 proposalId, + uint256 e3Id, + address operator, + bytes32 reason, + uint256 ticketAmount, + uint256 licenseAmount + ); + + // ── Errors (only those our called functions can revert with) ──────── + error OperatorNotInCommittee(); + error VoterNotInCommittee(); + error DuplicateEvidence(); + error InsufficientAttestations(); + error InvalidVoteSignature(); + error SignatureExpired(); + error DuplicateVoter(); + error VoterIsAccused(); + error EquivocationDetected(); + error ChainIdMismatch(); + error PartyIdNotInDkgAnchors(); + error ProofRequired(); + error InvalidProof(); + error Unauthorized(); + } +} + +// ── ICiphernodeRegistry ──────────────────────────────────────────────────── + +sol! { + #[sol(rpc)] + #[derive(Debug)] + interface ICiphernodeRegistry { + // ── Write functions ───────────────────────────────────────────────── + function submitTicket(uint256 e3Id, uint256 ticketNumber) external; + + function finalizeCommittee(uint256 e3Id) external returns (bool success); + + function publishCommittee( + uint256 e3Id, + bytes calldata publicKey, + bytes32 pkCommitment, + bytes calldata proof, + bytes calldata dkgAttestationBundle + ) external; + + // ── View functions ────────────────────────────────────────────────── + function isOpen(uint256 e3Id) external view returns (bool); + + function committeePublicKey(uint256 e3Id) external view returns (bytes32 publicKeyHash); + + function getDkgAnchors( + uint256 e3Id + ) + external + view + returns ( + uint256[] memory partyIds, + bytes32[] memory skAggCommits, + bytes32[] memory esmAggCommits + ); + + function canonicalCommitteeNodeAt( + uint256 e3Id, + uint256 partyId + ) external view returns (address); + + function dkgFoldAttestationVerifier() external view returns (address); + + function accusationVoteValidity() external view returns (uint256); + + // ── Events ────────────────────────────────────────────────────────── + event CiphernodeAdded( + address indexed node, + uint256 index, + uint256 numNodes, + uint256 size + ); + + event CiphernodeRemoved( + address indexed node, + uint256 index, + uint256 numNodes, + uint256 size + ); + + event CommitteeRequested( + uint256 indexed e3Id, + uint256 seed, + uint32[2] threshold, + uint256 requestBlock, + uint256 committeeDeadline + ); + + event SortitionCommitteeFinalized( + uint256 indexed e3Id, + address[] committee, + uint256[] scores + ); + + event TicketSubmitted( + uint256 indexed e3Id, + address indexed node, + uint256 ticketId, + uint256 score + ); + + event CommitteeMemberExpelled( + uint256 indexed e3Id, + address indexed node, + bytes32 reason, + uint256 activeCountAfter + ); + + // ── Errors (only those our called functions can revert with) ──────── + error CommitteeNotRequested(); + error CommitteeAlreadyFinalized(); + error CommitteeNotFinalized(); + error CommitteeNotPublished(); + error CommitteeAlreadyPublished(); + error SubmissionWindowClosed(); + error SubmissionWindowNotClosed(); + error ThresholdNotMet(); + error NodeAlreadySubmitted(); + error InvalidTicketNumber(); + error NodeNotEligible(); + error PkCommitmentRequired(); + error DkgProofRequired(); + error InvalidDkgProof(); + error FoldAttestationsRequired(); + } +} + +// ── IBondingRegistry ──────────────────────────────────────────────────────── + +sol! { + #[sol(rpc)] + #[derive(Debug)] + interface IBondingRegistry { + event TicketBalanceUpdated( + address indexed operator, + int256 delta, + uint256 newBalance, + bytes32 indexed reason + ); + + event OperatorActivationChanged(address indexed operator, bool active); + + event ConfigurationUpdated( + bytes32 indexed parameter, + uint256 oldValue, + uint256 newValue + ); + } +} diff --git a/crates/evm/src/domain/bonding_registry_events.rs b/crates/evm/src/domain/bonding_registry_events.rs index 9212b80820..578a4d3cdc 100644 --- a/crates/evm/src/domain/bonding_registry_events.rs +++ b/crates/evm/src/domain/bonding_registry_events.rs @@ -6,21 +6,14 @@ //! Pure translation of `BondingRegistry.sol` logs into `EnclaveEventData`. +use crate::contracts::IBondingRegistry; use alloy::{ primitives::{LogData, B256}, - sol, sol_types::SolEvent, }; use e3_events::EnclaveEventData; use tracing::{error, trace}; -sol!( - #[sol(rpc)] - #[derive(Debug)] - IBondingRegistry, - "../../packages/enclave-contracts/artifacts/contracts/interfaces/IBondingRegistry.sol/IBondingRegistry.json" -); - struct TicketBalanceUpdatedWithChainId(pub IBondingRegistry::TicketBalanceUpdated, pub u64); impl From for e3_events::TicketBalanceUpdated { diff --git a/crates/evm/src/domain/ciphernode_registry_events.rs b/crates/evm/src/domain/ciphernode_registry_events.rs index 111b5a9d26..61a7bb9bbc 100644 --- a/crates/evm/src/domain/ciphernode_registry_events.rs +++ b/crates/evm/src/domain/ciphernode_registry_events.rs @@ -6,21 +6,14 @@ //! Pure translation of `CiphernodeRegistry.sol` logs into `EnclaveEventData`. +use crate::contracts::ICiphernodeRegistry; use alloy::{ primitives::{LogData, B256}, - sol, sol_types::SolEvent, }; use e3_events::{CommitteeFinalized, E3id, EnclaveEventData, Seed}; use tracing::{error, info, trace}; -sol!( - #[sol(rpc)] - #[derive(Debug)] - ICiphernodeRegistry, - "../../packages/enclave-contracts/artifacts/contracts/interfaces/ICiphernodeRegistry.sol/ICiphernodeRegistry.json" -); - struct CiphernodeAddedWithChainId(pub ICiphernodeRegistry::CiphernodeAdded, pub u64); impl From for e3_events::CiphernodeAdded { diff --git a/crates/evm/src/domain/enclave_events.rs b/crates/evm/src/domain/enclave_events.rs index bf34db353b..0653d6a231 100644 --- a/crates/evm/src/domain/enclave_events.rs +++ b/crates/evm/src/domain/enclave_events.rs @@ -6,8 +6,9 @@ //! Pure translation of `Enclave.sol` logs into `EnclaveEventData`. +use crate::contracts::IEnclave; use alloy::primitives::{LogData, B256}; -use alloy::{sol, sol_types::SolEvent}; +use alloy::sol_types::SolEvent; use e3_events::E3id; use e3_events::EnclaveEventData; use e3_events::{E3Failed, E3Stage, E3StageChanged, FailureReason}; @@ -18,12 +19,6 @@ use e3_zk_helpers::CiphernodesCommitteeSize; use num_bigint::BigUint; use tracing::{error, info, trace, warn}; -sol!( - #[sol(rpc)] - IEnclave, - "../../packages/enclave-contracts/artifacts/contracts/interfaces/IEnclave.sol/IEnclave.json" -); - struct E3RequestedWithChainId(pub IEnclave::E3Requested, pub u64); impl E3RequestedWithChainId { diff --git a/crates/evm/src/domain/error_decoder.rs b/crates/evm/src/domain/error_decoder.rs index 623ab0c480..e56f4de585 100644 --- a/crates/evm/src/domain/error_decoder.rs +++ b/crates/evm/src/domain/error_decoder.rs @@ -6,42 +6,22 @@ //! Pure decoding of raw EVM revert data into human-readable contract errors. -use alloy::sol; +use crate::contracts::{ICiphernodeRegistry, IEnclave, ISlashingManager}; use alloy::sol_types::SolInterface; -sol!( - #[derive(Debug)] - #[sol(ignore_unlinked)] - Enclave, - "../../packages/enclave-contracts/artifacts/contracts/Enclave.sol/Enclave.json" -); - -sol!( - #[derive(Debug)] - #[sol(ignore_unlinked)] - CiphernodeRegistryOwnable, - "../../packages/enclave-contracts/artifacts/contracts/registry/CiphernodeRegistryOwnable.sol/CiphernodeRegistryOwnable.json" -); - -sol!( - #[derive(Debug)] - SlashingManager, - "../../packages/enclave-contracts/artifacts/contracts/interfaces/ISlashingManager.sol/ISlashingManager.json" -); - /// Try to decode raw revert data into a human-readable error string. pub fn decode_error(data: &[u8]) -> Option { if data.len() < 4 { return None; } - if let Ok(err) = Enclave::EnclaveErrors::abi_decode(data) { + if let Ok(err) = IEnclave::IEnclaveErrors::abi_decode(data) { return Some(format!("{err:?}")); } - if let Ok(err) = CiphernodeRegistryOwnable::CiphernodeRegistryOwnableErrors::abi_decode(data) { + if let Ok(err) = ICiphernodeRegistry::ICiphernodeRegistryErrors::abi_decode(data) { return Some(format!("{err:?}")); } - if let Ok(err) = SlashingManager::SlashingManagerErrors::abi_decode(data) { + if let Ok(err) = ISlashingManager::ISlashingManagerErrors::abi_decode(data) { return Some(format!("{err:?}")); } @@ -94,7 +74,7 @@ mod tests { #[test] fn test_decode_known_errors() { // CiphertextOutputNotPublished(uint256 e3Id) with e3Id = 1 - let mut data = Enclave::CiphertextOutputNotPublished::SELECTOR.to_vec(); + let mut data = IEnclave::CiphertextOutputNotPublished::SELECTOR.to_vec(); data.extend_from_slice(&[0u8; 31]); data.push(1); // e3Id = 1 let decoded = decode_error(&data).unwrap(); @@ -107,7 +87,7 @@ mod tests { #[test] fn test_decode_parameterless_error() { // CommitteeNotRequested() - let data = CiphernodeRegistryOwnable::CommitteeNotRequested::SELECTOR.to_vec(); + let data = ICiphernodeRegistry::CommitteeNotRequested::SELECTOR.to_vec(); let decoded = decode_error(&data).unwrap(); assert!(decoded.contains("CommitteeNotRequested"), "got: {decoded}"); } @@ -115,7 +95,7 @@ mod tests { #[test] fn test_decode_from_error_string() { // Simulate an alloy error string containing hex revert data - let selector = hex::encode(Enclave::CiphertextOutputNotPublished::SELECTOR); + let selector = hex::encode(IEnclave::CiphertextOutputNotPublished::SELECTOR); let param = "0000000000000000000000000000000000000000000000000000000000000001"; let error_str = format!( "server returned an error response: error code 3: execution reverted, data: \"0x{selector}{param}\"" @@ -142,7 +122,7 @@ mod tests { fn test_short_selector_found_despite_longer_hex() { // Error string contains a tx hash (32 bytes) AND a short 4-byte selector. // The decoder must find the selector even though the tx hash is longer. - let selector = hex::encode(CiphernodeRegistryOwnable::CommitteeNotRequested::SELECTOR); + let selector = hex::encode(ICiphernodeRegistry::CommitteeNotRequested::SELECTOR); let tx_hash = "aabbccddee11223344556677889900aabbccddee11223344556677889900aabb"; let error_str = format!("tx 0x{tx_hash} reverted with data: 0x{selector}"); let decoded = decode_error_from_str(&error_str).unwrap(); diff --git a/crates/evm/src/domain/slashing_events.rs b/crates/evm/src/domain/slashing_events.rs index 2ad45a84af..c45cc99119 100644 --- a/crates/evm/src/domain/slashing_events.rs +++ b/crates/evm/src/domain/slashing_events.rs @@ -6,20 +6,14 @@ //! Pure translation of `SlashingManager.sol` logs into `EnclaveEventData`. +use crate::contracts::ISlashingManager; use alloy::{ primitives::{LogData, B256, U256}, - sol, sol_types::SolEvent, }; use e3_events::{E3id, EnclaveEventData}; use tracing::{error, info, trace}; -sol!( - #[sol(rpc)] - ISlashingManager, - "../../packages/enclave-contracts/artifacts/contracts/interfaces/ISlashingManager.sol/ISlashingManager.json" -); - /// Convert a U256 to u128, returning None if the value overflows. fn safe_u256_to_u128(val: U256) -> Option { if val > U256::from(u128::MAX) { diff --git a/crates/evm/src/lib.rs b/crates/evm/src/lib.rs index 32a229ce2a..c8076640dd 100644 --- a/crates/evm/src/lib.rs +++ b/crates/evm/src/lib.rs @@ -13,6 +13,7 @@ //! - [`messages`] holds the actix message and event types exchanged between them. mod actors; +mod contracts; mod domain; mod messages; mod repo; diff --git a/crates/support/contracts/ImageID.sol b/crates/support/contracts/ImageID.sol index 6dec581b62..f37983df9b 100644 --- a/crates/support/contracts/ImageID.sol +++ b/crates/support/contracts/ImageID.sol @@ -19,5 +19,5 @@ pragma solidity ^0.8.20; library ImageID { - bytes32 public constant PROGRAM_ID = bytes32(0xc36e34f0b40876593adc519e4cccf8795ec90e9bfef0a44f20be865e7cb7f0a2); + bytes32 public constant PROGRAM_ID = bytes32(0xc36e34f0b40876593adc519e4cccf8795ec90e9bfef0a44f20be865e7cb7f0a2); } diff --git a/crates/support/tests/Elf.sol b/crates/support/tests/Elf.sol index 4647a56f75..e2a1e945cd 100644 --- a/crates/support/tests/Elf.sol +++ b/crates/support/tests/Elf.sol @@ -19,6 +19,6 @@ pragma solidity ^0.8.20; library Elf { - string public constant PROGRAM_PATH = - "/home/ace/main/gnosis/enclave/crates/support/target/riscv-guest/methods/guests/riscv32im-risc0-zkvm-elf/release/program.bin"; + string public constant PROGRAM_PATH = + "/home/ace/main/gnosis/enclave/crates/support/target/riscv-guest/methods/guests/riscv32im-risc0-zkvm-elf/release/program.bin"; } diff --git a/docs/pages/ciphernode-operators/running.mdx b/docs/pages/ciphernode-operators/running.mdx index 2ca1b7774a..722b5919d9 100644 --- a/docs/pages/ciphernode-operators/running.mdx +++ b/docs/pages/ciphernode-operators/running.mdx @@ -106,10 +106,10 @@ chains: address: '0xb9b64c5e0a30f38ed33760f299613087aAe87283' deploy_block: 10939870 slashing_manager: - address: "0x0553387EE0992Fe339579728B6c777164fD1de40" + address: '0x0553387EE0992Fe339579728B6c777164fD1de40' deploy_block: 10939868 fee_token: - address: "0x2721Cdf281d40744aD567cBf3e7100F60bcbAE79" + address: '0x2721Cdf281d40744aD567cBf3e7100F60bcbAE79' deploy_block: 10939865 ```