Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4,332 changes: 2,885 additions & 1,447 deletions CHANGELOG.md

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions crates/cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use crate::ciphernode::{self, ChainArgs, CiphernodeCommands};
use crate::config::{self, ConfigCommands};
use crate::events::{self, EventsCommands};
use crate::faucet;
use crate::helpers::telemetry::{setup_simple_tracing, setup_tracing};
use crate::net::{self, NetCommands};
use crate::node::{self, NodeCommands as NodeStateCommands};
Expand Down Expand Up @@ -188,6 +189,9 @@ impl Cli {
Commands::Node { command } => node::execute(out, command, &config).await?,
Commands::Rev => rev::execute(out).await?,
Commands::Config { command } => config::execute(out, command, &config).await?,
Commands::Faucet { chain } => {
faucet::execute(out, &config, chain.chain.as_deref()).await?
}
}

close_all_connections();
Expand Down Expand Up @@ -321,6 +325,12 @@ pub enum Commands {
#[command(subcommand)]
command: ConfigCommands,
},

/// Request testnet tokens (FOLD + fee token) from the configured faucet
Faucet {
#[command(flatten)]
chain: ChainArgs,
},
}

#[derive(Clone, Debug, Serialize, Deserialize)]
Expand Down
178 changes: 178 additions & 0 deletions crates/cli/src/faucet.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
// 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 std::str::FromStr;

use alloy::{
primitives::{Address, U256},
providers::WalletProvider,
sol,
};
use anyhow::{anyhow, bail, Context, Result};
use e3_config::{chain_config::ChainConfig, AppConfig};
use e3_console::{log, Console};
use e3_crypto::Cipher;
use e3_entrypoint::helpers::datastore::get_repositories;
use e3_evm::{
error_decoder::format_evm_error,
helpers::{load_signer_from_repository, ProviderConfig},
EthPrivateKeyRepositoryFactory,
};

mod faucet_contract {
use super::sol;

sol!(
#[sol(rpc)]
FaucetContract,
"../../packages/interfold-contracts/artifacts/contracts/test/Faucet.sol/Faucet.json"
);
}

mod erc20 {
use super::sol;

sol!(
#[sol(rpc)]
interface IERC20 {
function balanceOf(address account) external view returns (uint256);
}
);
}

use erc20::IERC20;
use faucet_contract::FaucetContract;

/// Calls `faucet()` on the configured Faucet contract, sending FOLD + fee
/// tokens to the operator's signing address. Testnet only.
pub async fn execute(out: Console, config: &AppConfig, selection: Option<&str>) -> Result<()> {
let chain = select_chain(config, selection)?;
let faucet_contract = chain
.contracts
.faucet
.as_ref()
.ok_or_else(|| anyhow!("No `faucet` contract configured for chain '{}'", chain.name))?;
let faucet_address =
Address::from_str(faucet_contract.address_str()).context("Invalid faucet address")?;

let rpc = chain.rpc_url()?;
let cipher = Cipher::from_file(config.key_file()).await?;
let repositories = get_repositories(config)?;
let signer = load_signer_from_repository(repositories.eth_private_key(), &cipher).await?;
let provider = ProviderConfig::new(rpc, chain.rpc_auth.clone())
.create_signer_provider(&signer)
.await?;
let recipient = provider.provider().default_signer_address();

let faucet = FaucetContract::new(faucet_address, provider.provider().clone());

// Replicate the contract's gating client-side so we can print a clear
// message instead of a bare "execution reverted" (many RPCs omit the
// revert reason). The faucet tops up each token independently, only when
// the caller's balance is below the per-token amount.
let fold_addr = faucet.fold().call().await?;
let fee_token_addr = faucet.feeToken().call().await?;
let amount_fold = faucet.AMOUNT_FOLD().call().await?;
let amount_fee_token = faucet.AMOUNT_FEE_TOKEN().call().await?;

let fold = IERC20::new(fold_addr, provider.provider().clone());
let fee_token = IERC20::new(fee_token_addr, provider.provider().clone());

let caller_fold = fold.balanceOf(recipient).call().await?;
let caller_fee_token = fee_token.balanceOf(recipient).call().await?;

let needs_fold = caller_fold < amount_fold;
let needs_fee_token = caller_fee_token < amount_fee_token;

if !needs_fold && !needs_fee_token {
log!(
out,
"Nothing to claim: {:#x} already holds at least {} FOLD and {} fee tokens.",
recipient,
format_units(amount_fold, 18),
format_units(amount_fee_token, 6)
);
return Ok(());
}

// Check the faucet itself is funded for whatever the caller needs.
if needs_fold {
let faucet_fold = fold.balanceOf(faucet_address).call().await?;
if faucet_fold < amount_fold {
bail!(
"Faucet is out of FOLD (has {}, needs {}). Ask an admin to refund it.",
format_units(faucet_fold, 18),
format_units(amount_fold, 18)
);
}
}
if needs_fee_token {
let faucet_fee_token = fee_token.balanceOf(faucet_address).call().await?;
if faucet_fee_token < amount_fee_token {
bail!(
"Faucet is out of fee tokens (has {}, needs {}). Ask an admin to refund it.",
format_units(faucet_fee_token, 6),
format_units(amount_fee_token, 6)
);
}
}

let receipt = faucet
.faucet()
.send()
.await
.map_err(|err| {
anyhow!(
"Faucet transaction failed: {}",
format_evm_error(&anyhow::Error::new(err))
)
})?
.get_receipt()
.await?;

log!(
out,
"Faucet sent {}{}{} to {:#x} (tx: {:#x})",
if needs_fold {
format!("{} FOLD", format_units(amount_fold, 18))
} else {
String::new()
},
if needs_fold && needs_fee_token {
" + "
} else {
""
},
if needs_fee_token {
format!("{} fee tokens", format_units(amount_fee_token, 6))
} else {
String::new()
},
recipient,
receipt.transaction_hash
);

Ok(())
}

