From 7063cebcf40c044ec011b5ed5c7625177f700179 Mon Sep 17 00:00:00 2001 From: phertyameen Date: Wed, 25 Mar 2026 12:36:47 +0100 Subject: [PATCH] feat(security): add nonce-based replay protection --- apps/rust/property-token/src/lib.rs | 18 ++++++++++ apps/rust/property-token/src/msg.rs | 23 ++++++++++++ apps/rust/property-token/src/security.rs | 29 +++++++++++++++ apps/rust/property-token/src/state.rs | 4 +++ .../rust/property-token/tests/replay_tests.rs | 35 +++++++++++++++++++ 5 files changed, 109 insertions(+) create mode 100644 apps/rust/property-token/src/lib.rs create mode 100644 apps/rust/property-token/src/msg.rs create mode 100644 apps/rust/property-token/src/security.rs create mode 100644 apps/rust/property-token/src/state.rs create mode 100644 apps/rust/property-token/tests/replay_tests.rs diff --git a/apps/rust/property-token/src/lib.rs b/apps/rust/property-token/src/lib.rs new file mode 100644 index 0000000..0dfbf93 --- /dev/null +++ b/apps/rust/property-token/src/lib.rs @@ -0,0 +1,18 @@ +use crate::security::prevent_replay; + +pub fn execute_set_metadata( + mut deps: DepsMut, + env: Env, + info: MessageInfo, + token_id: String, + metadata: PropertyMetadata, + auth: Auth, +) -> Result { + prevent_replay(&mut deps, &env, &info, auth.nonce, auth.expires_at)?; + + validate_metadata(&metadata)?; + + METADATA.save(deps.storage, &token_id, &metadata)?; + + Ok(Response::new().add_attribute("action", "set_metadata")) +} \ No newline at end of file diff --git a/apps/rust/property-token/src/msg.rs b/apps/rust/property-token/src/msg.rs new file mode 100644 index 0000000..079a306 --- /dev/null +++ b/apps/rust/property-token/src/msg.rs @@ -0,0 +1,23 @@ +#[cw_serde] +pub struct Auth { + pub nonce: u64, + pub expires_at: Option, +} + +#[cw_serde] +pub enum ExecuteMsg { + SetMetadata { + token_id: String, + metadata: PropertyMetadata, + auth: Auth, + }, + UpdateMetadata { + token_id: String, + metadata: PropertyMetadata, + auth: Auth, + }, + Batch { + msgs: Vec, + auth: Auth, + }, +} \ No newline at end of file diff --git a/apps/rust/property-token/src/security.rs b/apps/rust/property-token/src/security.rs new file mode 100644 index 0000000..d035454 --- /dev/null +++ b/apps/rust/property-token/src/security.rs @@ -0,0 +1,29 @@ +use cosmwasm_std::{Env, MessageInfo, StdError, StdResult}; +use crate::state::USED_NONCES; + +pub fn prevent_replay( + deps: &mut cosmwasm_std::DepsMut, + env: &Env, + info: &MessageInfo, + nonce: u64, + expires_at: Option, +) -> StdResult<()> { + let sender = info.sender.as_str(); + + // 1. Check expiry + if let Some(expiry) = expires_at { + if env.block.time.seconds() > expiry { + return Err(StdError::generic_err("Message expired")); + } + } + + // 2. Check nonce already used + if USED_NONCES.has(deps.storage, (sender, nonce)) { + return Err(StdError::generic_err("Replay detected")); + } + + // 3. Mark nonce as used + USED_NONCES.save(deps.storage, (sender, nonce), &true)?; + + Ok(()) +} \ No newline at end of file diff --git a/apps/rust/property-token/src/state.rs b/apps/rust/property-token/src/state.rs new file mode 100644 index 0000000..a7bb33b --- /dev/null +++ b/apps/rust/property-token/src/state.rs @@ -0,0 +1,4 @@ +use cw_storage_plus::Map; + +// (sender, nonce) -> used +pub const USED_NONCES: Map<(&str, u64), bool> = Map::new("used_nonces"); \ No newline at end of file diff --git a/apps/rust/property-token/tests/replay_tests.rs b/apps/rust/property-token/tests/replay_tests.rs new file mode 100644 index 0000000..23acda5 --- /dev/null +++ b/apps/rust/property-token/tests/replay_tests.rs @@ -0,0 +1,35 @@ +#[test] +fn test_replay_attack_prevented() { + let mut deps = mock_dependencies(); + + let auth = Auth { + nonce: 1, + expires_at: None, + }; + + let metadata = mock_metadata(); + + // First call should succeed + let res1 = execute_set_metadata( + deps.as_mut(), + mock_env(), + mock_info("user", &[]), + "token1".to_string(), + metadata.clone(), + auth.clone(), + ); + + assert!(res1.is_ok()); + + // Replay same nonce should fail + let res2 = execute_set_metadata( + deps.as_mut(), + mock_env(), + mock_info("user", &[]), + "token1".to_string(), + metadata, + auth, + ); + + assert!(res2.is_err()); +} \ No newline at end of file