diff --git a/crates/cli/src/commands/new.rs b/crates/cli/src/commands/new.rs index 668f1d6c..f381d50a 100644 --- a/crates/cli/src/commands/new.rs +++ b/crates/cli/src/commands/new.rs @@ -86,6 +86,7 @@ pub async fn handle_init(path: &Path) -> Result<(), InitError> { }, webhooks: None, rate_limits: None, + advanced: None, }; fs::write(project_path.join("rrelayer.yaml"), serde_yaml::to_string(&yaml_content)?)?; diff --git a/crates/core/src/background_tasks/automatic_top_up_task.rs b/crates/core/src/background_tasks/automatic_top_up_task.rs index ff26f94a..62814568 100644 --- a/crates/core/src/background_tasks/automatic_top_up_task.rs +++ b/crates/core/src/background_tasks/automatic_top_up_task.rs @@ -41,6 +41,7 @@ pub struct AutomaticTopUpTask { relayer_refresh_interval: Interval, top_up_check_interval: Interval, transactions_queues: Arc>, + allow_non_relayer_topups: bool, } impl AutomaticTopUpTask { @@ -51,6 +52,9 @@ impl AutomaticTopUpTask { transactions_queues: Arc>, safe_proxy_manager: Arc, ) -> Self { + let allow_non_relayer_topups = + config.advanced.as_ref().and_then(|a| a.allow_non_relayer_topups).unwrap_or(false); + Self { postgres_client, providers, @@ -60,6 +64,7 @@ impl AutomaticTopUpTask { relayer_refresh_interval: interval(Duration::from_secs(30)), top_up_check_interval: interval(Duration::from_secs(30)), transactions_queues, + allow_non_relayer_topups, } } @@ -184,43 +189,99 @@ impl AutomaticTopUpTask { } }; - if relayer_addresses.is_empty() { - info!("No relayer addresses found for top-up on chain {}", chain_id); - return; + let additional_addresses = self.resolve_additional_addresses(config, &relayer_addresses); + + if !additional_addresses.is_empty() { + info!( + "Including {} non-relayer additional addresses for top-up on chain {}", + additional_addresses.len(), + chain_id + ); } - if let Some(native_config) = &config.native { - info!("Processing native token top-ups for {} addresses", relayer_addresses.len()); - self.process_native_token_top_ups( - chain_id, - provider, - &config.from.relayer.address, - &relayer_addresses, - native_config, - config, - ) - .await; + if relayer_addresses.is_empty() && additional_addresses.is_empty() { + info!("No addresses found for top-up on chain {}", chain_id); + return; } - if let Some(erc20_tokens) = &config.erc20_tokens { - for (index, token_config) in erc20_tokens.iter().enumerate() { + if let Some(native_config) = &config.native { + if !relayer_addresses.is_empty() { info!( - "Processing ERC-20 token top-ups for token {} ({}/{}) on {} addresses", - token_config.address, - index + 1, - erc20_tokens.len(), + "Processing native token top-ups for {} relayer addresses", relayer_addresses.len() ); - self.process_erc20_token_top_ups( + self.process_native_token_top_ups( chain_id, provider, &config.from.relayer.address, &relayer_addresses, - token_config, + native_config, config, + false, ) .await; } + + if !additional_addresses.is_empty() { + info!( + "Processing native token top-ups for {} non-relayer additional addresses", + additional_addresses.len() + ); + self.process_native_token_top_ups( + chain_id, + provider, + &config.from.relayer.address, + &additional_addresses, + native_config, + config, + true, + ) + .await; + } + } + + if let Some(erc20_tokens) = &config.erc20_tokens { + for (index, token_config) in erc20_tokens.iter().enumerate() { + if !relayer_addresses.is_empty() { + info!( + "Processing ERC-20 token top-ups for token {} ({}/{}) on {} relayer addresses", + token_config.address, + index + 1, + erc20_tokens.len(), + relayer_addresses.len() + ); + self.process_erc20_token_top_ups( + chain_id, + provider, + &config.from.relayer.address, + &relayer_addresses, + token_config, + config, + false, + ) + .await; + } + + if !additional_addresses.is_empty() { + info!( + "Processing ERC-20 token top-ups for token {} ({}/{}) on {} non-relayer additional addresses", + token_config.address, + index + 1, + erc20_tokens.len(), + additional_addresses.len() + ); + self.process_erc20_token_top_ups( + chain_id, + provider, + &config.from.relayer.address, + &additional_addresses, + token_config, + config, + true, + ) + .await; + } + } } if config.native.is_none() && config.erc20_tokens.is_none() { @@ -231,7 +292,8 @@ impl AutomaticTopUpTask { } } - /// Processes native token top-ups for relayer addresses. + /// Processes native token top-ups for the given addresses. + #[allow(clippy::too_many_arguments)] async fn process_native_token_top_ups( &self, chain_id: &ChainId, @@ -240,6 +302,7 @@ impl AutomaticTopUpTask { relayer_addresses: &[EvmAddress], native_config: &NativeTokenConfig, config: &NetworkAutomaticTopUpConfig, + skip_relayer_validation: bool, ) { let mut addresses_needing_top_up = Vec::new(); @@ -313,6 +376,7 @@ impl AutomaticTopUpTask { &address, native_config, config, + skip_relayer_validation, ) .await { @@ -331,7 +395,8 @@ impl AutomaticTopUpTask { } } - /// Processes ERC-20 token top-ups for relayer addresses. + /// Processes ERC-20 token top-ups for the given addresses. + #[allow(clippy::too_many_arguments)] async fn process_erc20_token_top_ups( &self, chain_id: &ChainId, @@ -340,6 +405,7 @@ impl AutomaticTopUpTask { relayer_addresses: &[EvmAddress], token_config: &Erc20TokenConfig, config: &NetworkAutomaticTopUpConfig, + skip_relayer_validation: bool, ) { let mut addresses_needing_top_up = Vec::new(); @@ -419,6 +485,7 @@ impl AutomaticTopUpTask { &address, token_config, config, + skip_relayer_validation, ) .await { @@ -439,6 +506,7 @@ impl AutomaticTopUpTask { } /// Sends a native token top-up transaction from one relayer to another. + #[allow(clippy::too_many_arguments)] async fn send_native_top_up_transaction( &self, chain_id: &ChainId, @@ -447,6 +515,7 @@ impl AutomaticTopUpTask { relayer_address: &EvmAddress, native_config: &NativeTokenConfig, config: &NetworkAutomaticTopUpConfig, + skip_relayer_validation: bool, ) -> Result { if from_address == relayer_address { return Err(format!( @@ -455,21 +524,23 @@ impl AutomaticTopUpTask { )); } - match self.postgres_client.get_relayer_by_address(relayer_address, chain_id).await { - Ok(Some(_relayer)) => { - // Valid relayer, proceed - } - Ok(None) => { - return Err(format!( - "Security check failed: relayer_address {} is not a registered relayer on chain {}", - relayer_address, chain_id - )); - } - Err(e) => { - return Err(format!( - "Failed to validate relayer_address {} as relayer: {}", - relayer_address, e - )); + if !skip_relayer_validation { + match self.postgres_client.get_relayer_by_address(relayer_address, chain_id).await { + Ok(Some(_relayer)) => { + // Valid relayer, proceed + } + Ok(None) => { + return Err(format!( + "Security check failed: relayer_address {} is not a registered relayer on chain {}", + relayer_address, chain_id + )); + } + Err(e) => { + return Err(format!( + "Failed to validate relayer_address {} as relayer: {}", + relayer_address, e + )); + } } } @@ -659,6 +730,17 @@ impl AutomaticTopUpTask { Ok(addresses) } + /// Resolves additional non-relayer addresses from the config, filtering out + /// the from_address and any addresses already present in the relayer list. + /// Returns an empty list when `allow_non_relayer_topups` is disabled. + fn resolve_additional_addresses( + &self, + config: &NetworkAutomaticTopUpConfig, + relayer_addresses: &[EvmAddress], + ) -> Vec { + resolve_additional_addresses(self.allow_non_relayer_topups, config, relayer_addresses) + } + /// Checks if the from_address has sufficient native balance for top-up operations. async fn check_native_from_address_balance( &self, @@ -879,6 +961,7 @@ impl AutomaticTopUpTask { } /// Sends an ERC-20 token top-up transaction from one relayer to another. + #[allow(clippy::too_many_arguments)] async fn send_erc20_top_up_transaction( &self, chain_id: &ChainId, @@ -887,6 +970,7 @@ impl AutomaticTopUpTask { relayer_address: &EvmAddress, token_config: &Erc20TokenConfig, config: &NetworkAutomaticTopUpConfig, + skip_relayer_validation: bool, ) -> Result { if from_address == relayer_address { return Err(format!( @@ -896,21 +980,23 @@ impl AutomaticTopUpTask { } // Validate that relayer_address is a relayer for security - match self.postgres_client.get_relayer_by_address(relayer_address, chain_id).await { - Ok(Some(_relayer)) => { - // Valid relayer, proceed - } - Ok(None) => { - return Err(format!( - "Security check failed: relayer_address {} is not a registered relayer on chain {}", - relayer_address, chain_id - )); - } - Err(e) => { - return Err(format!( - "Failed to validate relayer_address {} as relayer: {}", - relayer_address, e - )); + if !skip_relayer_validation { + match self.postgres_client.get_relayer_by_address(relayer_address, chain_id).await { + Ok(Some(_relayer)) => { + // Valid relayer, proceed + } + Ok(None) => { + return Err(format!( + "Security check failed: relayer_address {} is not a registered relayer on chain {}", + relayer_address, chain_id + )); + } + Err(e) => { + return Err(format!( + "Failed to validate relayer_address {} as relayer: {}", + relayer_address, e + )); + } } } @@ -1101,3 +1187,116 @@ pub async fn run_automatic_top_up_task( info!("Started automatic top-up background task"); } + +/// Resolves additional non-relayer addresses from the config, filtering out +/// the from_address and any addresses already present in the relayer list. +/// Returns an empty list when `allow_non_relayer_topups` is disabled. +fn resolve_additional_addresses( + allow_non_relayer_topups: bool, + config: &NetworkAutomaticTopUpConfig, + relayer_addresses: &[EvmAddress], +) -> Vec { + if !allow_non_relayer_topups { + return Vec::new(); + } + + config + .additional_addresses + .as_ref() + .map(|addrs| { + addrs + .iter() + .filter(|addr| { + *addr != &config.from.relayer.address && !relayer_addresses.contains(addr) + }) + .copied() + .collect() + }) + .unwrap_or_default() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::yaml::{AllOrAddresses, NetworkAutomaticTopUpFrom, NetworkAutomaticTopUpRelayer}; + + fn addr(s: &str) -> EvmAddress { + s.parse().expect("valid address") + } + + fn make_config( + from_address: &str, + additional: Option>, + ) -> NetworkAutomaticTopUpConfig { + NetworkAutomaticTopUpConfig { + from: NetworkAutomaticTopUpFrom { + safe: None, + relayer: NetworkAutomaticTopUpRelayer { + address: addr(from_address), + internal_only: None, + }, + }, + relayers: AllOrAddresses::All, + native: None, + erc20_tokens: None, + additional_addresses: additional.map(|addrs| addrs.into_iter().map(addr).collect()), + } + } + + const FROM: &str = "0x1111111111111111111111111111111111111111"; + const RELAYER_A: &str = "0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"; + const EXTRA_B: &str = "0xBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB"; + const EXTRA_C: &str = "0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC"; + + #[test] + fn returns_empty_when_disabled() { + let config = make_config(FROM, Some(vec![EXTRA_B, EXTRA_C])); + let result = resolve_additional_addresses(false, &config, &[]); + assert!(result.is_empty()); + } + + #[test] + fn returns_additional_addresses_when_enabled() { + let config = make_config(FROM, Some(vec![EXTRA_B, EXTRA_C])); + let result = resolve_additional_addresses(true, &config, &[]); + assert_eq!(result, vec![addr(EXTRA_B), addr(EXTRA_C)]); + } + + #[test] + fn returns_empty_when_no_additional_addresses() { + let config = make_config(FROM, None); + let result = resolve_additional_addresses(true, &config, &[]); + assert!(result.is_empty()); + } + + #[test] + fn filters_out_from_address() { + let config = make_config(FROM, Some(vec![FROM, EXTRA_B])); + let result = resolve_additional_addresses(true, &config, &[]); + assert_eq!(result, vec![addr(EXTRA_B)]); + } + + #[test] + fn filters_out_existing_relayer_addresses() { + let relayer_addresses = vec![addr(RELAYER_A)]; + let config = make_config(FROM, Some(vec![RELAYER_A, EXTRA_B])); + let result = resolve_additional_addresses(true, &config, &relayer_addresses); + assert_eq!(result, vec![addr(EXTRA_B)]); + } + + #[test] + fn filters_both_from_and_relayer_addresses() { + let relayer_addresses = vec![addr(RELAYER_A)]; + let config = make_config(FROM, Some(vec![FROM, RELAYER_A, EXTRA_B, EXTRA_C])); + let result = resolve_additional_addresses(true, &config, &relayer_addresses); + assert_eq!(result, vec![addr(EXTRA_B), addr(EXTRA_C)]); + } + + #[test] + fn returns_empty_when_all_filtered_out() { + let relayer_addresses = vec![addr(EXTRA_B)]; + let config = make_config(FROM, Some(vec![FROM, EXTRA_B])); + let result = resolve_additional_addresses(true, &config, &relayer_addresses); + assert!(result.is_empty()); + } +} diff --git a/crates/core/src/yaml.rs b/crates/core/src/yaml.rs index 5613a514..8ea99442 100644 --- a/crates/core/src/yaml.rs +++ b/crates/core/src/yaml.rs @@ -813,6 +813,8 @@ pub struct NetworkAutomaticTopUpConfig { pub native: Option, #[serde(skip_serializing_if = "Option::is_none", default)] pub erc20_tokens: Option>, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub additional_addresses: Option>, } impl NetworkAutomaticTopUpConfig { @@ -914,6 +916,15 @@ pub struct SafeProxyConfig { pub chain_id: ChainId, } +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct AdvancedConfig { + /// When enabled, allows the automatic top-up job to send funds to addresses + /// specified in `additional_addresses` that are not registered relayers. + /// Defaults to false when not specified. + #[serde(skip_serializing_if = "Option::is_none", default)] + pub allow_non_relayer_topups: Option, +} + #[derive(Debug, Serialize, Deserialize, Clone)] pub struct SetupConfig { pub name: String, @@ -929,6 +940,8 @@ pub struct SetupConfig { pub webhooks: Option>, #[serde(skip_serializing_if = "Option::is_none", default)] pub rate_limits: Option, + #[serde(skip_serializing_if = "Option::is_none", default)] + pub advanced: Option, } fn substitute_env_variables(contents: &str) -> Result { diff --git a/documentation/rrelayer/docs/pages/changelog.mdx b/documentation/rrelayer/docs/pages/changelog.mdx index 32801244..fd96bd7e 100644 --- a/documentation/rrelayer/docs/pages/changelog.mdx +++ b/documentation/rrelayer/docs/pages/changelog.mdx @@ -6,6 +6,8 @@ ### Features +- feat: add `allow_non_relayer_topups` setting under `advanced` config (default: `false`) and `additional_addresses` field on automatic top-up config — when enabled, allows the automatic top-up job to send funds to arbitrary addresses specified in `additional_addresses` without requiring them to be registered relayers + --- ### Bug fixes diff --git a/documentation/rrelayer/docs/pages/config/advanced.mdx b/documentation/rrelayer/docs/pages/config/advanced.mdx new file mode 100644 index 00000000..7890746d --- /dev/null +++ b/documentation/rrelayer/docs/pages/config/advanced.mdx @@ -0,0 +1,71 @@ +# Advanced + +The `advanced` section in your `rrelayer.yaml` configuration file contains experimental or fine-tuning options. All options are optional and default to safe values when not specified. + +## Configuration + +```yaml [rrelayer.yaml] +advanced: // [!code focus] + allow_non_relayer_topups: true // [!code focus] +``` + +## Options + +### `allow_non_relayer_topups` - optional (default: `false`) + +When enabled, allows the automatic top-up job to send funds to addresses specified in `additional_addresses` on network automatic top-up configs, even if those addresses are not registered relayers. + +**What this solves:** + +By default, the automatic top-up system only sends funds to registered relayer addresses as a security measure. If you need to top up external wallets, contracts, or other services, enabling this option together with `additional_addresses` allows you to do so. + +**Behavior when enabled (`true`):** + +- Addresses listed in `additional_addresses` on any network's `automatic_top_up` config are included in the top-up cycle +- These addresses skip the relayer validation check +- The `from` address and any addresses already in the relayer list are automatically excluded from `additional_addresses` + +**Behavior when disabled (`false`) or not set:** + +- `additional_addresses` entries are ignored +- Only registered relayer addresses receive automatic top-ups + +:::warning +This bypasses the security check that ensures top-up targets are registered relayers. Only use this with addresses you trust. +::: + +### Example + +```yaml [rrelayer.yaml] +name: my-rrelayer +description: "production rrelayer" +api_config: + port: 3000 + authentication_username: ${RRELAYER_AUTH_USERNAME} + authentication_password: ${RRELAYER_AUTH_PASSWORD} +signing_provider: + aws_kms: + region: "eu-west-1" +networks: + - name: "ethereum" + chain_id: 1 + provider_urls: + - "${ETHEREUM_RPC}" + block_explorer_url: "https://etherscan.io" + automatic_top_up: + - from: + relayer: + address: "0x33993A4F4AA617DA4558A0CFD0C39A7989B67720" + relayers: "*" + native: + min_balance: "0.1" + top_up_amount: "0.5" + additional_addresses: + - "0xABCDEF1234567890ABCDEF1234567890ABCDEF12" +advanced: // [!code focus] + allow_non_relayer_topups: true // [!code focus] +``` + +:::info +The `advanced` section is entirely optional. If omitted, all options default to their safe values. +::: \ No newline at end of file diff --git a/documentation/rrelayer/docs/pages/config/index.mdx b/documentation/rrelayer/docs/pages/config/index.mdx index 58c5c29a..23660535 100644 --- a/documentation/rrelayer/docs/pages/config/index.mdx +++ b/documentation/rrelayer/docs/pages/config/index.mdx @@ -92,6 +92,8 @@ rate_limits: interval: 'minute' transactions: 3 signing_operations: 3 +advanced: + allow_non_relayer_topups: true ``` ### Environment Variables diff --git a/documentation/rrelayer/docs/pages/config/networks/automatic-top-up.mdx b/documentation/rrelayer/docs/pages/config/networks/automatic-top-up.mdx index 5502b8dc..e78ad9ee 100644 --- a/documentation/rrelayer/docs/pages/config/networks/automatic-top-up.mdx +++ b/documentation/rrelayer/docs/pages/config/networks/automatic-top-up.mdx @@ -302,6 +302,58 @@ networks: // [!code focus] decimals: 18 # [!code focus] ``` +### additional_addresses - optional + +If you need to top up addresses that are **not** registered relayers (e.g. external wallets, contracts, or other services), +you can use `additional_addresses` together with the `allow_non_relayer_topups` advanced config flag. + +These addresses are processed separately from the relayer addresses and skip the relayer validation check. + +:::warning +This bypasses the security check that ensures top-up targets are registered relayers. Only use this with addresses you trust. +::: + +```yaml [rrelayer.yaml] +name: first-rrelayer +description: "my first rrelayer" +api_config: + port: 3000 + authentication_username: ${RRELAYER_AUTH_USERNAME} + authentication_password: ${RRELAYER_AUTH_PASSWORD} +signing_provider: + aws_kms: + region: "eu-west-1" +advanced: // [!code focus] + allow_non_relayer_topups: true // [!code focus] +networks: // [!code focus] + - name: local_anvil + chain_id: 31337 + provider_urls: + - http://127.0.0.1:8545 + block_explorer_url: http://localhost:8545 + max_gas_price_multiplier: 4 + gas_bump_blocks_every: + slow: 10 + medium: 5 + fast: 4 + super_fast: 2 + automatic_top_up: // [!code focus] + - from: + relayer: + address: "0x33993A4F4AA617DA4558A0CFD0C39A7989B67720" + relayers: "*" + native: + min_balance: "0.1" + top_up_amount: "0.5" + additional_addresses: // [!code focus] + - "0xABCDEF1234567890ABCDEF1234567890ABCDEF12" // [!code focus] + - "0x1234567890ABCDEF1234567890ABCDEF12345678" // [!code focus] +``` + +:::info +`additional_addresses` requires `advanced.allow_non_relayer_topups: true` at the top level of your config. Without it, the additional addresses will be ignored. +::: + ## Multiple Top-up Configurations You can configure multiple top-up strategies on the same network. This is useful when you want different funding sources or strategies for different sets of relayers: diff --git a/documentation/rrelayer/vocs.config.tsx b/documentation/rrelayer/vocs.config.tsx index eb3f37a0..99c3217e 100644 --- a/documentation/rrelayer/vocs.config.tsx +++ b/documentation/rrelayer/vocs.config.tsx @@ -188,6 +188,7 @@ export default defineConfig({ }, { text: 'Webhooks', link: '/config/webhooks' }, { text: 'Rate limits', link: '/config/rate-limits' }, + { text: 'Advanced', link: '/config/advanced' }, ], }, {