Skip to content

Commit 51ced12

Browse files
authored
Merge pull request #930 from Stevieoche/fix/issues-884-887-886-885
refactor: replace magic literals with named constants and add is_deprecated view
2 parents c28197b + 27f860b commit 51ced12

2 files changed

Lines changed: 81 additions & 9 deletions

File tree

contracts/router-quote/src/lib.rs

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,15 @@ pub enum QuoteError {
9191
InvalidFeeTier = 9,
9292
}
9393

94+
// ── Constants ─────────────────────────────────────────────────────────────────
95+
96+
/// Basis-points denominator: 10000 bps = 100%.
97+
///
98+
/// Used in every fee validation (`fee_bps > BPS_DENOMINATOR`) and in the
99+
/// fee calculation (`fee_amount = amount_in * fee_bps / BPS_DENOMINATOR`).
100+
/// A single source of truth prevents the literal from diverging across sites.
101+
const BPS_DENOMINATOR: u32 = 10_000;
102+
94103
// ── Contract ──────────────────────────────────────────────────────────────────
95104

96105
/// Maximum number of routes that can be tracked in the configured-routes index.
@@ -125,7 +134,7 @@ impl RouterQuote {
125134
return Err(QuoteError::AlreadyInitialized);
126135
}
127136

128-
if default_fee_bps > 10000 {
137+
if default_fee_bps > BPS_DENOMINATOR {
129138
return Err(QuoteError::InvalidFeeBps);
130139
}
131140

@@ -168,7 +177,7 @@ impl RouterQuote {
168177
caller.require_auth();
169178
router_common::require_admin_simple!(&env, &caller, &DataKey::Admin, QuoteError)?;
170179

171-
if fee_bps > 10000 {
180+
if fee_bps > BPS_DENOMINATOR {
172181
return Err(QuoteError::InvalidFeeBps);
173182
}
174183

@@ -241,7 +250,7 @@ impl RouterQuote {
241250
if tier.min_amount < 0 {
242251
return Err(QuoteError::InvalidFeeTier);
243252
}
244-
if tier.fee_bps > 10000 {
253+
if tier.fee_bps > BPS_DENOMINATOR {
245254
return Err(QuoteError::InvalidFeeBps);
246255
}
247256

@@ -346,11 +355,11 @@ impl RouterQuote {
346355
let fee_bps =
347356
Self::resolve_route_fee_bps(env.clone(), request.route.clone(), request.amount_in)?;
348357

349-
// Calculate fee: fee_amount = amount_in * fee_bps / 10000
358+
// Calculate fee: fee_amount = amount_in * fee_bps / BPS_DENOMINATOR
350359
let fee_amount = request
351360
.amount_in
352361
.checked_mul(fee_bps as i128)
353-
.and_then(|v| v.checked_div(10000))
362+
.and_then(|v| v.checked_div(BPS_DENOMINATOR as i128))
354363
.ok_or(QuoteError::ArithmeticOverflow)?;
355364

356365
// Calculate output: amount_out = amount_in - fee_amount
@@ -509,7 +518,7 @@ impl RouterQuote {
509518
caller.require_auth();
510519
router_common::require_admin_simple!(&env, &caller, &DataKey::Admin, QuoteError)?;
511520

512-
if fee_bps > 10000 {
521+
if fee_bps > BPS_DENOMINATOR {
513522
return Err(QuoteError::InvalidFeeBps);
514523
}
515524

contracts/router-registry/src/lib.rs

Lines changed: 66 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,18 @@ pub enum RegistryError {
7676
InvalidHealthFn = 12,
7777
}
7878

79+
// ── Constants ─────────────────────────────────────────────────────────────────
80+
81+
/// Maximum byte length of a semver constraint string accepted by
82+
/// [`RouterRegistry::get_latest_with_constraint`].
83+
///
84+
/// `constraint_str_buf` copies the constraint into a fixed-size stack buffer
85+
/// of exactly this size. Both the length guard (`if len > MAX_CONSTRAINT_LEN`)
86+
/// and the buffer declaration (`[0u8; MAX_CONSTRAINT_LEN]`) must use this
87+
/// constant so they stay in sync — a mismatch would cause a panic on
88+
/// out-of-bounds indexing at runtime.
89+
const MAX_CONSTRAINT_LEN: usize = 32;
90+
7991
// ── Contract ──────────────────────────────────────────────────────────────────
8092

8193
#[contract]
@@ -297,6 +309,27 @@ impl RouterRegistry {
297309
.ok_or(RegistryError::NotFound)
298310
}
299311

312+
/// Check whether a specific version of a contract has been deprecated.
313+
///
314+
/// This is a lightweight view alternative to calling [`get`] and reading
315+
/// `.deprecated` off the full [`ContractEntry`]. Callers who only need to
316+
/// know the deprecation status (e.g. a UI rendering a deprecation badge)
317+
/// can avoid fetching and discarding the rest of the entry.
318+
///
319+
/// # Arguments
320+
/// * `env` - The Soroban environment.
321+
/// * `name` - The human-readable name of the contract.
322+
/// * `version` - The exact version number to query.
323+
///
324+
/// # Returns
325+
/// `true` if the entry is deprecated, `false` otherwise.
326+
///
327+
/// # Errors
328+
/// * [`RegistryError::NotFound`] — if no entry exists for `(name, version)`.
329+
pub fn is_deprecated(env: Env, name: String, version: u32) -> Result<bool, RegistryError> {
330+
Self::get(env, name, version).map(|entry| entry.deprecated)
331+
}
332+
300333
/// Get the latest (highest version) non-deprecated entry for a name.
301334
///
302335
/// Iterates registered versions in descending order and returns the first
@@ -794,12 +827,12 @@ impl RouterRegistry {
794827
/// `soroban_sdk::String` doesn't implement `Display`/`ToString` on the wasm
795828
/// target, so constraint strings (which are always short) are read via
796829
/// `copy_into_slice` instead of allocating.
797-
fn constraint_str_buf(constraint: &String) -> Result<([u8; 32], usize), RegistryError> {
830+
fn constraint_str_buf(constraint: &String) -> Result<([u8; MAX_CONSTRAINT_LEN], usize), RegistryError> {
798831
let len = constraint.len() as usize;
799-
if len > 32 {
832+
if len > MAX_CONSTRAINT_LEN {
800833
return Err(RegistryError::InvalidConstraint);
801834
}
802-
let mut buf = [0u8; 32];
835+
let mut buf = [0u8; MAX_CONSTRAINT_LEN];
803836
constraint.copy_into_slice(&mut buf[..len]);
804837
Ok((buf, len))
805838
}
@@ -938,6 +971,36 @@ mod tests {
938971
assert!(!entry.deprecated);
939972
}
940973

974+
#[test]
975+
fn test_is_deprecated_returns_false_for_fresh_registration() {
976+
let (env, admin, client) = setup();
977+
let name = String::from_str(&env, "oracle");
978+
let addr = Address::generate(&env);
979+
client.register(&admin, &name, &addr, &1);
980+
assert!(!client.is_deprecated(&name, &1));
981+
}
982+
983+
#[test]
984+
fn test_is_deprecated_returns_true_after_deprecate() {
985+
let (env, admin, client) = setup();
986+
let name = String::from_str(&env, "oracle");
987+
let addr = Address::generate(&env);
988+
client.register(&admin, &name, &addr, &1);
989+
client.deprecate(&admin, &name, &1, &None::<String>);
990+
assert!(client.is_deprecated(&name, &1));
991+
}
992+
993+
#[test]
994+
fn test_is_deprecated_returns_not_found_for_unregistered_version() {
995+
let (env, admin, client) = setup();
996+
let name = String::from_str(&env, "oracle");
997+
let addr = Address::generate(&env);
998+
client.register(&admin, &name, &addr, &1);
999+
// version 99 was never registered
1000+
let result = client.try_is_deprecated(&name, &99);
1001+
assert_eq!(result, Err(Ok(RegistryError::NotFound)));
1002+
}
1003+
9411004
#[test]
9421005
fn test_get_latest() {
9431006
let (env, admin, client) = setup();

0 commit comments

Comments
 (0)