/// Format a token amount for display, falling back to the raw integer if the
/// decimals can't be applied.
fn format_units(value: U256, decimals: u8) -> String {
alloy::primitives::utils::format_units(value, decimals).unwrap_or_else(|_| value.to_string())
}

fn select_chain<'a>(config: &'a AppConfig, name: Option<&str>) -> Result<&'a ChainConfig> {
match name {
Some(desired) => config
.chains()
.iter()
.find(|c| c.name == desired)
.ok_or_else(|| anyhow!("Chain '{}' not found in configuration", desired)),
None => config.chains().first().ok_or_else(|| {
anyhow!("No chains configured. Run `interfold ciphernode setup` first.")
}),
}
}
1 change: 1 addition & 0 deletions crates/cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ mod cli;
mod config;
mod config_setup;
mod events;
mod faucet;
pub mod helpers;
mod init;
mod net;
Expand Down
3 changes: 3 additions & 0 deletions crates/config/src/contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ pub struct ContractAddresses {
pub fee_token: Option<Contract>,
pub slashing_manager: Option<Contract>,
pub dkg_fold_attestation_verifier: Option<Contract>,
/// Testnet faucet (sepolia). Distributes FOLD + fee tokens to callers.
pub faucet: Option<Contract>,
}

impl ContractAddresses {
Expand All @@ -62,6 +64,7 @@ impl ContractAddresses {
self.fee_token.as_ref(),
self.slashing_manager.as_ref(),
self.dkg_fold_attestation_verifier.as_ref(),
self.faucet.as_ref(),
]
.into_iter()
.flatten()
Expand Down
20 changes: 19 additions & 1 deletion crates/evm/src/domain/error_decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,24 @@
//! Pure decoding of raw EVM revert data into human-readable contract errors.

use crate::contracts::{ICiphernodeRegistry, IInterfold, ISlashingManager};
use alloy::sol_types::SolInterface;
use alloy::sol_types::{Panic, Revert, SolError, SolInterface};

/// Try to decode raw revert data into a human-readable error string.
pub fn decode_error(data: &[u8]) -> Option<String> {
if data.len() < 4 {
return None;
}

// Standard `Error(string)` reverts (e.g. `require(cond, "msg")` /
// `revert("msg")`) and `Panic(uint256)` are not contract-specific, so
// try them first.
if let Ok(revert) = Revert::abi_decode(data) {
return Some(revert.reason);
}
if let Ok(panic) = Panic::abi_decode(data) {
return Some(format!("panic: {panic}"));
}

if let Ok(err) = IInterfold::IInterfoldErrors::abi_decode(data) {
return Some(format!("{err:?}"));
}
Expand Down Expand Up @@ -113,6 +123,14 @@ mod tests {
assert!(decode_error(&data).is_none());
}

#[test]
fn test_decode_string_revert() {
// Standard `revert("No FOLD")` -> Error(string)
let data = Revert::from("No FOLD").abi_encode();
let decoded = decode_error(&data).unwrap();
assert_eq!(decoded, "No FOLD");
}

#[test]
fn test_extract_hex_blobs_too_short() {
assert!(extract_all_hex_blobs("0x1234").is_empty());
Expand Down
2 changes: 1 addition & 1 deletion crates/support/contracts/ImageID.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,5 @@
pragma solidity ^0.8.20;

library ImageID {
bytes32 public constant PROGRAM_ID = bytes32(0x7ac6020923d10004ccb27d8cdb5df72e7476e8a9775b9c1c711e9d906b44e105);
bytes32 public constant PROGRAM_ID = bytes32(0x7ac6020923d10004ccb27d8cdb5df72e7476e8a9775b9c1c711e9d906b44e105);
}
1 change: 1 addition & 0 deletions crates/tests/tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1417,6 +1417,7 @@ async fn test_trbfv_actor() -> Result<()> {
)),
dkg_fold_attestation_verifier: benchmark_dkg_fold_attestation_verifier_address()
.map(|a| e3_config::Contract::AddressOnly(a.to_string())),
faucet: None,
},
finalization_ms: None,
reorg_confirmations: None,
Expand Down
Loading
Loading