diff --git a/crates/starknet-types-core/Cargo.toml b/crates/starknet-types-core/Cargo.toml index 3e59bfa..c214156 100644 --- a/crates/starknet-types-core/Cargo.toml +++ b/crates/starknet-types-core/Cargo.toml @@ -24,7 +24,7 @@ serde = { version = "1", optional = true, default-features = false, features = [ "alloc", "derive" ] } lambdaworks-crypto = { version = "0.12.0", default-features = false, optional = true } -parity-scale-codec = { version = "3.6", default-features = false, optional = true } +parity-scale-codec = { version = "3.6", default-features = false, features = ["derive"], optional = true } lazy_static = { version = "1.5", default-features = false, optional = true } zeroize = { version = "1.8.1", default-features = false, optional = true } subtle = { version = "2.6.1", default-features = false, optional = true } diff --git a/crates/starknet-types-core/src/contract_address.rs b/crates/starknet-types-core/src/contract_address.rs new file mode 100644 index 0000000..77de96a --- /dev/null +++ b/crates/starknet-types-core/src/contract_address.rs @@ -0,0 +1,178 @@ +//! A starknet contract address +//! +//! In starknet valid contract addresses exists as a subset of the type `Felt`. +//! Therefore some checks must be done in order to produce protocol valid addresses. +//! This module provides this logic as a type `ContractAddress`, that can garantee the validity of the address. +//! It also comes with some quality of life methods. + +use core::str::FromStr; + +use crate::felt::Felt; + +#[repr(transparent)] +#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +#[cfg_attr(feature = "serde", serde(transparent))] +#[cfg_attr( + feature = "parity-scale-codec", + derive(parity_scale_codec::Encode, parity_scale_codec::Decode) +)] +pub struct ContractAddress(Felt); + +impl core::fmt::Display for ContractAddress { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl AsRef for ContractAddress { + fn as_ref(&self) -> &Felt { + &self.0 + } +} + +impl From for Felt { + fn from(value: ContractAddress) -> Self { + value.0 + } +} + +#[derive(Debug, Copy, Clone)] +/// In Starknet, contract addresses must follow specific constraints to be considered valid: +/// - They must be greater than or equal to 2, as addresses 0 and 1 are reserved for system use: +/// * 0x0 acts as the default caller address for external calls and has no storage +/// * 0x1 functions as a storage space for block mapping [link](https://docs.starknet.io/architecture-and-concepts/network-architecture/starknet-state/#special_addresses) +/// - They must be less than 2^251 (0x800000000000000000000000000000000000000000000000000000000000000) +/// +/// Making the valid addressabe range be [2, 2^251) +pub enum ContactAddressFromFeltError { + Zero, + One, + TooBig, +} + +impl core::fmt::Display for ContactAddressFromFeltError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + ContactAddressFromFeltError::Zero => { + write!( + f, + "address 0x0 is reserved as the default caller address and has no storage" + ) + } + ContactAddressFromFeltError::One => { + write!( + f, + "address 0x1 is reserved as storage space for block mapping" + ) + } + ContactAddressFromFeltError::TooBig => { + write!(f, "the highest possible address is 2^251 - 1") + } + } + } +} + +#[cfg(feature = "std")] +impl std::error::Error for ContactAddressFromFeltError {} + +const ADDRESS_UPPER_BOUND: Felt = + Felt::from_hex_unchecked("0x800000000000000000000000000000000000000000000000000000000000000"); + +/// Validates that a Felt value represents a valid Starknet contract address. +/// +/// This validation is critical for preventing funds from being sent to invalid addresses, +/// which would result in permanent loss. +impl Felt { + pub fn is_valid_contract_address(&self) -> bool { + self >= &Felt::from(2u64) && self < &ADDRESS_UPPER_BOUND + } +} + +impl TryFrom for ContractAddress { + type Error = ContactAddressFromFeltError; + + fn try_from(value: Felt) -> Result { + if value == Felt::ZERO { + return Err(ContactAddressFromFeltError::Zero); + } + if value == Felt::ONE { + return Err(ContactAddressFromFeltError::One); + } + if value >= ADDRESS_UPPER_BOUND { + return Err(ContactAddressFromFeltError::TooBig); + } + + Ok(ContractAddress(value)) + } +} + +#[derive(Debug)] +pub enum ContractAddressFromStrError { + BadFelt(::Err), + BadAddress(ContactAddressFromFeltError), +} + +impl core::fmt::Display for ContractAddressFromStrError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + ContractAddressFromStrError::BadFelt(e) => write!(f, "invalid felt string: {e}"), + ContractAddressFromStrError::BadAddress(e) => write!(f, "invalid address value: {e}"), + } + } +} + +impl FromStr for ContractAddress { + type Err = ContractAddressFromStrError; + + fn from_str(s: &str) -> Result { + let felt = Felt::from_str(s).map_err(ContractAddressFromStrError::BadFelt)?; + let contract_address = + ContractAddress::try_from(felt).map_err(ContractAddressFromStrError::BadAddress)?; + + Ok(contract_address) + } +} + +impl ContractAddress { + pub const fn from_hex_unchecked(s: &str) -> ContractAddress { + let felt = Felt::from_hex_unchecked(s); + + ContractAddress(felt) + } +} + +#[cfg(test)] +mod test { + #[cfg(feature = "alloc")] + pub extern crate alloc; + use alloc::format; + use proptest::prelude::*; + + use crate::{ + contract_address::{ContractAddress, ADDRESS_UPPER_BOUND}, + felt::Felt, + }; + + #[test] + fn basic_values() { + assert!(ContractAddress::try_from(Felt::ZERO).is_err()); + assert!(ContractAddress::try_from(Felt::ONE).is_err()); + assert!(ContractAddress::try_from(ADDRESS_UPPER_BOUND).is_err()); + + let felt = Felt::TWO; + let contract_address = ContractAddress::try_from(felt).unwrap(); + assert_eq!(Felt::from(contract_address), felt); + } + + proptest! { + #[test] + fn is_valid_match_try_into(ref x in any::()) { + if x.is_valid_contract_address() { + prop_assert!(ContractAddress::try_from(*x).is_ok()); + } else { + prop_assert!(ContractAddress::try_from(*x).is_err()); + } + } + } +} diff --git a/crates/starknet-types-core/src/lib.rs b/crates/starknet-types-core/src/lib.rs index bda7d42..e814d93 100644 --- a/crates/starknet-types-core/src/lib.rs +++ b/crates/starknet-types-core/src/lib.rs @@ -8,6 +8,7 @@ pub mod hash; pub mod felt; pub mod qm31; +pub mod contract_address; #[cfg(any(feature = "std", feature = "alloc"))] pub mod short_string; pub mod u256;