diff --git a/docs/api-documentation.md b/docs/api-documentation.md index f6cee062..2e9d8c8b 100644 --- a/docs/api-documentation.md +++ b/docs/api-documentation.md @@ -87,7 +87,7 @@ curl "https://forest-explorer.chainsafe.dev/api/claim_token?faucet_info=Calibnet **Response:** ```bash -ServerError|Invalid address: Not a valid Testnet address +ServerError|Invalid address - failed to parse: Not a valid Testnet address ``` #### 429 Too Many Requests @@ -241,13 +241,13 @@ curl "https://forest-explorer.chainsafe.dev/api/claim_token_all?address=invalida { "faucet_info": "CalibnetUSDFC", "error": { - "ServerError": "Invalid address: Not a valid Testnet address" + "ServerError": "Invalid address - failed to parse: Not a valid Testnet address" } }, { "faucet_info": "CalibnetFIL", "error": { - "ServerError": "Invalid address: Not a valid Testnet address" + "ServerError": "Invalid address - failed to parse: Not a valid Testnet address" } } ] @@ -291,6 +291,8 @@ curl "https://forest-explorer.chainsafe.dev/api/claim_token_all?address=0xAe9C4b - Each address is subject to rate limiting to prevent abuse. - This API only distributes Calibnet `tFIL` and `tUSDFC` tokens. +- ID address or its corresponding eth style `0xff…ID` address are restricted to + claim `tUSDFC` tokens. ## Rate Limits diff --git a/e2e/test_claim_token_api_config.js b/e2e/test_claim_token_api_config.js index 831d90e4..22a24c49 100644 --- a/e2e/test_claim_token_api_config.js +++ b/e2e/test_claim_token_api_config.js @@ -136,7 +136,21 @@ export const TEST_SCENARIOS = { name: 'Invalid address format for CalibnetUSDFC', faucet_info: FaucetTypes.CalibnetUSDFC, address: TEST_ADDRESSES.T1_FORMAT_ADDRESS, - expectedStatus: STATUS_CODES.INTERNAL_SERVER_ERROR, + expectedStatus: STATUS_CODES.BAD_REQUEST, + expectedErrorContains: 'invalid address' + }, + { + name: 'CalibnetUSDFC (0xff...ID) - restricted address (RESTRICTED)', + faucet_info: FaucetTypes.CalibnetUSDFC, + address: TEST_ADDRESSES.ETH_ID_CORRESPONDING, + expectedStatus: STATUS_CODES.BAD_REQUEST, + expectedErrorContains: 'invalid address' + }, + { + name: 'CalibnetUSDFC (t0) - restricted address (RESTRICTED)', + faucet_info: FaucetTypes.CalibnetUSDFC, + address: TEST_ADDRESSES.T0_ADDRESS, + expectedStatus: STATUS_CODES.BAD_REQUEST, expectedErrorContains: 'invalid address' } ], @@ -186,19 +200,6 @@ export const TEST_SCENARIOS = { faucet_info: FaucetTypes.CalibnetUSDFC, address: TEST_ADDRESSES.T410_ADDRESS, expectedStatus: STATUS_CODES.TOO_MANY_REQUESTS - }, - { - name: 'CalibnetUSDFC (t0) - RATE LIMITED (within CalibnetUSDFC cooldown)', - faucet_info: FaucetTypes.CalibnetUSDFC, - address: TEST_ADDRESSES.T0_ADDRESS, - expectedStatus: STATUS_CODES.TOO_MANY_REQUESTS - }, - // CalibnetUSDFC doesn't support the t1 format address - { - name: 'CalibnetUSDFC (ID) - RATE LIMITED (within CalibnetUSDFC cooldown)', - faucet_info: FaucetTypes.CalibnetUSDFC, - address: TEST_ADDRESSES.ETH_ID_CORRESPONDING, - expectedStatus: STATUS_CODES.TOO_MANY_REQUESTS } ], @@ -295,44 +296,6 @@ export const TEST_SCENARIOS = { expectedStatus: STATUS_CODES.TOO_MANY_REQUESTS, waitBefore: 0, // No wait needed, should be capped, already from the previous step walletCapErrorResponse: true, - }, - - // TODO(forest-explorer): https://github.com/ChainSafe/forest-explorer/issues/335 - // Token sent to the t0 and it's eth mapping address 0x - // are not accessible. Hence commenting out the following - // test cases. - // === CalibnetUSDFC ID Wallet (fresh wallet, 0 transactions) === - // { - // name: 'CalibnetUSDFC (ID) - 1st SUCCESS (fresh wallet)', - // faucet_info: FaucetTypes.CalibnetUSDFC, - // address: TEST_ADDRESSES.ETH_ID_CORRESPONDING, - // expectedStatus: STATUS_CODES.SUCCESS, - // waitBefore: 65, // Wait for cooldown from the previous test group to expire - // walletCapErrorResponse: false, - // }, - // { - // name: 'CalibnetUSDFC (ID) - 2nd SUCCESS (reaches cap)', - // faucet_info: FaucetTypes.CalibnetUSDFC, - // address: TEST_ADDRESSES.ETH_ID_CORRESPONDING, - // expectedStatus: STATUS_CODES.SUCCESS, - // waitBefore: 65, // Wait for cooldown from its own 1st transaction - // walletCapErrorResponse: false, - // }, - // { - // name: 'CalibnetUSDFC (ID) - 3rd attempt (WALLET CAPPED)', - // faucet_info: FaucetTypes.CalibnetUSDFC, - // address: TEST_ADDRESSES.ETH_ID_CORRESPONDING, - // expectedStatus: STATUS_CODES.TOO_MANY_REQUESTS, - // waitBefore: 65, // Wait for cooldown from its own 2nd transaction - // walletCapErrorResponse: true, - // }, - // { - // name: 'CalibnetUSDFC (t0) - check equivalence (WALLET CAPPED)', - // faucet_info: FaucetTypes.CalibnetUSDFC, - // address: TEST_ADDRESSES.T0_ADDRESS, // This is the same wallet as the ID address - // expectedStatus: STATUS_CODES.TOO_MANY_REQUESTS, - // waitBefore: 0, // No wait needed, should be capped already - // walletCapErrorResponse: true, - // }, + } ] }; diff --git a/src/faucet/server_api.rs b/src/faucet/server_api.rs index c13bc45f..cfc1e330 100644 --- a/src/faucet/server_api.rs +++ b/src/faucet/server_api.rs @@ -222,7 +222,7 @@ pub async fn claim_token( let network = faucet_info.network(); set_current_network(network); - let recipient = parse_and_validate_address(&address, network)?; + let recipient = parse_and_validate_address(&address, faucet_info)?; let rpc = Provider::from_network(network); let from = faucet_address(faucet_info) .await? @@ -271,18 +271,36 @@ pub async fn claim_token_all(address: String) -> Result, Serv Ok(results) } +/// Checks if the provided address is valid for the faucet, ensuring invalid addresses are rejected. +#[cfg(feature = "ssr")] +fn check_valid_address(address: Address, faucet_info: FaucetInfo) -> Result<(), ServerFnError> { + use fvm_shared::address::Protocol; + + if matches!(faucet_info, FaucetInfo::CalibnetUSDFC) + && (address.protocol() != Protocol::Delegated) + { + log::error!("Invalid address: {:?}", address); + set_response_status(StatusCode::BAD_REQUEST); + return Err(ServerFnError::ServerError("Invalid address: Only Ethereum-compatible addresses (delegated t4 addresses or native Ethereum 0x addresses) are allowed for Calibnet USDFC token claims.".to_string())); + } + Ok(()) +} + #[cfg(feature = "ssr")] fn parse_and_validate_address( address: &str, - network: fvm_shared::address::Network, + faucet_info: FaucetInfo, ) -> Result { - match crate::utils::address::parse_address(address, network) { - Ok(addr) => Ok(addr), + match crate::utils::address::parse_address(address, faucet_info.network()) { + Ok(addr) => { + check_valid_address(addr, faucet_info)?; + Ok(addr) + } Err(e) => { - log::error!("Invalid address: {}", e); + log::error!("Invalid address - failed to parse: {}", e); set_response_status(StatusCode::BAD_REQUEST); Err(ServerFnError::ServerError(format!( - "Invalid address: {}", + "Invalid address - failed to parse: {}", e ))) } @@ -403,5 +421,81 @@ fn handle_faucet_error(err: FaucetError) -> ServerFnError { #[cfg(feature = "ssr")] fn set_response_status(status: StatusCode) { - leptos::prelude::expect_context::().set_status(status); + if let Some(res) = leptos::context::use_context::() { + res.set_status(status) + } +} + +#[cfg(all(test, feature = "ssr"))] +mod tests { + use crate::faucet::server_api::*; + + fn assert_valid_address(address: &str, faucet: FaucetInfo) { + let network = faucet.network(); + let addr = crate::utils::address::parse_address(address, network).unwrap(); + assert!(check_valid_address(addr, faucet).is_ok()); + assert!(parse_and_validate_address(address, faucet).is_ok()); + } + + fn assert_invalid_address(address: &str, faucet: FaucetInfo) { + let network = faucet.network(); + let addr = crate::utils::address::parse_address(address, network).unwrap(); + assert!(check_valid_address(addr, faucet).is_err()); + assert!(parse_and_validate_address(address, faucet).is_err()); + } + + #[test] + fn test_check_valid_address_mainnet() { + let addresses = [ + "f03603846", + "f1rgci272nfk4k6cpyejepzv4xstpejjckldlzidy", + "f2yjb6dq3jggychgnuhevcwe7ehv3ot2rkhkbk4qy", + "f3s5kg6rehbbmgvngpec6b7m4uxmwbscdafn2pvtrrp65wbgjuymrr2z6qbkqiunkyjul6b62buqk76q47cjeq", + "f410fv2oexfiizeuzm3xtoie3gnxfpfwwglg4q3dgxki", + "0xff0000000000000000000000000000000036fd86", + "0xAe9C4b9508c929966ef37209b336E5796D632CDc", + ]; + for addr in addresses.iter() { + assert_valid_address(addr, FaucetInfo::MainnetFIL); + } + } + + #[test] + fn test_check_valid_address_calibnet() { + let addresses = [ + "t03603846", + "t1rgci272nfk4k6cpyejepzv4xstpejjckldlzidy", + "t2yjb6dq3jggychgnuhevcwe7ehv3ot2rkhkbk4qy", + "t3s5kg6rehbbmgvngpec6b7m4uxmwbscdafn2pvtrrp65wbgjuymrr2z6qbkqiunkyjul6b62buqk76q47cjeq", + "t410fv2oexfiizeuzm3xtoie3gnxfpfwwglg4q3dgxki", + "0xff0000000000000000000000000000000036f672", + "0xAe9C4b9508c929966ef37209b336E5796D632CDc", + ]; + for addr in addresses.iter() { + assert_valid_address(addr, FaucetInfo::CalibnetFIL); + } + } + + #[test] + fn test_check_valid_address_calibnet_usdfc() { + let valid_addresses = [ + "0xAe9C4b9508c929966ef37209b336E5796D632CDc", + "t410fv2oexfiizeuzm3xtoie3gnxfpfwwglg4q3dgxki", + ]; + let invalid_addresses = [ + "t03603846", + "t1rgci272nfk4k6cpyejepzv4xstpejjckldlzidy", + "t2yjb6dq3jggychgnuhevcwe7ehv3ot2rkhkbk4qy", + "t3s5kg6rehbbmgvngpec6b7m4uxmwbscdafn2pvtrrp65wbgjuymrr2z6qbkqiunkyjul6b62buqk76q47cjeq", + "0xff0000000000000000000000000000000036f672", + ]; + + for addr in valid_addresses.iter() { + assert_valid_address(addr, FaucetInfo::CalibnetUSDFC); + } + + for addr in invalid_addresses.iter() { + assert_invalid_address(addr, FaucetInfo::CalibnetUSDFC); + } + } } diff --git a/src/utils/address.rs b/src/utils/address.rs index 3e7ff11e..cd8c0825 100644 --- a/src/utils/address.rs +++ b/src/utils/address.rs @@ -63,7 +63,7 @@ pub fn parse_address(raw: &str, n: Network) -> anyhow::Result
{ s.chars().skip(2).all(|c| c.is_ascii_hexdigit()), "Invalid characters in address" ); - if let Some(id) = s.strip_prefix("0xff") { + if let Some(id) = s.strip_prefix("0xff0000000000000000000000") { let id = u64::from_str_radix(id, 16)?; Ok(Address::new_id(id)) } else {