Skip to content

Conversation

@hlgltvnnk
Copy link
Collaborator

@hlgltvnnk hlgltvnnk commented Sep 30, 2025

  • Nonce versioning
  • Salted nonces
  • State versioning

Summary by CodeRabbit

  • New Features

    • Salt management: rotate/invalidate salts, query current/validity, SaltRotation events; salted, expirable and versioned nonces with pre-commit validation.
  • Changes

    • Cleanup API renamed to cleanup_nonces and clears by nonce prefix; salt checks added to intent/commit and simulation output exposes current_salt; new nonce/salt error variants; storage versioning and migration; contract storage refactored with role additions.
  • Tests

    • Expanded tests for salt flows, nonce commit/cleanup, and fee management.
  • Documentation

    • CHANGELOG updated for v0.3.0.
  • Chores

    • Package version bumped to 0.3.0.

@coderabbitai
Copy link

coderabbitai bot commented Sep 30, 2025

Walkthrough

Adds versioned, salted, and expirable nonces with salt rotation and validation; verifies nonce salt and deadline before commit; replaces per-nonce cleanup with prefix-based cleanup; introduces SaltManager API/events and SaltRegistry state; migrates contract storage to a versioned form and updates tests and release to v0.3.0.

Changes

Cohort / File(s) Summary
Nonce subsystem (new modular layout)
core/src/nonce/mod.rs, core/src/nonce/expirable.rs, core/src/nonce/salted.rs, core/src/nonce/versioned.rs, core/src/nonce.rs
Introduce modular nonce subsystem: Nonce alias, Nonces<T> bitmap wrapper, ExpirableNonce<T>, Salt/SaltRegistry/SaltedNonce, VersionedNonce with magic prefix; remove monolithic core/src/nonce.rs.
Engine: intent nonce verification
core/src/engine/mod.rs
Add private Engine::verify_intent_nonce and call it before committing nonces; import Deadline/Nonce/SaltedNonce/VersionedNonce; error variants for invalid salt/deadline/expiry.
State view & cleanup API
core/src/engine/state/mod.rs, core/src/engine/state/cached.rs, core/src/engine/state/deltas.rs
Add StateView::is_valid_salt; rename cleanup_expired_noncescleanup_nonce_by_prefix with NoncePrefix/AccountIdRef API and Result<bool> return; adapt CachedState/CachedAccount/Deltas to new API and delegate salt validation.
Errors & events
core/src/error.rs, core/src/accounts.rs, core/src/events.rs
Add DefuseError variants (InvalidNonce, InvalidSalt, SaltGenerationFailed); add SaltRotationEvent struct and DefuseEvent::SaltRotation variant.
Contract storage & versioning
defuse/src/contract/mod.rs, defuse/src/contract/versioned/*, defuse/src/contract/state/*
Introduce versioned contract storage (MaybeVersionedContractStorage, ContractStorageV0), migration helpers; split Contract into storage/runtime wrapper; add SaltRegistry to state and Prefix::Salts.
Salt management API & impl
defuse/src/salts.rs, defuse/src/contract/salts.rs, defuse/src/intents.rs, defuse/src/lib.rs
Add SaltManager ext trait and salts module; implement SaltManager on Contract (update_current_salt, invalidate_salts, is_valid_salt, current_salt); make Intents depend on SaltManager.
Accounts & cleanup flow (contract-side)
defuse/src/accounts.rs, defuse/src/contract/accounts/*, defuse/src/contract/intents/state.rs
Rename cleanup public API: cleanup_expired_noncescleanup_nonces; per-account method clear_expired_noncecleanup_nonce_by_prefix; switch callers to prefix-based cleanup and update error/account handling.
Bitmap API
bitmap/src/lib.rs
Rename BitMap256::clear_by_prefixcleanup_by_prefix and use U248 alias for prefix parameter.
Contract internals & minor adjustments
defuse/src/contract/tokens/mod.rs, defuse/src/contract/events.rs
Adjust internal access to storage/runtime fields; remove PostponedMtBurnEvents::new() public constructor.
Tests & test helpers
tests/src/tests/defuse/accounts/*, tests/src/tests/defuse/state/*, tests/src/tests/defuse/state/extensions/*, tests/src/tests/defuse/mod.rs
Update tests to use salted/versioned/expirable nonces and salt flows; add SaltManagerExt and FeesManagerExt test helpers; rename cleanup helpers to cleanup_nonces.
CHANGELOG & version bump
CHANGELOG.md, defuse/Cargo.toml
Add v0.3.0 changelog entry and bump defuse package version to 0.3.0.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant Client
  participant Engine
  participant State
  participant Nonces

  User->>Client: submit signed intent(nonce, intent_deadline)
  Client->>Engine: execute_signed_intent(...)
  Engine->>Engine: verify_intent_nonce(nonce, intent_deadline)
  Engine->>Nonces: VersionedNonce::maybe_from(nonce)
  alt V1 (Salted & Expirable)
    Engine->>State: is_valid_salt(salt)?
    Engine-->>Client: Err(InvalidSalt) if false
    Engine->>Engine: check intent_deadline <= nonce.deadline
    Engine-->>Client: Err(DeadlineGreaterThanNonce) if violated
    Engine->>Engine: check nonce.deadline not expired
    Engine-->>Client: Err(NonceExpired) if expired
  else Legacy
    note right of Engine: legacy nonce — no salt/deadline checks
  end
  Engine->>State: commit_nonce(nonce)
  State->>Nonces: Nonces::commit(...)
  Engine-->>Client: Ok
Loading
sequenceDiagram
  autonumber
  actor Admin as DAO/SaltManager
  participant Contract
  participant Registry as SaltRegistry
  participant Events as DefuseEvent

  Admin->>Contract: update_current_salt() (1 yocto)
  Contract->>Registry: set_new()
  Registry-->>Contract: new current salt
  Contract->>Events: emit SaltRotationEvent{current, invalidated: {}}
  Contract-->>Admin: current_salt

  Admin->>Contract: invalidate_salts([s1,s2]) (1 yocto)
  loop each salt
    Contract->>Registry: is_valid(salt)?
    alt valid
      Contract->>Registry: invalidate(salt)
    else invalid
      Contract-->>Admin: Err(InvalidSalt)
    end
  end
  Contract->>Events: emit SaltRotationEvent{current, invalidated}
  Contract-->>Admin: current_salt
Loading
sequenceDiagram
  autonumber
  participant Caller
  participant Contract
  participant State
  participant Accounts

  Caller->>Contract: cleanup_nonces([(acct, [nonce,...]), ...])
  loop per (acct, nonces)
    Contract->>Contract: VersionedNonce::maybe_from(nonce)
    alt V1 salted && (expired or invalid salt)
      Contract->>State: cleanup_nonce_by_prefix(&acct, prefix)
      State->>Accounts: account.cleanup_nonce_by_prefix(prefix)
      Accounts-->>State: bool (cleared?)
    else skip
      note right of Contract: not cleanable
    end
  end
  Contract-->>Caller: done
Loading

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

Suggested reviewers

  • mitinarseny
  • fusede
  • BonelessImpl

Poem

I hop with salt upon my paw,
I tuck old nonces in my straw.
I spin a prefix, clear the way —
New salts, new checks, a brighter day.
A rabbit builds and bounces: commit. 🐇✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title “Salted nonces” accurately highlights the introduction of salted nonce support, which is a major feature in this changeset, and it is concise and clear without being misleading. Although the PR also includes nonce versioning and state versioning enhancements, the title still correctly references a central aspect of the work. It provides meaningful context to reviewers scanning the history.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/salted-nonces

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 010f6ae and f30b752.

📒 Files selected for processing (1)
  • tests/src/tests/defuse/accounts/mod.rs (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
tests/src/tests/defuse/accounts/mod.rs (2)
defuse/src/contract/accounts/mod.rs (1)
  • cleanup_nonces (58-72)
defuse/src/accounts.rs (1)
  • cleanup_nonces (35-35)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: Build
  • GitHub Check: Shared / Static Analysis (intents-contract, contract, near, defuse, 1, ., 1.18, false, --filter-paths "nod...
  • GitHub Check: Shared / Static Analysis (intents-poa-factory, contract, near, poa-factory, 1, ., 1.18, false, --filter-pa...
  • GitHub Check: Shared / Static Analysis (intents-poa-token, contract, near, poa-token, 1, ., 1.18, false, --filter-paths ...
  • GitHub Check: Shared security analysis / Static Analysis (intents-poa-token, contract, near, poa-token, 1, ., 1.18, false, --filter-paths ...
  • GitHub Check: Shared security analysis / Static Analysis (intents-contract, contract, near, defuse, 1, ., 1.18, false, --filter-paths "nod...
  • GitHub Check: Shared security analysis / Static Analysis (intents-poa-factory, contract, near, poa-factory, 1, ., 1.18, false, --filter-pa...
  • GitHub Check: Security Audit - report
🔇 Additional comments (3)
tests/src/tests/defuse/accounts/mod.rs (3)

26-30: LGTM! Method signature updated consistently.

The method rename from cleanup_expired_nonces to cleanup_nonces and the addition of the defuse_contract_id parameter align with the contract API changes and support calling different defuse contract instances.


94-121: LGTM! Implementation correctly updated.

The implementation properly:

  • Uses the new defuse_contract_id parameter to route the contract call
  • Maintains the 1 yoctoNEAR deposit requirement for cleanup_nonces
  • Transforms nonces to AsBase64<Nonce> format as expected by the contract

207-215: All cleanup_nonces call sites updated with defuse_contract_id Verified that every invocation includes the new parameter.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (23)
defuse/src/contract/accounts/account/mod.rs (1)

153-154: Minor grammar issue in documentation.

The documentation has a subject-verb agreement error: "whether the nonces was cleared" should be "whether the nonce was cleared" (singular).

Apply this diff to fix the grammar:

-    /// Returns whether the nonces was cleared,
+    /// Returns whether the nonce was cleared,
core/src/engine/state/cached.rs (1)

123-129: Forwarders look good; consider adding #[inline] for consistency.

Matches the trait changes and mirrors deltas.rs. Add #[inline] as done for other small passthroughs.

-    fn verify_intent_nonce(&self, nonce: Nonce, intent_deadline: Deadline) -> Result<()> {
+    #[inline]
+    fn verify_intent_nonce(&self, nonce: Nonce, intent_deadline: Deadline) -> Result<()> {
         self.view.verify_intent_nonce(nonce, intent_deadline)
     }
 
-    fn is_nonce_cleanable(&self, nonce: Nonce) -> Result<bool> {
+    #[inline]
+    fn is_nonce_cleanable(&self, nonce: Nonce) -> Result<bool> {
         self.view.is_nonce_cleanable(nonce)
     }
defuse/src/contract/state/mod.rs (2)

14-14: Remove stale TODO; versioning now exists via ContractStateEntry.

This comment is outdated; replace with a brief note pointing to entry::ContractStateEntry.

-// TODO: add versioning
+// State versioning implemented via entry::ContractStateEntry (see module `entry`)

33-41: Use prefix.as_slice().nest for storage key construction and add a legacy‐state deserialization test
Storage keys now use prefix.as_slice().nest for both total_supplies and salts, matching other modules’ conventions. Add a unit test that deserializes a v0 state via ContractStateEntry to catch future layout regressions.

tests/src/tests/defuse/state/extentions/mod.rs (1)

1-2: Module re-exports are fine; minor naming nit.

If feasible later, consider renaming extentionsextensions repo‑wide to avoid typos in paths/imports. Low priority.

defuse/src/contract/accounts/mod.rs (1)

57-66: Don’t silently drop errors; add minimal observability and skip empty batches.

Swallowing errors from is_nonce_cleanable and cleanup makes diagnosing issues hard. Log or count failures; also avoid calling cleanup with an empty list.

-            // TODO: optimize that
-            let cleanable = nonces
+            // NOTE: collect to end the shared &self borrow before &mut self below.
+            let mut errors = 0usize;
+            let cleanable = nonces
                 .into_iter()
                 .map(AsBase64::into_inner)
-                .filter(|&n| self.is_nonce_cleanable(n).unwrap_or_default())
+                .filter(|&n| {
+                    match self.is_nonce_cleanable(n) {
+                        Ok(v) => v,
+                        Err(_) => { errors += 1; false }
+                    }
+                })
                 .collect::<Vec<_>>();
 
-            // NOTE: all errors are omitted
-            State::cleanup_expired_nonces(self, &account_id, cleanable).ok();
+            if !cleanable.is_empty() {
+                // NOTE: ignore cleanup errors, but surface counts for visibility.
+                let _ = State::cleanup_expired_nonces(self, &account_id, cleanable);
+            }
+            if errors > 0 {
+                near_sdk::env::log_str(&format!("is_nonce_cleanable errors: {errors}"));
+            }
core/src/events.rs (1)

33-35: Resolve the TODO: clarify event version.

The TODO comment questions which version to use for SaltRotationEvent. The current version "0.1.0" is significantly lower than other recent events (e.g., "0.3.0", "0.3.1"). Consider whether:

  • This should be "0.3.0" (or higher) to align with the current event schema version
  • "0.1.0" is intentional to indicate a new feature with its own versioning track

Would you like me to search for documentation or patterns around event versioning in this codebase to help determine the appropriate version?

tests/src/tests/defuse/state/salt.rs (2)

17-23: Don’t couple tests to exact error text

Asserting on a hard-coded substring ("Insufficient permissions for method") is brittle. Prefer checking an error code/variant or a stable pattern (e.g., status and method name) to avoid false failures on message wording changes.

If there’s a stable error code for ACL failures, switch assertions to match it. Otherwise, at least narrow to a prefix and method name.

Also applies to: 50-56


31-40: Assert event emission to cover integration behavior

Both rotate_salt and invalidate_salt emit SaltRotationEvent. Add an assertion that the expected event was emitted (old_salt/new_salt) to catch regressions in event wiring.

If you want, I can extend the SaltManagerExt call helpers to return the transaction outcome so logs can be parsed and asserted.

Also applies to: 71-77, 85-95

core/src/nonce/expirable.rs (2)

39-41: Remove unused import; rstest fixture already provides random_bytes

The imported symbol shadows the rstest fixture name and is unused, triggering a warning.

-    use defuse_test_utils::random::random_bytes;

35-61: Add a Borsh roundtrip test to pin the 8‑byte deadline encoding

Given the custom adapter As::, add a simple serialize/deserialize roundtrip to guard against accidental format changes.

#[test]
fn borsh_roundtrip() {
    use near_sdk::borsh::{BorshDeserialize, BorshSerialize};
    let now = chrono::Utc::now();
    let en = ExpirableNonce::new(Deadline::new(now), [7u8; 24]);
    let data = en.try_to_vec().unwrap();
    let de = ExpirableNonce::<[u8; 24]>::try_from_slice(&data).unwrap();
    assert_eq!(en, de);
}

Based on learnings (chrono 0.4.42 APIs and Days usage are stable).

defuse/src/contract/salts.rs (2)

47-55: Mark read-only methods as view for clarity and tooling

is_valid_salt and get_current_salt are pure reads. Consider marking them as view (if supported in your #[near] macro version) to make intent explicit and prevent accidental payable calls.

Example:

#[view]
fn is_valid_salt(&self, salt: &Salt) -> bool { ... }

#[view]
fn get_current_salt(&self) -> Salt { ... }

25-45: Invalidate-current path: document the two-step rotation + clear behavior

Logic is correct (rotate if current, then clear_previous on the now-previous salt), but it’s non-obvious. A short comment will prevent accidental reordering that would break correctness.

-        if salt == &self.salts.current() {
+        // If invalidating the current salt, advance to a new one first so `salt`
+        // becomes the previous entry we can clear below.
+        if salt == &self.salts.current() {
             self.salts.set_new();
         }
tests/src/tests/defuse/state/extentions/fee.rs (1)

31-36: Make view helpers explicit about the target contract

Confirmed that all current calls (env.defuse.fee() and .fee_collector()) are on Contract instances, but since FeesManagerExt is also implemented for Account, invoking these on a non‐contract account would issue a view on the wrong account ID. Mirror the mutators’ signatures by adding a defuse_contract_id parameter:

-    async fn fee(&self) -> anyhow::Result<Pips>;
+    async fn fee(&self, defuse_contract_id: &AccountId) -> anyhow::Result<Pips>;

-    async fn fee_collector(&self) -> anyhow::Result<AccountId>;
+    async fn fee_collector(&self, defuse_contract_id: &AccountId) -> anyhow::Result<AccountId>;

and update both Account and Contract impls to forward the provided defuse_contract_id.

core/src/nonce/salted.rs (3)

19-22: Avoid unwrap in contract code paths; prefer explicit invariant checks.

unwrap() will panic if invariants ever change. Replace with an explicit expect or, better, avoid fallible conversion.

-        // NOTE: it is safe to unwrap here as random_seed_array always returns 32 bytes
-        env::random_seed_array()[..4].try_into().unwrap()
+        // random_seed_array() returns 32 bytes on NEAR; take first 4 deterministically
+        let bytes = env::random_seed_array();
+        // If the SDK ever changes, fail with a clear message instead of panicking mid‑serialization
+        bytes[..4].try_into().expect("random_seed_array must be at least 4 bytes")

36-45: Clarify rotation semantics and bound retained “previous” salts.

set_new inserts every previous salt and leaves it valid until explicitly cleared. Over many rotations this can grow unbounded and keep multiple previous salts valid.

  • Option A (policy): keep only the last previous salt; clear older entries inside set_new.
  • Option B (API): add clear_all_previous() or retain_previous<F: Fn(&Salt)->bool> to bound storage and validity windows.

53-56: Minor: constant‑time lookup is fine; consider documenting expected slice size.

is_valid is O(1), but is_valid_salt on SaltedNonce does a linear scan over expected_salts. If this is only used in tests, fine; otherwise document the typical size (e.g., 1–2 salts) to set expectations.

tests/src/tests/defuse/accounts/nonces.rs (3)

27-37: Helper constructs correct V1 layout; consider asserting encoded length in tests.

VersionedNonce::V1(..).try_into() relies on the encoded layout being exactly 32 bytes. Add an assertion in tests to guard against future changes to Deadline encoding.

let raw: [u8; 32] = VersionedNonce::V1(...).try_into().unwrap();
assert_eq!(raw.len(), 32, "V1 nonce must encode to 32 bytes");

72-88: Error string coupling can be brittle.

assert_err_contains("invalid salt") couples tests to exact messages. Prefer error codes/variants or stable substrings behind helpers to reduce churn.


315-337: Invalidate‑then‑cleanup flow looks good; add a check that legacy/non‑expired nonces remain.

You already assert long‑term nonce is cleared post‑invalidation. Add asserts that unaffected nonces (e.g., a different salt or still‑valid deadline) remain to catch over‑eager cleanup.

core/src/nonce/versioned.rs (3)

55-69: Legacy branch: make chaining explicit for readability and type clarity.

prefix.chain(reader) relies on coercions; using Cursor reads cleaner and avoids surprises.

-            _ => Self::Legacy(Nonce::deserialize_reader(&mut prefix.chain(reader))?),
+            _ => {
+                let mut chained = std::io::Cursor::new(prefix).chain(reader);
+                Self::Legacy(Nonce::deserialize_reader(&mut chained)?)
+            }

11-16: Document the layout source of truth and add a static size check.

The docstring states V1 = 4 (prefix) + 4 (salt) + 8 (deadline) + 16 (nonce) = 32. Add a const_assert! (or test) to ensure SaltedNonce<ExpirableNonce<[u8; 16]>> serializes to 28 bytes, guarding future Deadline changes.


88-120: Strengthen tests with negative cases and fuzz.

  • Add a test that feeds a 32‑byte payload with an unknown prefix and ensures it decodes as Legacy.
  • Add a test that encodes V1, flips one byte in the prefix, and roundtrips to Legacy.
  • Consider arbitrary/fuzz to ensure deserializer never panics on random 32‑byte inputs.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7ca45ed and 06d2459.

📒 Files selected for processing (32)
  • core/src/accounts.rs (2 hunks)
  • core/src/engine/mod.rs (2 hunks)
  • core/src/engine/state/cached.rs (2 hunks)
  • core/src/engine/state/deltas.rs (2 hunks)
  • core/src/engine/state/mod.rs (2 hunks)
  • core/src/error.rs (2 hunks)
  • core/src/events.rs (2 hunks)
  • core/src/nonce.rs (0 hunks)
  • core/src/nonce/expirable.rs (1 hunks)
  • core/src/nonce/mod.rs (1 hunks)
  • core/src/nonce/salted.rs (1 hunks)
  • core/src/nonce/versioned.rs (1 hunks)
  • defuse/src/contract/accounts/account/mod.rs (1 hunks)
  • defuse/src/contract/accounts/account/nonces.rs (4 hunks)
  • defuse/src/contract/accounts/mod.rs (1 hunks)
  • defuse/src/contract/intents/state.rs (2 hunks)
  • defuse/src/contract/mod.rs (5 hunks)
  • defuse/src/contract/salts.rs (1 hunks)
  • defuse/src/contract/state/entry.rs (1 hunks)
  • defuse/src/contract/state/mod.rs (4 hunks)
  • defuse/src/contract/state/v0.rs (1 hunks)
  • defuse/src/intents.rs (1 hunks)
  • defuse/src/lib.rs (1 hunks)
  • defuse/src/salts.rs (1 hunks)
  • tests/src/tests/defuse/accounts/nonces.rs (4 hunks)
  • tests/src/tests/defuse/mod.rs (1 hunks)
  • tests/src/tests/defuse/state/extentions/fee.rs (1 hunks)
  • tests/src/tests/defuse/state/extentions/mod.rs (1 hunks)
  • tests/src/tests/defuse/state/extentions/salt.rs (1 hunks)
  • tests/src/tests/defuse/state/fee.rs (1 hunks)
  • tests/src/tests/defuse/state/mod.rs (1 hunks)
  • tests/src/tests/defuse/state/salt.rs (1 hunks)
💤 Files with no reviewable changes (1)
  • core/src/nonce.rs
🧰 Additional context used
🧬 Code graph analysis (23)
tests/src/tests/defuse/state/extentions/fee.rs (2)
tests/src/tests/defuse/state/fee.rs (2)
  • set_fee (14-43)
  • set_fee_collector (47-74)
defuse/src/contract/intents/state.rs (2)
  • fee (31-33)
  • fee_collector (36-38)
core/src/engine/state/mod.rs (3)
core/src/engine/state/cached.rs (2)
  • verify_intent_nonce (123-125)
  • is_nonce_cleanable (127-129)
core/src/engine/state/deltas.rs (2)
  • verify_intent_nonce (101-103)
  • is_nonce_cleanable (106-108)
defuse/src/contract/intents/state.rs (2)
  • verify_intent_nonce (94-117)
  • is_nonce_cleanable (119-130)
tests/src/tests/defuse/state/extentions/mod.rs (2)
defuse/src/contract/intents/state.rs (1)
  • fee (31-33)
tests/src/tests/defuse/state/extentions/fee.rs (3)
  • fee (8-8)
  • fee (31-36)
  • fee (68-70)
tests/src/tests/defuse/state/fee.rs (3)
tests/src/tests/defuse/state/extentions/fee.rs (12)
  • set_fee (6-6)
  • set_fee (18-29)
  • set_fee (64-66)
  • fee (8-8)
  • fee (31-36)
  • fee (68-70)
  • set_fee_collector (9-13)
  • set_fee_collector (38-54)
  • set_fee_collector (72-80)
  • fee_collector (14-14)
  • fee_collector (55-60)
  • fee_collector (82-84)
defuse/src/contract/intents/state.rs (2)
  • fee (31-33)
  • fee_collector (36-38)
core/src/fees.rs (1)
  • from_pips (39-44)
defuse/src/contract/salts.rs (4)
tests/src/tests/defuse/state/salt.rs (2)
  • rotate_salt (13-41)
  • invalidate_salt (45-96)
tests/src/tests/defuse/state/extentions/salt.rs (12)
  • rotate_salt (6-6)
  • rotate_salt (20-29)
  • rotate_salt (64-66)
  • invalidate_salt (8-12)
  • invalidate_salt (31-45)
  • invalidate_salt (68-76)
  • is_valid_salt (14-14)
  • is_valid_salt (47-53)
  • is_valid_salt (78-80)
  • get_current_salt (16-16)
  • get_current_salt (55-60)
  • get_current_salt (82-84)
defuse/src/salts.rs (4)
  • rotate_salt (8-8)
  • invalidate_salt (12-12)
  • is_valid_salt (15-15)
  • get_current_salt (18-18)
core/src/nonce/salted.rs (1)
  • is_valid_salt (82-84)
defuse/src/contract/intents/state.rs (4)
core/src/engine/state/cached.rs (2)
  • verify_intent_nonce (123-125)
  • is_nonce_cleanable (127-129)
core/src/engine/state/deltas.rs (2)
  • verify_intent_nonce (101-103)
  • is_nonce_cleanable (106-108)
core/src/engine/state/mod.rs (2)
  • verify_intent_nonce (46-46)
  • is_nonce_cleanable (49-49)
core/src/nonce/versioned.rs (2)
  • try_from (38-40)
  • try_from (46-52)
defuse/src/contract/accounts/account/nonces.rs (2)
defuse/src/contract/accounts/account/mod.rs (1)
  • new (48-65)
test-utils/src/random.rs (2)
  • new (77-79)
  • random_bytes (143-151)
defuse/src/contract/state/v0.rs (3)
defuse/src/contract/state/entry.rs (4)
  • from (68-73)
  • from (78-81)
  • from (86-88)
  • new (36-41)
defuse/src/contract/mod.rs (1)
  • new (83-92)
defuse/src/contract/state/mod.rs (1)
  • new (29-42)
core/src/engine/state/deltas.rs (3)
core/src/engine/state/cached.rs (2)
  • verify_intent_nonce (123-125)
  • is_nonce_cleanable (127-129)
core/src/engine/state/mod.rs (2)
  • verify_intent_nonce (46-46)
  • is_nonce_cleanable (49-49)
defuse/src/contract/intents/state.rs (2)
  • verify_intent_nonce (94-117)
  • is_nonce_cleanable (119-130)
defuse/src/salts.rs (3)
defuse/src/contract/salts.rs (4)
  • rotate_salt (13-23)
  • invalidate_salt (27-45)
  • is_valid_salt (48-50)
  • get_current_salt (53-55)
tests/src/tests/defuse/state/extentions/salt.rs (12)
  • rotate_salt (6-6)
  • rotate_salt (20-29)
  • rotate_salt (64-66)
  • invalidate_salt (8-12)
  • invalidate_salt (31-45)
  • invalidate_salt (68-76)
  • is_valid_salt (14-14)
  • is_valid_salt (47-53)
  • is_valid_salt (78-80)
  • get_current_salt (16-16)
  • get_current_salt (55-60)
  • get_current_salt (82-84)
core/src/nonce/salted.rs (1)
  • is_valid_salt (82-84)
core/src/nonce/expirable.rs (2)
test-utils/src/random.rs (2)
  • random (82-84)
  • random_bytes (143-151)
core/src/nonce.rs (1)
  • ExpirableNonce (73-80)
tests/src/tests/defuse/state/extentions/salt.rs (4)
defuse/src/contract/salts.rs (4)
  • rotate_salt (13-23)
  • invalidate_salt (27-45)
  • is_valid_salt (48-50)
  • get_current_salt (53-55)
tests/src/tests/defuse/state/salt.rs (2)
  • rotate_salt (13-41)
  • invalidate_salt (45-96)
defuse/src/salts.rs (4)
  • rotate_salt (8-8)
  • invalidate_salt (12-12)
  • is_valid_salt (15-15)
  • get_current_salt (18-18)
core/src/nonce/salted.rs (1)
  • is_valid_salt (82-84)
defuse/src/contract/accounts/mod.rs (6)
serde-utils/src/base64.rs (1)
  • into_inner (77-79)
core/src/engine/state/cached.rs (1)
  • cleanup_expired_nonces (186-202)
core/src/engine/state/deltas.rs (1)
  • cleanup_expired_nonces (131-137)
core/src/engine/state/mod.rs (1)
  • cleanup_expired_nonces (68-72)
defuse/src/contract/intents/state.rs (1)
  • cleanup_expired_nonces (166-182)
defuse/src/accounts.rs (1)
  • cleanup_expired_nonces (35-35)
core/src/nonce/salted.rs (7)
defuse/src/contract/accounts/mod.rs (1)
  • new (100-109)
defuse/src/contract/mod.rs (1)
  • new (83-92)
defuse/src/contract/state/entry.rs (1)
  • new (36-41)
defuse/src/contract/state/mod.rs (1)
  • new (29-42)
defuse/src/contract/salts.rs (1)
  • is_valid_salt (48-50)
defuse/src/salts.rs (1)
  • is_valid_salt (15-15)
test-utils/src/random.rs (3)
  • s (54-54)
  • random (82-84)
  • random_bytes (143-151)
tests/src/tests/defuse/state/mod.rs (2)
defuse/src/contract/intents/state.rs (1)
  • fee (31-33)
tests/src/tests/defuse/state/extentions/fee.rs (3)
  • fee (8-8)
  • fee (31-36)
  • fee (68-70)
core/src/nonce/mod.rs (5)
core/src/nonce/salted.rs (2)
  • new (26-34)
  • new (78-80)
core/src/nonce/expirable.rs (1)
  • new (25-27)
defuse/src/contract/accounts/account/mod.rs (1)
  • new (48-65)
defuse/src/contract/accounts/account/nonces.rs (3)
  • new (31-37)
  • is_used (63-69)
  • commit (48-60)
core/src/nonce/versioned.rs (1)
  • prefix (27-32)
core/src/engine/state/cached.rs (3)
core/src/engine/state/deltas.rs (2)
  • verify_intent_nonce (101-103)
  • is_nonce_cleanable (106-108)
core/src/engine/state/mod.rs (2)
  • verify_intent_nonce (46-46)
  • is_nonce_cleanable (49-49)
defuse/src/contract/intents/state.rs (2)
  • verify_intent_nonce (94-117)
  • is_nonce_cleanable (119-130)
defuse/src/contract/state/entry.rs (3)
defuse/src/contract/mod.rs (1)
  • new (83-92)
defuse/src/contract/state/mod.rs (1)
  • new (29-42)
defuse/src/contract/state/v0.rs (1)
  • from (23-40)
tests/src/tests/defuse/state/salt.rs (3)
defuse/src/contract/salts.rs (3)
  • rotate_salt (13-23)
  • is_valid_salt (48-50)
  • invalidate_salt (27-45)
tests/src/tests/defuse/state/extentions/salt.rs (9)
  • rotate_salt (6-6)
  • rotate_salt (20-29)
  • rotate_salt (64-66)
  • is_valid_salt (14-14)
  • is_valid_salt (47-53)
  • is_valid_salt (78-80)
  • invalidate_salt (8-12)
  • invalidate_salt (31-45)
  • invalidate_salt (68-76)
core/src/nonce/salted.rs (1)
  • is_valid_salt (82-84)
tests/src/tests/defuse/accounts/nonces.rs (3)
core/src/nonce/salted.rs (2)
  • new (26-34)
  • new (78-80)
defuse/src/contract/accounts/mod.rs (2)
  • new (100-109)
  • is_nonce_used (51-53)
tests/src/tests/defuse/accounts/mod.rs (3)
  • is_nonce_used (44-44)
  • is_nonce_used (145-154)
  • is_nonce_used (232-234)
defuse/src/contract/mod.rs (3)
core/src/nonce/salted.rs (2)
  • new (26-34)
  • new (78-80)
defuse/src/contract/state/entry.rs (1)
  • new (36-41)
defuse/src/contract/state/mod.rs (1)
  • new (29-42)
core/src/nonce/versioned.rs (4)
core/src/payload/mod.rs (1)
  • nonce (68-70)
test-utils/src/random.rs (2)
  • random (82-84)
  • random_bytes (143-151)
core/src/nonce/salted.rs (2)
  • new (26-34)
  • new (78-80)
core/src/nonce/expirable.rs (1)
  • new (25-27)
defuse/src/contract/state/mod.rs (4)
core/src/nonce/salted.rs (2)
  • new (26-34)
  • new (78-80)
defuse/src/contract/accounts/mod.rs (1)
  • new (100-109)
defuse/src/contract/mod.rs (1)
  • new (83-92)
defuse/src/contract/state/entry.rs (1)
  • new (36-41)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: Build
  • GitHub Check: Shared / Static Analysis (intents-contract, contract, near, defuse, 1, ., 1.18, false, --filter-paths "nod...
  • GitHub Check: Shared / Static Analysis (intents-poa-factory, contract, near, poa-factory, 1, ., 1.18, false, --filter-pa...
  • GitHub Check: Shared / Static Analysis (intents-poa-token, contract, near, poa-token, 1, ., 1.18, false, --filter-paths ...
  • GitHub Check: Shared security analysis / Static Analysis (intents-poa-token, contract, near, poa-token, 1, ., 1.18, false, --filter-paths ...
  • GitHub Check: Shared security analysis / Static Analysis (intents-poa-factory, contract, near, poa-factory, 1, ., 1.18, false, --filter-pa...
  • GitHub Check: Shared security analysis / Static Analysis (intents-contract, contract, near, defuse, 1, ., 1.18, false, --filter-paths "nod...
  • GitHub Check: Security Audit - report
🔇 Additional comments (48)
tests/src/tests/defuse/mod.rs (1)

4-4: LGTM!

The new state module declaration correctly integrates the salt management and nonce-related tests introduced in this PR.

core/src/error.rs (1)

58-59: LGTM!

The new error variants InvalidNonce and InvalidSalt are well-positioned and provide clear error messages for the salted nonce validation flow introduced in this PR.

Also applies to: 73-74

defuse/src/lib.rs (1)

8-8: LGTM!

The public salts module declaration correctly exposes the new salt management functionality introduced in this PR.

core/src/engine/state/mod.rs (1)

5-5: All StateView implementations updated. The methods verify_intent_nonce and is_nonce_cleanable are implemented in CachedState, Deltas, and the Contract state with correct delegation.

defuse/src/contract/state/mod.rs (1)

24-25: Salt storage integration is correct and scoped.

Initializing ValidSalts under a dedicated Salts prefix is the right boundary and keeps keys stable. No issues.

Also applies to: 40-41, 49-49

core/src/engine/mod.rs (1)

77-80: Approve: nonce enforcement in Engine

Delegates salt/deadline/format checks to StateView; ordering (verify_intent_noncecommit_nonce) is correct.

• Confirmed a single StateView impl in defuse/src/contract/intents/state.rs; no legacy implementations missing verify_intent_nonce.

defuse/src/intents.rs (2)

14-14: LGTM! Import statement is correct.

The addition of SaltManager to the imports is necessary to support the updated trait bound on line 17.


17-17: All Intents implementors implement SaltManager. The sole implementor (Contract) already provides a SaltManager impl (defuse/src/contract/salts.rs:10), so no further action is required.

core/src/events.rs (1)

7-7: LGTM! Import statement is correct.

The SaltRotationEvent is properly imported from the accounts module.

core/src/accounts.rs (2)

7-7: LGTM! Import statement is correct.

The Salt type is properly imported alongside Nonce to support the new SaltRotationEvent.


67-73: LGTM! Well-defined event structure.

The SaltRotationEvent is properly structured with:

  • Appropriate must_use lint to ensure events are emitted
  • Correct near serializers annotation
  • Both old_salt and new_salt fields for complete audit trail
  • Standard derives (Debug, Clone)
tests/src/tests/defuse/state/fee.rs (2)

12-43: LGTM! Comprehensive permission and functionality test.

The set_fee test correctly verifies:

  • Permission enforcement (user without role cannot set fee)
  • Role-based access (user with FeesManager role can set fee)
  • State mutation (fee value changes correctly)

The test structure is clean and follows good practices with clear test phases.


45-74: LGTM! Comprehensive permission and functionality test.

The set_fee_collector test correctly verifies:

  • Permission enforcement (user without role cannot set collector)
  • Role-based access (user with FeesManager role can set collector)
  • State mutation (collector changes correctly)

The test structure is consistent with set_fee and follows the same good testing patterns.

tests/src/tests/defuse/state/mod.rs (1)

5-5: LGTM! Clean public API surface.

The re-exports provide a clean public API by exposing the extension traits at the module root, making them easy to import and use in tests.

Note: This line references the misspelled module name extentions which should be corrected as noted in the comment on line 1.

defuse/src/contract/intents/state.rs (3)

2-2: LGTM! Imports align with new nonce verification.

The added imports (Deadline, ExpirableNonce, SaltedNonce, VersionedNonce) are necessary for the new nonce validation logic introduced below.


119-130: Nonce cleanability logic is correct.

The method properly determines whether a nonce can be cleaned:

  • V1 nonces are cleanable if the salt is invalid or the deadline has expired.
  • Legacy nonces cannot be cleaned (preserving backward compatibility until they are fully deprecated).

The comment on line 125 clearly documents the rationale for not cleaning legacy nonces.


94-117: Intent nonce verification logic approved. Legacy and V1 nonces are handled correctly, and all referenced error variants (InvalidSalt, DeadlineGreaterThanNonce, NonceExpired) are present in DefuseError.

defuse/src/contract/accounts/account/nonces.rs (6)

63-69: LGTM! Nonce usage check is straightforward.

The is_used method correctly checks both the new nonces map and the legacy map (if present), ensuring backward compatibility.


72-75: Clear expired logic aligns with versioned nonce design.

The clear_expired method only clears nonces from the new map. The comment on line 73 correctly notes that expirable nonces cannot be in the legacy map, which aligns with the broader transition to versioned/salted nonces.


98-99: Test helper simplified correctly.

The random_nonces fixture now generates plain random nonces using rng.random(), removing the expirable nonce generation logic. This aligns with the removal of expirable nonce support.


132-148: Test updated to use plain random nonces.

The commit_new_nonce test now generates plain random nonces for both new_nonce and legacy_nonce, removing the expirable nonce logic. This simplifies the test and aligns with the broader nonce model changes.


166-176: Duplicate nonce test simplified.

The commit_duplicate_nonce test now uses a plain random nonce, removing the expirable nonce logic. The test logic remains correct.


200-210: Legacy nonces cannot be cleared test updated.

The legacy_nonces_cant_be_cleared test now uses a plain random nonce. The test correctly verifies that legacy nonces cannot be cleared and remain marked as used.

defuse/src/salts.rs (1)

1-19: SaltManager trait is well-defined.

The trait provides a clean interface for managing salts:

  • rotate_salt: Rotates to a new salt while keeping the previous one valid (graceful rotation).
  • invalidate_salt: Invalidates a specific salt and sets a new one if the invalidated salt was current.
  • is_valid_salt: Queries salt validity.
  • get_current_salt: Retrieves the current salt.

The comments clearly document each method's behavior. The trait is appropriately exposed via ext_contract for cross-contract calls.

core/src/engine/state/deltas.rs (2)

2-2: LGTM! Deadline import added.

The Deadline type is now imported to support the new verify_intent_nonce method signature.


100-108: Forwarding methods are correct.

Both verify_intent_nonce and is_nonce_cleanable correctly forward calls to the underlying state. The #[inline] attribute is appropriate for these trivial delegation methods, ensuring no overhead from the wrapper.

defuse/src/contract/mod.rs (5)

9-9: Salts module added.

The new salts module is added to support the salt management functionality introduced in this PR.


30-30: State type updated to ContractStateEntry.

The import changed from ContractState to ContractStateEntry, which aligns with the introduction of versioned contract state (as noted in the PR objectives: "State versioning").


49-50: SaltManager role added for access control.

The new SaltManager role enables fine-grained access control for salt rotation and invalidation operations, consistent with the existing role-based access control pattern in the contract.


70-70: Contract state field updated.

The state field type changed from ContractState to ContractStateEntry, consistent with the import change on line 30.


86-86: Approve: Signatures are consistent
ContractStateEntry::new<T>(prefix: T, wnear_id: AccountId, fees: FeesConfig) matches the original ContractState::new<S>(prefix: S, wnear_id: AccountId, fees: FeesConfig).

defuse/src/contract/state/v0.rs (1)

22-40: Refactor From<ContractStateV0> to use ContractState::new for salts prefix consistency
Replace the inline ValidSalts::new(...) with:

let mut state = ContractState::new(ContractPrefix::State, wnear_id, fees);
state.total_supplies = total_supplies;
state

This centralizes prefix logic in ContractState::new and prevents future drift.

core/src/nonce/mod.rs (7)

1-10: LGTM!

Clean modular structure with appropriate re-exports for versioned, salted, and expirable nonce types.


18-24: LGTM!

The Nonces wrapper over BitMap256 with proper serialization support is a clean abstraction.


30-33: LGTM!

Const constructor properly delegates to the underlying BitMap256.


35-38: LGTM!

The method correctly checks whether a nonce has been used by delegating to the bitmap.


40-47: LGTM!

The commit method correctly prevents nonce reuse by returning an error if the nonce was already set, providing replay protection.


49-54: LGTM!

The method correctly extracts the prefix from the nonce (all bytes except the last) and clears all nonces with that prefix, which aligns with the expirable nonce design.


56-63: LGTM!

The iterator method properly constrains the type bound and delegates to the underlying bitmap iterator.

defuse/src/contract/state/entry.rs (6)

19-42: LGTM!

The transparent wrapper pattern with custom Borsh adapters provides clean version-aware (de)serialization. The constructor properly delegates to the inner type.


44-58: LGTM!

The magic prefix approach using u32::MAX is safe given the assumption that the legacy IterableMap length will never reach this value. The comment clearly documents the collision avoidance strategy.


60-65: LGTM!

The versioned enum with Cow and PanicOnClone wrappers provides efficient serialization while preventing accidental clones.


67-89: LGTM!

The conversion paths correctly handle ownership transfer and unwrapping for both deserialization (V0/Latest → ContractState) and serialization (always Latest).


91-115: LGTM!

The deserialization logic correctly handles both versioned and legacy formats. The use of chain() to prepend the consumed prefix bytes back to the reader for legacy deserialization is the right approach.


117-132: LGTM!

Serialization always writes the versioned format with the magic prefix, ensuring forward compatibility while maintaining backward compatibility for reads.

tests/src/tests/defuse/state/extentions/salt.rs (3)

5-17: LGTM!

The trait provides a clean async interface for salt management operations, with method signatures matching the contract interface.


20-45: LGTM!

The mutation methods correctly target the defuse contract, attach the required 1 yoctoNEAR deposit, and use max gas for the cross-contract calls.


63-85: Contract delegation is correct, but inherits view method issue.

The Contract implementation correctly delegates all methods to the underlying account. However, it inherits the design issue flagged in the Account implementation regarding view method contract targeting.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
core/src/nonce/versioned.rs (1)

65-96: LGTM! Serialization logic is correct.

The Borsh implementations correctly handle both legacy and V1 formats. The deserialize logic appropriately uses chain to prepend the prefix back when falling back to legacy format.

The TODO on line 86 suggests the serialize implementation could be more concise. Consider using pattern matching to combine the prefix writing and data serialization:

 impl BorshSerialize for VersionedNonce {
     fn serialize<W: io::Write>(&self, writer: &mut W) -> io::Result<()> {
-        // TODO: make it more concise
-        if let Some(prefix) = self.prefix() {
-            writer.write_all(&prefix)?;
-        }
-
         match self {
-            Self::Legacy(nonce) => nonce.serialize(writer),
-            Self::V1(salted) => salted.serialize(writer),
+            Self::Legacy(nonce) => nonce.serialize(writer),
+            Self::V1(salted) => {
+                writer.write_all(&Self::V1_MAGIC_PREFIX)?;
+                salted.serialize(writer)
+            }
         }
     }
 }
tests/src/tests/defuse/state/extensions/salt.rs (1)

5-21: Consider consistent parameter passing for Salt.

The trait takes Salt by value in invalidate_salt (line 11) but by reference in is_valid_salt (line 17). For consistency and to avoid unnecessary clones in calling code, consider taking &Salt in both methods.

Apply this diff for consistency:

     async fn invalidate_salt(
         &self,
         defuse_contract_id: &AccountId,
-        salt: Salt,
+        salt: &Salt,
     ) -> anyhow::Result<()>;

Then update the implementations accordingly (lines 35-49 and 76-84).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 06d2459 and efae3c5.

📒 Files selected for processing (8)
  • core/src/nonce/versioned.rs (1 hunks)
  • tests/src/tests/defuse/accounts/nonces.rs (4 hunks)
  • tests/src/tests/defuse/state/extensions/fee.rs (1 hunks)
  • tests/src/tests/defuse/state/extensions/mod.rs (1 hunks)
  • tests/src/tests/defuse/state/extensions/salt.rs (1 hunks)
  • tests/src/tests/defuse/state/fee.rs (1 hunks)
  • tests/src/tests/defuse/state/mod.rs (1 hunks)
  • tests/src/tests/defuse/state/salt.rs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/src/tests/defuse/state/fee.rs
  • tests/src/tests/defuse/state/salt.rs
🧰 Additional context used
🧬 Code graph analysis (6)
tests/src/tests/defuse/state/extensions/mod.rs (1)
tests/src/tests/defuse/state/extensions/fee.rs (3)
  • fee (8-8)
  • fee (31-36)
  • fee (68-70)
tests/src/tests/defuse/state/extensions/salt.rs (1)
tests/src/tests/defuse/state/salt.rs (2)
  • rotate_salt (13-46)
  • invalidate_salt (50-107)
tests/src/tests/defuse/state/mod.rs (2)
tests/src/tests/defuse/state/extensions/fee.rs (3)
  • fee (8-8)
  • fee (31-36)
  • fee (68-70)
defuse/src/contract/intents/state.rs (1)
  • fee (31-33)
tests/src/tests/defuse/state/extensions/fee.rs (1)
tests/src/tests/defuse/state/fee.rs (2)
  • set_fee (14-43)
  • set_fee_collector (47-74)
core/src/nonce/versioned.rs (2)
core/src/nonce/salted.rs (2)
  • new (26-34)
  • new (78-80)
core/src/nonce/expirable.rs (1)
  • new (25-27)
tests/src/tests/defuse/accounts/nonces.rs (3)
core/src/nonce/salted.rs (2)
  • new (26-34)
  • new (78-80)
defuse/src/contract/accounts/mod.rs (2)
  • new (100-109)
  • is_nonce_used (51-53)
tests/src/tests/defuse/accounts/mod.rs (3)
  • is_nonce_used (44-44)
  • is_nonce_used (145-154)
  • is_nonce_used (232-234)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: Build
  • GitHub Check: Shared security analysis / Static Analysis (intents-contract, contract, near, defuse, 1, ., 1.18, false, --filter-paths "nod...
  • GitHub Check: Shared security analysis / Static Analysis (intents-poa-factory, contract, near, poa-factory, 1, ., 1.18, false, --filter-pa...
  • GitHub Check: Shared security analysis / Static Analysis (intents-poa-token, contract, near, poa-token, 1, ., 1.18, false, --filter-paths ...
  • GitHub Check: Shared / Static Analysis (intents-contract, contract, near, defuse, 1, ., 1.18, false, --filter-paths "nod...
  • GitHub Check: Shared / Static Analysis (intents-poa-token, contract, near, poa-token, 1, ., 1.18, false, --filter-paths ...
  • GitHub Check: Shared / Static Analysis (intents-poa-factory, contract, near, poa-factory, 1, ., 1.18, false, --filter-pa...
  • GitHub Check: Security Audit - report
🔇 Additional comments (16)
tests/src/tests/defuse/state/mod.rs (1)

1-5: LGTM! Spelling issue resolved.

The module structure is correct and the spelling of "extensions" has been fixed (previously flagged as "extentions").

tests/src/tests/defuse/state/extensions/mod.rs (1)

1-2: LGTM!

The module declarations correctly expose the fee and salt extension modules publicly.

tests/src/tests/defuse/state/extensions/fee.rs (3)

5-15: LGTM!

The trait provides a clean API for fee management with symmetric getter/setter pairs for both fee and fee_collector.


17-61: LGTM!

The implementation follows NEAR workspaces best practices with appropriate deposit (1 yoctoNEAR) for change methods and view calls for getters.


63-85: LGTM!

The Contract implementation correctly delegates all methods to the underlying Account via as_account().

tests/src/tests/defuse/accounts/nonces.rs (5)

1-26: LGTM!

The imports correctly include the new salted/versioned nonce types and salt management traits aligned with the PR's objectives.


27-37: LGTM!

The helper correctly constructs a V1 salted nonce with the provided salt and deadline. Using expect is acceptable in test helpers.


39-208: LGTM!

Comprehensive test coverage of nonce validation including legacy compatibility, salt validation, deadline checks, expiry, rotation windows, and invalidation. The test correctly verifies both acceptance and rejection paths.


210-338: LGTM!

Thorough test of nonce cleanup covering time-based expiry, idempotency, edge cases (unknown users, legacy nonces, not-yet-expired), and salt invalidation cleanup. The test correctly verifies selective cleanup behavior.


340-391: LGTM!

Performance test correctly measures gas costs for batch cleanup with parameterized nonce counts. The chunked creation and batch cleanup logic is sound.

core/src/nonce/versioned.rs (4)

11-33: LGTM!

The enum structure cleanly distinguishes legacy from V1 salted nonces. The magic prefix approach for version detection is a standard technique and the prefix() method provides clean access to version metadata.


35-41: LGTM!

The conversion correctly delegates to Borsh deserialization which will detect the version based on the prefix.


43-64: LGTM! Past review concern addressed.

The implementation now uses checked serialization with explicit length validation, eliminating the panic risk flagged in the previous review. The error handling correctly returns io::Error for unexpected lengths.


98-130: LGTM!

The tests correctly verify roundtrip conversions for both legacy and V1 nonce formats, ensuring serialization/deserialization correctness.

tests/src/tests/defuse/state/extensions/salt.rs (2)

23-69: LGTM!

The Account implementation follows consistent patterns for both state-changing methods (with 1 yoctoNEAR deposit and max_gas) and view methods. Error handling is appropriate for test utilities.


71-99: LGTM!

The Contract implementation correctly delegates all methods to the underlying Account implementation, providing a consistent interface for both Account and Contract types in tests.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
core/src/nonce/versioned.rs (1)

39-55: Good: safe serialization into fixed array with length check.

This addresses the prior panic risk from writing into a fixed [u8; 32] with unwrap(). Length is validated before copying.

🧹 Nitpick comments (4)
CHANGELOG.md (1)

8-8: Clarify salt size phrasing.

Consider rephrasing “4 bytes salts” to “4-byte salts” so the byte-width reads naturally in English.

core/src/nonce/salted.rs (2)

49-53: Avoid on-chain storage growth: remove invalidated salts instead of toggling a bool.

clear_previous flips a flag but keeps the key, so previous grows unbounded across rotations. On NEAR this increases state size (and storage staking) over time. Prefer removing the key to reclaim storage. If IterableMap lacks remove, consider switching to a set-like structure for previous valid salts.

Proposed change (if IterableMap::remove is available):

-    pub fn clear_previous(&mut self, salt: &Salt) -> bool {
-        self.previous.get_mut(salt).map(|v| *v = false).is_some()
-    }
+    pub fn clear_previous(&mut self, salt: &Salt) -> bool {
+        self.previous.remove(salt).is_some()
+    }

38-47: API naming/return value clarity for rotation.

set_new returns the previous salt while also updating current. Consider renaming to rotate and/or returning the new salt (callers can query current() but returning it is more direct). Not a blocker—just polish.

tests/src/tests/defuse/accounts/nonces.rs (1)

269-279: Potential time-based flakiness: rely on chain/env time rather than wall clock sleeps.

Tests use Utc::now() and tokio::sleep to drive expiry. On slower CI or different runtimes, deadlines may race with execution. Prefer advancing the contract’s notion of time (e.g., builder/context timestamp or a helper that bumps block timestamp) to deterministically cross deadlines.

Can you confirm whether Env exposes a way to set/block_timestamp for calls? If yes, we can replace sleeps with timestamp bumps for deterministic tests.

Also applies to: 286-297, 315-337

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between efae3c5 and ecac381.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (11)
  • CHANGELOG.md (1 hunks)
  • core/src/events.rs (2 hunks)
  • core/src/nonce/salted.rs (1 hunks)
  • core/src/nonce/versioned.rs (1 hunks)
  • defuse/Cargo.toml (1 hunks)
  • defuse/src/contract/state/entry.rs (1 hunks)
  • defuse/src/contract/state/mod.rs (4 hunks)
  • defuse/src/contract/state/v0.rs (1 hunks)
  • tests/src/tests/defuse/accounts/nonces.rs (4 hunks)
  • tests/src/tests/defuse/state/extensions/salt.rs (1 hunks)
  • tests/src/tests/defuse/state/salt.rs (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • defuse/Cargo.toml
🚧 Files skipped from review as they are similar to previous changes (3)
  • tests/src/tests/defuse/state/salt.rs
  • core/src/events.rs
  • tests/src/tests/defuse/state/extensions/salt.rs
🧰 Additional context used
🧬 Code graph analysis (6)
defuse/src/contract/state/mod.rs (3)
core/src/nonce/salted.rs (2)
  • new (28-36)
  • new (80-82)
defuse/src/contract/state/entry.rs (1)
  • new (36-41)
defuse/src/contract/mod.rs (1)
  • new (83-92)
core/src/nonce/salted.rs (4)
defuse/src/contract/state/mod.rs (1)
  • new (28-41)
tests/src/tests/defuse/state/extensions/salt.rs (3)
  • is_valid_salt (14-18)
  • is_valid_salt (51-61)
  • is_valid_salt (86-94)
defuse/src/contract/salts.rs (1)
  • is_valid_salt (48-50)
defuse/src/salts.rs (1)
  • is_valid_salt (15-15)
core/src/nonce/versioned.rs (3)
core/src/nonce/salted.rs (2)
  • new (28-36)
  • new (80-82)
core/src/nonce/expirable.rs (1)
  • new (25-27)
core/src/nonce/mod.rs (1)
  • new (31-33)
defuse/src/contract/state/v0.rs (3)
defuse/src/contract/state/entry.rs (4)
  • from (69-74)
  • from (79-82)
  • from (87-89)
  • new (36-41)
defuse/src/contract/state/mod.rs (1)
  • new (28-41)
defuse/src/contract/mod.rs (1)
  • new (83-92)
defuse/src/contract/state/entry.rs (4)
core/src/nonce/versioned.rs (1)
  • deserialize_reader (59-74)
defuse/src/contract/state/mod.rs (1)
  • new (28-41)
defuse/src/contract/mod.rs (1)
  • new (83-92)
defuse/src/contract/state/v0.rs (1)
  • from (21-38)
tests/src/tests/defuse/accounts/nonces.rs (3)
core/src/nonce/salted.rs (2)
  • new (28-36)
  • new (80-82)
defuse/src/accounts.rs (1)
  • is_nonce_used (30-30)
tests/src/tests/defuse/accounts/mod.rs (3)
  • is_nonce_used (44-44)
  • is_nonce_used (145-154)
  • is_nonce_used (232-234)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: Build
  • GitHub Check: Shared security analysis / Static Analysis (intents-contract, contract, near, defuse, 1, ., 1.18, false, --filter-paths "nod...
  • GitHub Check: Shared security analysis / Static Analysis (intents-poa-factory, contract, near, poa-factory, 1, ., 1.18, false, --filter-pa...
  • GitHub Check: Shared security analysis / Static Analysis (intents-poa-token, contract, near, poa-token, 1, ., 1.18, false, --filter-paths ...
  • GitHub Check: Shared / Static Analysis (intents-poa-token, contract, near, poa-token, 1, ., 1.18, false, --filter-paths ...
  • GitHub Check: Shared / Static Analysis (intents-contract, contract, near, defuse, 1, ., 1.18, false, --filter-paths "nod...
  • GitHub Check: Shared / Static Analysis (intents-poa-factory, contract, near, poa-factory, 1, ., 1.18, false, --filter-pa...
  • GitHub Check: Security Audit - report
🔇 Additional comments (12)
defuse/src/contract/state/mod.rs (3)

1-4: LGTM!

The module structure correctly introduces versioning support with the v0 legacy module and the new entry wrapper.


6-6: LGTM!

The ValidSalts import is correctly added to support the new salts field in ContractState.


48-48: LGTM!

The Salts storage prefix variant is correctly added to support the new salts field.

defuse/src/contract/state/v0.rs (2)

1-18: LGTM!

ContractStateV0 correctly represents the legacy state format before the salts field was introduced, enabling backward-compatible migration.


20-39: Approve From<ContractStateV0> prefix initialization.
The migration’s salts key construction (ContractPrefix::State.into_storage_key().nest(StatePrefix::Salts)) matches ContractState::new (Prefix::State.as_slice().nest(Prefix::Salts)). No further action required.

defuse/src/contract/state/entry.rs (5)

1-42: LGTM!

The ContractStateEntry wrapper correctly encapsulates ContractState with custom versioned serialization hooks and provides a convenient constructor.


61-90: LGTM!

The versioned state enum and conversion implementations correctly handle both legacy (V0) and current (Latest) formats with appropriate zero-copy optimization using Cow and PanicOnClone.


92-116: LGTM!

The custom deserialization correctly handles both versioned and legacy formats by inspecting the magic prefix and either deserializing as versioned state or reconstructing the legacy reader by prepending the consumed prefix bytes.


118-133: LGTM!

The custom serialization correctly always writes the versioned format with the magic prefix, ensuring all future states are stored in the versioned format.


46-59: Comment documents magic-prefix assumption; no further action required.

The existing comment clearly explains why u32::MAX is chosen and its safety assumption; static code analysis cannot enforce runtime token counts. Proceed with monitoring token entry growth externally if needed.

tests/src/tests/defuse/accounts/nonces.rs (1)

27-37: Nice coverage and scenarios.

Good spread: legacy path, invalid salt, mismatched deadline, expired nonce, success, rotation (previous salt valid), and invalidation. This gives strong behavioral confidence.

Also applies to: 72-88, 90-106, 108-124, 126-149, 151-183, 185-207

core/src/nonce/versioned.rs (1)

11-20: Spec and layout are precise.

Clear format doc, explicit V1 magic, and round‑trip tests for both Legacy and V1 variants look solid.

Also applies to: 77-86, 89-121

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
core/src/nonce/versioned.rs (2)

1-1: Resolved: size_of properly brought into scope.

Import at Line 1 fixes the prior compile error where size_of::<Nonce>() wasn’t qualified. LGTM.

Also applies to: 41-41


67-72: Fix: prefix chaining uses [u8; 4] which doesn’t implement Read. Wrap in io::Cursor.

prefix.chain(reader) won’t compile because [u8; 4] is not a Read. Chain a Cursor over prefix with reader.

-            _ => Self::Legacy(Nonce::deserialize_reader(&mut prefix.chain(reader))?),
+            _ => {
+                let mut chained = io::Cursor::new(prefix).chain(reader);
+                Self::Legacy(Nonce::deserialize_reader(&mut chained)?)
+            }
🧹 Nitpick comments (1)
core/src/nonce/versioned.rs (1)

39-57: Optional: Avoid alloc + copy in TryFrom<VersionedNonce> for Nonce.

You serialize into a Vec then copy to an array. Use a fixed buffer with io::Cursor to eliminate allocation and the extra copy.

-        let mut buf = Vec::with_capacity(SIZE);
-        value.serialize(&mut buf)?;
-
-        if buf.len() != SIZE {
+        let mut out = [0u8; SIZE];
+        let mut cursor = io::Cursor::new(&mut out[..]);
+        value.serialize(&mut cursor)?;
+        let written = cursor.position() as usize;
+        debug_assert_eq!(written, SIZE, "VersionedNonce must serialize to 32 bytes");
+        if written != SIZE {
             return Err(io::Error::new(
                 io::ErrorKind::InvalidData,
                 format!(
                     "encoded VersionedNonce has unexpected length: {}",
-                    buf.len()
+                    written
                 ),
             ));
         }
-        let mut result = [0u8; SIZE];
-        result.copy_from_slice(&buf);
-        Ok(result)
+        Ok(out)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ecac381 and a5c2d45.

📒 Files selected for processing (1)
  • core/src/nonce/versioned.rs (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
core/src/nonce/versioned.rs (4)
core/src/nonce/salted.rs (2)
  • new (28-36)
  • new (80-82)
core/src/nonce/expirable.rs (1)
  • new (25-27)
core/src/nonce/mod.rs (1)
  • new (31-33)
defuse/src/contract/accounts/account/nonces.rs (1)
  • new (31-37)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: Build
  • GitHub Check: Shared / Static Analysis (intents-poa-token, contract, near, poa-token, 1, ., 1.18, false, --filter-paths ...
  • GitHub Check: Shared / Static Analysis (intents-poa-factory, contract, near, poa-factory, 1, ., 1.18, false, --filter-pa...
  • GitHub Check: Shared / Static Analysis (intents-contract, contract, near, defuse, 1, ., 1.18, false, --filter-paths "nod...
  • GitHub Check: Shared security analysis / Static Analysis (intents-poa-factory, contract, near, poa-factory, 1, ., 1.18, false, --filter-pa...
  • GitHub Check: Shared security analysis / Static Analysis (intents-poa-token, contract, near, poa-token, 1, ., 1.18, false, --filter-paths ...
  • GitHub Check: Shared security analysis / Static Analysis (intents-contract, contract, near, defuse, 1, ., 1.18, false, --filter-paths "nod...
  • GitHub Check: Security Audit - report

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (8)
core/src/engine/state/mod.rs (1)

45-46: Mark salt check as must-use and clarify semantics.
Return value is significant; add #[must_use] and briefly document rotation window semantics (current vs previous salts).

-    /// Returns whether salt in nonce is valid
-    fn is_valid_salt(&self, salt: &Salt) -> bool;
+    /// Returns whether the provided salt is currently accepted (e.g., current or within a rotation window).
+    #[must_use]
+    fn is_valid_salt(&self, salt: &Salt) -> bool;
defuse/src/accounts.rs (1)

32-35: Update documentation to reflect per-nonce cleanup semantics.

The documentation states "Clears all expired nonces" but the implementation (in defuse/src/contract/accounts/mod.rs lines 54-76) performs per-nonce validation that checks both salt validity and expiration. The method now processes each nonce individually with version-specific cleanup logic rather than bulk-clearing expired nonces.

Consider updating the doc comment to:

-    /// Clears all expired nonces for given accounts.
-    /// Omitting any errors, e.g. if account doesn't exist or nonces are not expired.
+    /// Attempts to clean up the provided nonces for given accounts.
+    /// Each nonce is validated (salt validity, expiration) before cleanup.
+    /// Omits errors (e.g., account not found, nonce not cleanable).
     /// NOTE: MUST attach 1 yⓃ for security purposes.
defuse/src/contract/accounts/mod.rs (1)

58-61: Don’t revert on malformed nonce; skip and continue.

unwrap_or_panic() aborts the whole cleanup if any nonce is malformed. Prefer skipping bad inputs and optionally logging/counting them to make the call robust against mixed-quality batches.

-                let versioned = VersionedNonce::try_from(nonce)
-                    .map_err(|_| DefuseError::InvalidNonce)
-                    .unwrap_or_panic();
+                let versioned = match VersionedNonce::try_from(nonce) {
+                    Ok(v) => v,
+                    Err(_) => {
+                        // optionally: log/metric for invalid nonce
+                        continue;
+                    }
+                };
defuse/src/contract/salts.rs (1)

30-52: Tighten invalidate_salt error path (minor).

The then_some(()).ok_or(...).unwrap_or_panic() chain is fine; a small readability tweak avoids the unit dance.

-        self.salts
-            .clear_previous(salt)
-            .then_some(())
-            .ok_or(DefuseError::InvalidSalt)
-            .unwrap_or_panic();
+        if !self.salts.clear_previous(salt) {
+            DefuseError::InvalidSalt.panic();
+        }
tests/src/tests/defuse/accounts/nonces.rs (1)

27-37: Nice helper; consider passing rng by &mut to avoid moves.

Tweak signature to accept &mut impl Rng; call sites already pass &mut rng, so no changes elsewhere.

-fn create_random_salted_nonce(salt: [u8; 4], deadline: Deadline, mut rng: impl Rng) -> Nonce {
+fn create_random_salted_nonce(salt: [u8; 4], deadline: Deadline, rng: &mut impl Rng) -> Nonce {
     VersionedNonce::V1(SaltedNonce::new(
         salt,
         ExpirableNonce {
             deadline,
             nonce: rng.random::<[u8; 16]>(),
         },
     ))
     .try_into()
     .expect("unable to create salted nonce")
 }
core/src/nonce/salted.rs (2)

1-9: Missing import for size_of.

Line 22 uses size_of::<Salt>() but size_of is not explicitly imported. While it may be available through the prelude, explicit imports improve clarity.

Apply this diff to add the explicit import:

-use core::mem;
+use core::mem::{self, size_of};

45-52: Clarify return value: set_new() returns the OLD salt.

The method rotates to a new current salt but returns the previous one. While this may be intentional for event emission or tracking, the naming and documentation could be clearer about this behavior.

Consider either:

  1. Renaming to rotate_salt() with documentation: "Returns the old salt that was rotated out"
  2. Returning both old and new salts as a tuple: (Salt, Salt)
  3. Adding a doc comment explaining the return value represents the previous salt
tests/src/tests/defuse/state/extensions/salt.rs (1)

24-49: Consider if max_gas() is necessary for salt operations.

Both rotate_salt and invalidate_salt use .max_gas(), which attaches the maximum possible gas to the transaction. While this ensures operations won't fail due to gas limits, salt rotation is likely a lightweight operation that doesn't require maximum gas.

Consider specifying a reasonable fixed gas amount instead of max_gas() to:

  1. Make gas costs more predictable
  2. Avoid unnecessarily high gas attachment
  3. Better align with production usage patterns

Example:

-            .max_gas()
+            .gas(NearGas::from_tgas(5)) // Adjust based on actual gas profiling
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a5c2d45 and 40a10ad.

📒 Files selected for processing (17)
  • core/src/accounts.rs (2 hunks)
  • core/src/engine/mod.rs (3 hunks)
  • core/src/engine/state/cached.rs (4 hunks)
  • core/src/engine/state/deltas.rs (3 hunks)
  • core/src/engine/state/mod.rs (3 hunks)
  • core/src/events.rs (2 hunks)
  • core/src/nonce/salted.rs (1 hunks)
  • defuse/src/accounts.rs (1 hunks)
  • defuse/src/contract/accounts/account/mod.rs (1 hunks)
  • defuse/src/contract/accounts/mod.rs (2 hunks)
  • defuse/src/contract/intents/state.rs (3 hunks)
  • defuse/src/contract/salts.rs (1 hunks)
  • defuse/src/salts.rs (1 hunks)
  • tests/src/tests/defuse/accounts/mod.rs (4 hunks)
  • tests/src/tests/defuse/accounts/nonces.rs (5 hunks)
  • tests/src/tests/defuse/state/extensions/salt.rs (1 hunks)
  • tests/src/tests/defuse/state/salt.rs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • defuse/src/salts.rs
  • tests/src/tests/defuse/state/salt.rs
  • core/src/events.rs
🧰 Additional context used
🧬 Code graph analysis (13)
defuse/src/accounts.rs (2)
defuse/src/contract/accounts/mod.rs (1)
  • cleanup_nonces (55-77)
tests/src/tests/defuse/accounts/mod.rs (3)
  • cleanup_nonces (26-26)
  • cleanup_nonces (90-112)
  • cleanup_nonces (198-200)
core/src/engine/state/deltas.rs (7)
core/src/engine/state/cached.rs (3)
  • is_valid_salt (123-125)
  • cleanup_nonce (182-192)
  • cleanup_nonce (418-420)
core/src/engine/state/mod.rs (2)
  • is_valid_salt (46-46)
  • cleanup_nonce (65-65)
core/src/nonce/salted.rs (1)
  • is_valid_salt (89-91)
defuse/src/contract/intents/state.rs (2)
  • is_valid_salt (94-96)
  • cleanup_nonce (132-141)
defuse/src/contract/salts.rs (1)
  • is_valid_salt (55-57)
defuse/src/salts.rs (1)
  • is_valid_salt (15-15)
defuse/src/contract/accounts/account/mod.rs (1)
  • cleanup_nonce (156-158)
defuse/src/contract/intents/state.rs (6)
core/src/engine/state/cached.rs (3)
  • is_valid_salt (123-125)
  • cleanup_nonce (182-192)
  • cleanup_nonce (418-420)
core/src/engine/state/deltas.rs (2)
  • is_valid_salt (101-103)
  • cleanup_nonce (126-128)
core/src/engine/state/mod.rs (2)
  • is_valid_salt (46-46)
  • cleanup_nonce (65-65)
core/src/nonce/salted.rs (1)
  • is_valid_salt (89-91)
defuse/src/salts.rs (1)
  • is_valid_salt (15-15)
defuse/src/contract/accounts/account/mod.rs (1)
  • cleanup_nonce (156-158)
tests/src/tests/defuse/accounts/nonces.rs (5)
core/src/nonce/salted.rs (2)
  • new (33-41)
  • new (85-87)
defuse/src/contract/state/mod.rs (1)
  • new (28-41)
core/src/nonce/expirable.rs (1)
  • new (25-27)
defuse/src/contract/intents/state.rs (1)
  • is_nonce_used (65-70)
tests/src/tests/defuse/accounts/mod.rs (3)
  • is_nonce_used (41-41)
  • is_nonce_used (139-148)
  • is_nonce_used (223-225)
core/src/engine/state/cached.rs (5)
core/src/engine/state/deltas.rs (2)
  • is_valid_salt (101-103)
  • cleanup_nonce (126-128)
core/src/engine/state/mod.rs (2)
  • is_valid_salt (46-46)
  • cleanup_nonce (65-65)
core/src/nonce/salted.rs (1)
  • is_valid_salt (89-91)
defuse/src/contract/intents/state.rs (2)
  • is_valid_salt (94-96)
  • cleanup_nonce (132-141)
defuse/src/contract/accounts/account/mod.rs (1)
  • cleanup_nonce (156-158)
core/src/engine/mod.rs (2)
core/src/payload/mod.rs (1)
  • nonce (68-70)
core/src/nonce/versioned.rs (2)
  • try_from (31-33)
  • try_from (39-57)
defuse/src/contract/accounts/mod.rs (8)
defuse/src/accounts.rs (1)
  • cleanup_nonces (35-35)
borsh-utils/src/adapters/mod.rs (1)
  • into_inner (105-107)
core/src/nonce/versioned.rs (2)
  • try_from (31-33)
  • try_from (39-57)
core/src/engine/state/cached.rs (2)
  • cleanup_nonce (182-192)
  • cleanup_nonce (418-420)
core/src/engine/state/deltas.rs (1)
  • cleanup_nonce (126-128)
core/src/engine/state/mod.rs (1)
  • cleanup_nonce (65-65)
defuse/src/contract/accounts/account/mod.rs (1)
  • cleanup_nonce (156-158)
defuse/src/contract/intents/state.rs (1)
  • cleanup_nonce (132-141)
defuse/src/contract/salts.rs (8)
defuse/src/salts.rs (4)
  • rotate_salt (8-8)
  • invalidate_salt (12-12)
  • is_valid_salt (15-15)
  • get_current_salt (18-18)
tests/src/tests/defuse/state/extensions/salt.rs (12)
  • rotate_salt (6-6)
  • rotate_salt (24-33)
  • rotate_salt (72-74)
  • invalidate_salt (8-12)
  • invalidate_salt (35-49)
  • invalidate_salt (76-84)
  • is_valid_salt (14-18)
  • is_valid_salt (51-61)
  • is_valid_salt (86-94)
  • get_current_salt (20-20)
  • get_current_salt (63-68)
  • get_current_salt (96-98)
tests/src/tests/defuse/state/salt.rs (2)
  • rotate_salt (13-48)
  • invalidate_salt (52-107)
core/src/engine/state/cached.rs (1)
  • is_valid_salt (123-125)
core/src/engine/state/deltas.rs (1)
  • is_valid_salt (101-103)
core/src/engine/state/mod.rs (1)
  • is_valid_salt (46-46)
core/src/nonce/salted.rs (1)
  • is_valid_salt (89-91)
defuse/src/contract/intents/state.rs (1)
  • is_valid_salt (94-96)
tests/src/tests/defuse/accounts/mod.rs (2)
defuse/src/accounts.rs (1)
  • cleanup_nonces (35-35)
defuse/src/contract/accounts/mod.rs (1)
  • cleanup_nonces (55-77)
tests/src/tests/defuse/state/extensions/salt.rs (3)
defuse/src/contract/salts.rs (4)
  • rotate_salt (13-26)
  • invalidate_salt (30-52)
  • is_valid_salt (55-57)
  • get_current_salt (60-62)
defuse/src/salts.rs (4)
  • rotate_salt (8-8)
  • invalidate_salt (12-12)
  • is_valid_salt (15-15)
  • get_current_salt (18-18)
tests/src/tests/defuse/state/salt.rs (2)
  • rotate_salt (13-48)
  • invalidate_salt (52-107)
core/src/nonce/salted.rs (7)
core/src/engine/state/cached.rs (3)
  • new (33-38)
  • new (368-370)
  • is_valid_salt (123-125)
core/src/engine/state/deltas.rs (3)
  • new (33-38)
  • new (213-215)
  • is_valid_salt (101-103)
defuse/src/contract/state/mod.rs (1)
  • new (28-41)
core/src/engine/state/mod.rs (1)
  • is_valid_salt (46-46)
defuse/src/contract/intents/state.rs (1)
  • is_valid_salt (94-96)
defuse/src/contract/salts.rs (1)
  • is_valid_salt (55-57)
defuse/src/salts.rs (1)
  • is_valid_salt (15-15)
core/src/engine/state/mod.rs (7)
core/src/engine/state/cached.rs (3)
  • is_valid_salt (123-125)
  • cleanup_nonce (182-192)
  • cleanup_nonce (418-420)
core/src/engine/state/deltas.rs (2)
  • is_valid_salt (101-103)
  • cleanup_nonce (126-128)
core/src/nonce/salted.rs (1)
  • is_valid_salt (89-91)
defuse/src/contract/intents/state.rs (2)
  • is_valid_salt (94-96)
  • cleanup_nonce (132-141)
defuse/src/contract/salts.rs (1)
  • is_valid_salt (55-57)
defuse/src/salts.rs (1)
  • is_valid_salt (15-15)
defuse/src/contract/accounts/account/mod.rs (1)
  • cleanup_nonce (156-158)
defuse/src/contract/accounts/account/mod.rs (5)
core/src/engine/state/cached.rs (2)
  • cleanup_nonce (182-192)
  • cleanup_nonce (418-420)
core/src/engine/state/deltas.rs (1)
  • cleanup_nonce (126-128)
core/src/engine/state/mod.rs (1)
  • cleanup_nonce (65-65)
defuse/src/contract/intents/state.rs (1)
  • cleanup_nonce (132-141)
core/src/payload/mod.rs (1)
  • nonce (68-70)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: Build
  • GitHub Check: Shared security analysis / Static Analysis (intents-poa-factory, contract, near, poa-factory, 1, ., 1.18, false, --filter-pa...
  • GitHub Check: Shared security analysis / Static Analysis (intents-contract, contract, near, defuse, 1, ., 1.18, false, --filter-paths "nod...
  • GitHub Check: Shared security analysis / Static Analysis (intents-poa-token, contract, near, poa-token, 1, ., 1.18, false, --filter-paths ...
  • GitHub Check: Shared / Static Analysis (intents-contract, contract, near, defuse, 1, ., 1.18, false, --filter-paths "nod...
  • GitHub Check: Shared / Static Analysis (intents-poa-token, contract, near, poa-token, 1, ., 1.18, false, --filter-paths ...
  • GitHub Check: Shared / Static Analysis (intents-poa-factory, contract, near, poa-factory, 1, ., 1.18, false, --filter-pa...
  • GitHub Check: Security Audit - report
🔇 Additional comments (36)
core/src/engine/state/mod.rs (1)

5-5: Import looks correct.
No issues with adding Salt here.

core/src/accounts.rs (1)

7-7: Import looks correct.
No issues.

core/src/engine/mod.rs (2)

9-12: Imports look good.
Matches usages below.


87-114: Clarify legacy nonce policy and add missing tests

  • Document or feature-gate the migration window for accepting Legacy nonces.
  • Add unit tests covering:
    • acceptance of legacy nonces
    • invalid salt error
    • expired nonce error
    intent_deadline > nonce_deadline error
defuse/src/contract/intents/state.rs (3)

2-2: LGTM - Salt import added.

Correctly imports Salt from defuse_core to support the new salt validation functionality.


94-96: LGTM - Salt validation properly delegated.

The is_valid_salt method correctly delegates to the salts manager, consistent with the StateView trait requirements.


132-140: LGTM - Per-nonce cleanup correctly implemented.

The method correctly transitions from batch cleanup to per-nonce processing, calling account.cleanup_nonce(nonce) for a single nonce as expected by the updated API.

defuse/src/contract/accounts/account/mod.rs (1)

152-158: LGTM - Method renamed with updated documentation.

The rename from clear_expired_nonce to cleanup_nonce aligns with the broader refactoring to per-nonce cleanup semantics. The updated documentation accurately reflects that nonces are cleared if "expired/invalidated," which properly encompasses both expiration and salt invalidation scenarios introduced with versioned nonces.

core/src/engine/state/cached.rs (4)

2-2: LGTM - Salt import added.

Correctly adds Salt to the crate imports to support the new salt validation functionality.


123-125: LGTM - Salt validation delegated to view.

The is_valid_salt method correctly delegates to the inner view, consistent with the StateView trait contract and the forwarding pattern used throughout this cached state implementation.


182-191: LGTM - Per-nonce cleanup correctly implemented.

The method correctly transitions from batch cleanup_expired_nonces to per-nonce cleanup_nonce, processing a single nonce and calling account.cleanup_nonce(nonce) as expected by the updated State trait.


418-420: LGTM - CachedAccount method renamed consistently.

The rename from clear_expired_nonce to cleanup_nonce maintains consistency with the broader API refactoring while preserving the existing behavior through the unchanged delegation to self.nonces.clear_expired(n).

core/src/engine/state/deltas.rs (3)

2-2: LGTM - Salt import added.

Correctly adds Salt to the crate imports to support the new salt validation functionality in the StateView implementation.


100-103: LGTM - Salt validation delegated to underlying state.

The is_valid_salt method correctly delegates to self.state, consistent with the Deltas wrapper pattern that forwards all StateView operations to the underlying state.


126-128: LGTM - Per-nonce cleanup correctly delegated.

The method correctly transitions from batch cleanup to per-nonce processing, delegating cleanup_nonce to the underlying state as expected by the Deltas wrapper pattern and the updated State trait.

defuse/src/contract/salts.rs (1)

11-26: Rotate salt flow looks solid (ACL + event + return).

Please confirm clear_previous in invalidate_salt only permits clearing the previous salt (and not arbitrary historical salts). If broader invalidation is required, we may need a loop/guard in clear_previous implementation.

tests/src/tests/defuse/accounts/mod.rs (3)

26-26: API rename/signature looks consistent.


90-112: Marshalling B64 nonces and contract call LGTM.

Correctly converts to Vec<AsBase64> and invokes cleanup_nonces on the defuse contract.


198-200: Wrapper delegation is correct.

tests/src/tests/defuse/accounts/nonces.rs (12)

42-71: Legacy nonce acceptance check reads well.

Covers the Legacy path and is_nonce_used assertion. Good baseline.


72-88: Invalid salt path exercised.

Good negative test for random salt mismatch.


90-106: “Deadline greater than nonce” validation covered.

Asserts protocol-level constraint; useful guard.


108-124: Expired nonce rejection covered.


126-149: Happy-path commit with current salt looks good.


151-183: Commit with previous salt after rotation is validated.

Great coverage of “previous salt still valid” rule.


185-208: Commit with invalidated current salt rejected.

Covers the invalidate_salt control path well.


212-237: Commit setup for cleanup_nonces test is comprehensive.


271-284: Expired nonce cleanup verifies removal.


286-313: Idempotency/skip cases are well covered.


315-337: Cleanup on invalidated salt is validated.

Matches contract-side “invalid salt OR expired” cleanable rule.


351-362: Batch cleanup/gas measurement test is useful.

Good coverage for chunked commits and a single cleanup batch.

Also applies to: 382-391

defuse/src/contract/accounts/mod.rs (1)

62-70: No action required — invalid-salt nonces are cleared
cleanup_nonce is only invoked when a nonce is expired or has an invalid salt, and clear_expired unconditionally deletes that nonce. Invalid-salt nonces will therefore be removed as intended.

core/src/nonce/salted.rs (2)

94-160: Well-structured test coverage.

The test module provides good coverage of the salt lifecycle:

  • Salt validity checks
  • Rotation behavior (both old and new salts remain valid)
  • Invalidation of previous salts
  • SaltedNonce validation against multiple expected salts

The use of rstest fixtures and property-based testing with arbitrary strengthens the test suite.


89-91: Salt validation API distinctions are intentional.

SaltedNonce::is_valid_salt(&[Salt]) checks a nonce’s salt against a list of allowed salts, whereas ValidSalts::is_valid(&Salt) (and its State::is_valid_salt(&Salt) wrappers) validate a single salt against stored valid salts. The differing signatures reflect these distinct use cases.

tests/src/tests/defuse/state/extensions/salt.rs (2)

1-21: LGTM: Clean trait definition for test utilities.

The SaltManagerExt trait provides a clear async interface for salt management operations in integration tests, with proper error handling through anyhow::Result.


71-99: LGTM: Clean delegation pattern.

The Contract implementation appropriately delegates to the Account implementation through .as_account(), avoiding code duplication while providing a uniform interface for test utilities.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
core/src/accounts.rs (1)

67-73: Add serde_as attributes and Base64 encoding to align with NonceEvent pattern.

SaltRotationEvent is missing the same serde attributes that NonceEvent uses (lines 45-58). Without these, the salt fields won't encode as Base64 in JSON serialization, causing ABI schema inconsistencies.

Apply this diff to add the required attributes:

+#[cfg_attr(
+    all(feature = "abi", not(target_arch = "wasm32")),
+    serde_as(schemars = true)
+)]
+#[cfg_attr(
+    not(all(feature = "abi", not(target_arch = "wasm32"))),
+    serde_as(schemars = false)
+)]
+#[serde_as]
 #[must_use = "make sure to `.emit()` this event"]
 #[near(serializers = [json])]
 #[derive(Debug, Clone)]
 pub struct SaltRotationEvent {
+    #[serde_as(as = "Base64")]
     pub old_salt: Salt,
+    #[serde_as(as = "Base64")]
     pub new_salt: Salt,
 }
🧹 Nitpick comments (4)
defuse/src/contract/accounts/mod.rs (4)

57-64: Drop the unused error mapping; simplify the conversion.

You don’t surface DefuseError::InvalidNonce here; map_err is redundant. Simpler and a touch cheaper:

-                let Ok(versioned) =
-                    VersionedNonce::try_from(nonce).map_err(|_| DefuseError::InvalidNonce)
-                else {
-                    continue;
-                };
+                let Ok(versioned) = VersionedNonce::try_from(nonce) else {
+                    // invalid/unknown nonce encoding → skip
+                    continue;
+                };

65-73: Encapsulate “cleanable” predicate for clarity and unit testing.

Extract the match into a small helper (e.g., fn is_cleanable(&self, v: &VersionedNonce) -> bool). This improves readability and lets you unit‑test edge cases (salt invalid but not expired, expired but valid salt, legacy).

Quick check: ensure ExpirableNonce::has_expired() uses the same time base as NEAR env (block_timestamp in ns vs ms), or handles conversion internally.


74-78: Avoid fully swallowing errors; at least log or count unexpected ones.

Ignoring all cleanup errors makes diagnosing data issues hard (e.g., AccountNotFound vs storage corruption). Consider counting outcomes and emitting a compact event/log, or only suppressing “benign” cases while noting the rest.

Example shape:

-                if cleanable {
-                    State::cleanup_nonce(self, &account_id, nonce).ok();
-                }
+                if cleanable {
+                    if let Err(e) = State::cleanup_nonce(self, &account_id, nonce) {
+                        // Allow benign misses; record others.
+                        if !matches!(e, DefuseError::AccountNotFound(_) | DefuseError::NonceNotFound) {
+                            near_sdk::env::log_str(&format!("cleanup_nonces: failed {:?} for {}", e, account_id));
+                        }
+                    }
+                }

55-79: Reduce repeated account lookups and deduplicate inputs to save gas.

  • You call State::cleanup_nonce once per nonce, which re‑looks up the account each time. If layering allows, fetch the account lock once per account_id and call account.cleanup_nonce(nonce) in the inner loop.
  • Deduplicate nonces per account before looping (HashSet/BTreeSet) to avoid redundant cleanups when callers pass duplicates.

Both are optional but can materially reduce storage reads on large batches.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 40a10ad and 0728277.

📒 Files selected for processing (4)
  • core/src/accounts.rs (2 hunks)
  • core/src/events.rs (2 hunks)
  • defuse/src/contract/accounts/mod.rs (2 hunks)
  • defuse/src/contract/salts.rs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • core/src/events.rs
🧰 Additional context used
🧬 Code graph analysis (2)
defuse/src/contract/accounts/mod.rs (6)
defuse/src/accounts.rs (1)
  • cleanup_nonces (35-35)
tests/src/tests/defuse/accounts/mod.rs (3)
  • cleanup_nonces (26-26)
  • cleanup_nonces (90-112)
  • cleanup_nonces (198-200)
core/src/nonce/versioned.rs (2)
  • try_from (31-33)
  • try_from (39-57)
core/src/engine/state/mod.rs (1)
  • cleanup_nonce (65-65)
core/src/engine/state/cached.rs (2)
  • cleanup_nonce (182-192)
  • cleanup_nonce (418-420)
defuse/src/contract/intents/state.rs (1)
  • cleanup_nonce (132-141)
defuse/src/contract/salts.rs (1)
defuse/src/salts.rs (4)
  • rotate_salt (8-8)
  • invalidate_salt (12-12)
  • is_valid_salt (15-15)
  • get_current_salt (18-18)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: Tests
🔇 Additional comments (3)
defuse/src/contract/salts.rs (2)

54-62: LGTM! Read-only accessors are well-structured.

The is_valid_salt and get_current_salt methods correctly delegate to the underlying salt storage without mutation or unnecessary complexity. The inline hints are appropriate for these simple accessors.


33-41: Provide ValidSalts methods for verification.

Please share the implementations of set_new() and clear_previous() from defuse/src/contract/state/v0.rs so we can confirm that the old salt is added to previous before being cleared.

defuse/src/contract/accounts/mod.rs (1)

10-10: Imports align with versioned/salted nonces — OK

The expanded imports match the new nonce model and are used below. No issues.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (6)
core/src/engine/mod.rs (1)

77-79: Invoke cleanup after committing the nonce to prevent stale state growth.

After commit_nonce, invoke cleanup_nonce for the signer to purge expired entries. This prevents unbounded accumulation of expired nonces in storage.

Apply this diff:

 self.verify_intent_nonce(nonce, deadline)?;
 self.state.commit_nonce(signer_id.clone(), nonce)?;
+self.state.cleanup_nonce(&signer_id, nonce)?;
core/src/nonce/salted.rs (1)

19-29: Salt generation is predictable within the same block.

env::random_seed_array() returns the same value for all calls within a single block. If set_new is invoked multiple times in one transaction or block, all generated salts will be identical, undermining the rotation mechanism.

Consider one of these approaches:

  1. Combine random_seed with additional entropy (e.g., env::block_timestamp(), account ID, or a per-contract counter)
  2. Document this limitation if the current behavior is acceptable for your threat model
  3. Use a deterministic counter combined with block hash

Example mixing entropy:

 fn random() -> Self {
     const SIZE: usize = size_of::<Salt>();
-    let seed = &env::random_seed_array()[..SIZE];
+    let seed = env::random_seed_array();
+    let timestamp = env::block_timestamp().to_le_bytes();
+    let mut combined = [0u8; 32];
+    for i in 0..32 {
+        combined[i] = seed[i] ^ timestamp[i % 8];
+    }
     let mut result = [0u8; SIZE];
-
-    result.copy_from_slice(seed);
+    result.copy_from_slice(&combined[..SIZE]);
 
     Self(result)
 }
core/src/accounts.rs (1)

67-81: Add Base64 encoding to salt event fields for ABI consistency.

RotateSaltEvent and InvalidateSaltEvent should encode salt fields as Base64, matching NonceEvent's pattern. This ensures consistent schema generation and serialization.

Apply this diff:

+#[cfg_attr(all(feature = "abi", not(target_arch = "wasm32")), serde_as(schemars = true))]
+#[cfg_attr(not(all(feature = "abi", not(target_arch = "wasm32"))), serde_as(schemars = false))]
+#[serde_as]
 #[must_use = "make sure to `.emit()` this event"]
 #[near(serializers = [json])]
 #[derive(Debug, Clone)]
 pub struct RotateSaltEvent {
+    #[serde_as(as = "Base64")]
     pub old_salt: Salt,
+    #[serde_as(as = "Base64")]
     pub new_salt: Salt,
 }

+#[cfg_attr(all(feature = "abi", not(target_arch = "wasm32")), serde_as(schemars = true))]
+#[cfg_attr(not(all(feature = "abi", not(target_arch = "wasm32"))), serde_as(schemars = false))]
+#[serde_as]
 #[must_use = "make sure to `.emit()` this event"]
 #[near(serializers = [json])]
 #[derive(Debug, Clone)]
 pub struct InvalidateSaltEvent {
+    #[serde_as(as = "Base64")]
     pub current: Salt,
+    #[serde_as(as = "Base64")]
     pub invalidated: Salt,
 }

Based on coding guidelines

defuse/src/contract/accounts/mod.rs (2)

58-65: Avoid duplicating nonce validation logic; extract a shared helper

Logic to decide cleanable (salt validity/expiry) duplicates engine verification. Extract into a StateView extension/helper and reuse here and in engine.

If you want, I can draft a StateViewExt with is_valid_nonce(...) and update both call sites.


55-73: Add 1‑yocto guard to public state‑changing cleanup_nonces

This method mutates state and should be guarded to prevent accidental cross‑contract calls. Mark #[payable] and assert_one_yocto() at entry.

Apply this diff:

-    fn cleanup_nonces(&mut self, nonces: Vec<(AccountId, Vec<AsBase64<Nonce>>)>) {
+    #[payable]
+    fn cleanup_nonces(&mut self, nonces: Vec<(AccountId, Vec<AsBase64<Nonce>>)>) {
+        assert_one_yocto();
core/src/nonce/versioned.rs (1)

73-77: Fix legacy deserialization: wrap prefix with Cursor before chaining

[u8; 4] doesn’t implement Read. Use io::Cursor to prepend the consumed bytes.

Apply this diff:

-        let versioned = if prefix == Self::VERSIONED_MAGIC_PREFIX {
-            VersionedNonce::deserialize_reader(reader)?
-        } else {
-            VersionedNonce::Legacy(Nonce::deserialize_reader(&mut prefix.chain(reader))?)
-        };
+        let versioned = if prefix == Self::VERSIONED_MAGIC_PREFIX {
+            VersionedNonce::deserialize_reader(reader)?
+        } else {
+            let mut chained = io::Cursor::new(prefix).chain(reader);
+            VersionedNonce::Legacy(Nonce::deserialize_reader(&mut chained)?)
+        };
🧹 Nitpick comments (4)
core/src/engine/state/cached.rs (1)

182-191: Single‑nonce cleanup implementation is sound; consider surfacing result

Logic mirrors defuse intent state; returns Ok(()) regardless of whether anything changed. Optional: log/metric (or return bool upstream) to aid observability/debugging.

Would you like me to add a debug log counter for successful/failed nonce cleanups?

core/src/nonce/versioned.rs (1)

105-115: Stabilize test to avoid rare magic‑prefix collision

A random Nonce has a 1/2^32 chance to match VERSIONED_MAGIC_PREFIX, flipping Legacy→V1 parsing. Force a non‑matching prefix in the fixture.

Apply this diff:

-        let nonce: Nonce = u.arbitrary().unwrap();
+        let mut nonce: Nonce = u.arbitrary().unwrap();
+        if nonce[..4] == MaybeVersionedNonce::VERSIONED_MAGIC_PREFIX {
+            nonce[0] ^= 0x01; // ensure it’s not interpreted as V1
+        }
tests/src/tests/defuse/accounts/nonces.rs (2)

55-56: Consider using more entropy or a fixed seed for test reproducibility.

The test generates SigningStandard using only 1 byte of randomness (rng.random::<[u8; 1]>()), which limits the variability. For better test coverage, consider using more random bytes or document why 1 byte is sufficient. Alternatively, for reproducibility, consider using a fixed seed or deterministic SigningStandard selection.

Also applies to: 81-82, 99-100, 117-118, 135-136, 169-170, 200-201


342-393: LGTM! Appropriate performance test for batch cleanup.

The parameterized test correctly validates cleanup scalability by testing with 1, 10, and 100 nonces. The chunked commit approach (CHUNK_SIZE=10) avoids transaction size limits, and gas measurement provides useful performance data.

Consider documenting why CHUNK_SIZE = 10 was chosen (e.g., transaction size limits, gas constraints) for better maintainability:

-    const CHUNK_SIZE: usize = 10;
+    // Chunk size chosen to stay within transaction size/gas limits
+    const CHUNK_SIZE: usize = 10;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0728277 and cf75a19.

📒 Files selected for processing (21)
  • core/src/accounts.rs (2 hunks)
  • core/src/engine/mod.rs (3 hunks)
  • core/src/engine/state/cached.rs (4 hunks)
  • core/src/engine/state/deltas.rs (3 hunks)
  • core/src/engine/state/mod.rs (3 hunks)
  • core/src/events.rs (2 hunks)
  • core/src/nonce/mod.rs (1 hunks)
  • core/src/nonce/salted.rs (1 hunks)
  • core/src/nonce/versioned.rs (1 hunks)
  • defuse/src/contract/accounts/account/mod.rs (1 hunks)
  • defuse/src/contract/accounts/account/nonces.rs (5 hunks)
  • defuse/src/contract/accounts/mod.rs (2 hunks)
  • defuse/src/contract/intents/state.rs (3 hunks)
  • defuse/src/contract/mod.rs (4 hunks)
  • defuse/src/contract/salts.rs (1 hunks)
  • defuse/src/contract/state/mod.rs (4 hunks)
  • defuse/src/contract/state/versioned.rs (1 hunks)
  • defuse/src/salts.rs (1 hunks)
  • tests/src/tests/defuse/accounts/nonces.rs (5 hunks)
  • tests/src/tests/defuse/state/extensions/salt.rs (1 hunks)
  • tests/src/tests/defuse/state/salt.rs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (6)
  • tests/src/tests/defuse/state/salt.rs
  • core/src/events.rs
  • defuse/src/contract/salts.rs
  • core/src/engine/state/mod.rs
  • core/src/nonce/mod.rs
  • defuse/src/contract/intents/state.rs
🧰 Additional context used
🧬 Code graph analysis (13)
defuse/src/salts.rs (2)
defuse/src/contract/salts.rs (4)
  • rotate_salt (17-30)
  • invalidate_salt (34-52)
  • is_valid_salt (55-57)
  • get_current_salt (60-62)
tests/src/tests/defuse/state/extensions/salt.rs (12)
  • rotate_salt (6-10)
  • rotate_salt (28-42)
  • rotate_salt (81-89)
  • invalidate_salt (12-16)
  • invalidate_salt (44-58)
  • invalidate_salt (91-99)
  • is_valid_salt (18-22)
  • is_valid_salt (60-70)
  • is_valid_salt (101-109)
  • get_current_salt (24-24)
  • get_current_salt (72-77)
  • get_current_salt (111-113)
core/src/engine/state/cached.rs (6)
core/src/engine/state/deltas.rs (2)
  • is_valid_salt (101-103)
  • cleanup_nonce (126-128)
core/src/engine/state/mod.rs (2)
  • is_valid_salt (46-46)
  • cleanup_nonce (65-65)
defuse/src/contract/intents/state.rs (2)
  • is_valid_salt (94-96)
  • cleanup_nonce (132-141)
defuse/src/contract/salts.rs (1)
  • is_valid_salt (55-57)
defuse/src/salts.rs (1)
  • is_valid_salt (17-17)
defuse/src/contract/accounts/account/mod.rs (1)
  • cleanup_nonce (156-158)
defuse/src/contract/state/mod.rs (4)
core/src/nonce/salted.rs (2)
  • new (51-59)
  • new (109-111)
defuse/src/contract/accounts/account/mod.rs (1)
  • new (48-65)
defuse/src/contract/accounts/mod.rs (1)
  • new (106-115)
defuse/src/contract/mod.rs (1)
  • new (88-97)
core/src/engine/mod.rs (2)
core/src/nonce/versioned.rs (2)
  • from (31-33)
  • from (39-55)
core/src/nonce/salted.rs (1)
  • from (32-36)
tests/src/tests/defuse/state/extensions/salt.rs (3)
defuse/src/contract/salts.rs (4)
  • rotate_salt (17-30)
  • invalidate_salt (34-52)
  • is_valid_salt (55-57)
  • get_current_salt (60-62)
defuse/src/salts.rs (4)
  • rotate_salt (9-9)
  • invalidate_salt (14-14)
  • is_valid_salt (17-17)
  • get_current_salt (20-20)
tests/src/tests/defuse/state/salt.rs (2)
  • rotate_salt (13-69)
  • invalidate_salt (73-128)
core/src/nonce/salted.rs (3)
core/src/nonce/versioned.rs (2)
  • from (31-33)
  • from (39-55)
defuse/src/contract/state/v0.rs (1)
  • from (21-38)
defuse/src/contract/state/mod.rs (1)
  • new (28-41)
tests/src/tests/defuse/accounts/nonces.rs (4)
core/src/nonce/salted.rs (2)
  • new (51-59)
  • new (109-111)
defuse/src/contract/state/mod.rs (1)
  • new (28-41)
defuse/src/contract/intents/state.rs (1)
  • is_nonce_used (65-70)
tests/src/tests/defuse/accounts/mod.rs (3)
  • is_nonce_used (41-41)
  • is_nonce_used (139-148)
  • is_nonce_used (223-225)
defuse/src/contract/accounts/mod.rs (6)
defuse/src/accounts.rs (1)
  • cleanup_nonces (35-35)
core/src/nonce/versioned.rs (2)
  • from (31-33)
  • from (39-55)
core/src/nonce/salted.rs (1)
  • from (32-36)
core/src/engine/state/cached.rs (2)
  • cleanup_nonce (182-192)
  • cleanup_nonce (418-420)
core/src/engine/state/deltas.rs (1)
  • cleanup_nonce (126-128)
core/src/engine/state/mod.rs (1)
  • cleanup_nonce (65-65)
defuse/src/contract/state/versioned.rs (1)
core/src/nonce/versioned.rs (5)
  • size_of (41-41)
  • from (31-33)
  • from (39-55)
  • deserialize_as (66-80)
  • serialize_as (84-92)
defuse/src/contract/accounts/account/mod.rs (5)
core/src/engine/state/cached.rs (2)
  • cleanup_nonce (182-192)
  • cleanup_nonce (418-420)
core/src/engine/state/deltas.rs (1)
  • cleanup_nonce (126-128)
core/src/engine/state/mod.rs (1)
  • cleanup_nonce (65-65)
defuse/src/contract/intents/state.rs (1)
  • cleanup_nonce (132-141)
core/src/payload/mod.rs (1)
  • nonce (68-70)
defuse/src/contract/accounts/account/nonces.rs (1)
core/src/nonce/mod.rs (2)
  • cleanup (50-54)
  • new (31-33)
core/src/nonce/versioned.rs (3)
core/src/nonce/salted.rs (5)
  • size_of (21-21)
  • from (32-36)
  • random (20-28)
  • new (51-59)
  • new (109-111)
defuse/src/contract/state/versioned.rs (6)
  • size_of (72-72)
  • from (41-46)
  • from (51-54)
  • from (59-61)
  • deserialize_as (65-87)
  • serialize_as (94-104)
core/src/nonce/expirable.rs (1)
  • new (25-27)
core/src/engine/state/deltas.rs (6)
core/src/engine/state/cached.rs (3)
  • is_valid_salt (123-125)
  • cleanup_nonce (182-192)
  • cleanup_nonce (418-420)
core/src/engine/state/mod.rs (2)
  • is_valid_salt (46-46)
  • cleanup_nonce (65-65)
defuse/src/contract/intents/state.rs (2)
  • is_valid_salt (94-96)
  • cleanup_nonce (132-141)
defuse/src/contract/salts.rs (1)
  • is_valid_salt (55-57)
defuse/src/salts.rs (1)
  • is_valid_salt (17-17)
defuse/src/contract/accounts/account/mod.rs (1)
  • cleanup_nonce (156-158)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: Build
  • GitHub Check: Shared security analysis / Static Analysis (intents-poa-factory, contract, near, poa-factory, 1, ., 1.18, false, --filter-pa...
  • GitHub Check: Shared security analysis / Static Analysis (intents-poa-token, contract, near, poa-token, 1, ., 1.18, false, --filter-paths ...
  • GitHub Check: Shared security analysis / Static Analysis (intents-contract, contract, near, defuse, 1, ., 1.18, false, --filter-paths "nod...
  • GitHub Check: Shared / Static Analysis (intents-poa-token, contract, near, poa-token, 1, ., 1.18, false, --filter-paths ...
  • GitHub Check: Shared / Static Analysis (intents-poa-factory, contract, near, poa-factory, 1, ., 1.18, false, --filter-pa...
  • GitHub Check: Shared / Static Analysis (intents-contract, contract, near, defuse, 1, ., 1.18, false, --filter-paths "nod...
  • GitHub Check: Security Audit - report
🔇 Additional comments (20)
core/src/engine/mod.rs (1)

87-111: Nonce verification logic looks correct for V1 salted nonces.

The pre-commit verification correctly:

  • Validates the salt via is_valid_salt
  • Ensures intent deadline ≤ nonce deadline
  • Checks nonce expiration
  • Allows legacy nonces without checks
core/src/nonce/salted.rs (1)

48-93: ValidSalts logic correctly manages current and previous salts.

The rotation and invalidation logic is sound:

  • set_new rotates the current salt and optionally invalidates the previous one
  • invalidate handles both current and previous salts
  • is_valid checks both current and previous valid salts
defuse/src/salts.rs (1)

1-21: SaltManager trait API is well-designed.

The trait provides a clean public interface for salt lifecycle management with clear method names and documentation. The cross-contract annotation enables proper external contract calls.

tests/src/tests/defuse/state/extensions/salt.rs (1)

1-114: Test extension trait follows established patterns.

SaltManagerExt correctly implements the test helper pattern:

  • Async methods for all SaltManager operations
  • Proper deposit (1 yoctoNear) for state-changing methods
  • Max gas allocation for transactions
  • Contract implementation delegates to Account
  • View methods use .view() without deposit
defuse/src/contract/accounts/account/mod.rs (1)

152-158: Method rename aligns with codebase conventions.

Renaming clear_expired_nonce to cleanup_nonce improves clarity and consistency with the broader cleanup semantics introduced in this PR. The documentation correctly reflects the expanded scope (expired/invalidated nonces).

defuse/src/contract/state/mod.rs (1)

1-49: v0 migration prefix nesting is consistent
Confirmed that ValidSalts::new(ContractPrefix::State.into_storage_key().nest(StatePrefix::Salts)) mirrors the .as_slice().nest(Prefix::Salts) pattern in mod.rs.

core/src/engine/state/cached.rs (2)

122-125: Forwarder for is_valid_salt looks correct

Matches StateView trait and delegates to inner view. No issues.


418-420: CachedAccount::cleanup_nonce rename and delegation LGTM

Renamed API and delegation to Nonces::cleanup is consistent across layers.

defuse/src/contract/accounts/account/nonces.rs (1)

72-74: Rename to cleanup and test updates are consistent

Delegation to Nonces::cleanup and tests (including legacy nonces not cleanable) align with intended semantics.

Also applies to: 132-147, 199-209

core/src/engine/state/deltas.rs (2)

100-103: is_valid_salt delegation LGTM

Straightforward forwarder to inner state view.


126-128: cleanup_nonce delegation LGTM

Shifts to single‑nonce cleanup as per updated trait; correct delegation.

defuse/src/contract/mod.rs (1)

9-9: Versioned state (de)serialization wiring is correct

Using As on Contract.state matches the provided BorshSerializeAs/BorshDeserializeAs impls. Module exposure and SaltManager role addition look good.

Also applies to: 16-16, 30-31, 71-75

tests/src/tests/defuse/accounts/nonces.rs (8)

1-27: LGTM! Imports align with salted nonce testing requirements.

The updated imports correctly expose the necessary types (Salt, SaltedNonce, VersionedNonce, Role) and trait extensions (SaltManagerExt, AclExt) to support the new salted/versioned nonce test scenarios.


29-38: LGTM! Helper function correctly constructs versioned salted nonces.

The helper appropriately creates a VersionedNonce::V1 containing a SaltedNonce with the provided salt, deadline, and a random 15-byte nonce, reducing code duplication across test cases.


153-185: LGTM! Correctly tests salt rotation with old salt preservation.

The test appropriately grants SaltManager role, rotates the salt with preserve_old=false, and verifies that nonces signed with the previous salt remain valid. This validates the salt lifecycle management.


187-209: LGTM! Correctly tests that invalidated salts are rejected.

The test appropriately invalidates the current salt and verifies that nonces signed with the invalidated salt are rejected with an "invalid salt" error.


273-286: LGTM! Correctly tests cleanup of expired nonces.

The test appropriately waits for the deadline to pass, calls cleanup_nonces, and verifies the expired nonce is no longer marked as used.


288-315: LGTM! Comprehensive edge case coverage for cleanup skipping.

The test correctly verifies that cleanup_nonces skips:

  • Already cleaned nonces (reusing expirable_nonce from the previous block)
  • Legacy nonces (non-expirable)
  • Non-expired nonces
  • Nonces for non-existent users

This ensures the cleanup logic is robust and handles various edge cases gracefully.


73-90: Salt construction is correct. Salt wraps a fixed [u8; 4] and its From<&[u8]> impl copies exactly four bytes, so providing a 4-byte slice is safe and won’t fail.


317-339: cleanup_nonces intentionally clears nonces with invalidated salts. Implementation uses !is_valid_salt(salt) || deadline.has_expired(), so removing non-expired nonces for invalid salts is intended and this test is correct.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
defuse/src/contract/accounts/mod.rs (1)

55-55: Enforce the 1 yocto guard on cleanup_nonces_by_prefix.

This public, state-changing method must be marked #[payable] and begin with assert_one_yocto() to prevent accidental cross-contract calls. This was flagged as a critical issue in a previous review and remains unresolved.

Apply this diff to add the guard:

+    #[payable]
     fn cleanup_nonces_by_prefix(&mut self, nonces: Vec<(AccountId, Vec<AsBase64<Nonce>>)>) {
+        assert_one_yocto();
         for (account_id, nonces) in nonces {

Additionally, update all test call sites in tests/src/tests/defuse/accounts/… to attach a 1 yocto deposit when calling cleanup_nonces_by_prefix.

🧹 Nitpick comments (6)
defuse/src/contract/accounts/mod.rs (2)

58-65: Consider extracting nonce validation logic into a reusable trait method.

The cleanability check duplicates logic found in Engine::verify_intent_nonce(). A previous reviewer suggested unifying this via a StateViewExt trait with an is_valid_nonce method that both locations can call.

Consider this pattern:

pub trait StateViewExt: StateView {
    fn is_valid_nonce(&self, nonce: Nonce) -> bool {
        match VersionedNonce::from(nonce) {
            VersionedNonce::V1(SaltedNonce {
                salt,
                nonce: ExpirableNonce { deadline, .. },
            }) => !deadline.has_expired() && self.is_valid_salt(salt),
            VersionedNonce::Legacy(_) => true,
        }
    }
}
impl<T> StateViewExt for T where T: StateView {}

Then simplify this code to:

let cleanable = !self.is_valid_nonce(nonce);

67-74: Document why errors are intentionally suppressed.

The comment "all errors are omitted" (line 71) without explanation may confuse maintainers. Clarify which error cases are expected (e.g., AccountNotFound, already-cleaned nonces) and why silent failure is acceptable here.

Consider expanding the comment:

-                // NOTE: all errors are omitted
+                // NOTE: Silently ignore errors (AccountNotFound, already-cleaned nonces, etc.)
+                // as this is a permissionless cleanup method that should not fail on partial success
defuse/src/contract/salts.rs (2)

33-36: Silent filtering of invalid salts may hide caller mistakes.

The filter on line 35 silently drops salts that are already invalid or don't exist. A caller expecting all provided salts to be invalidated won't receive feedback if some were already gone.

If callers need to know which salts failed, consider one of these approaches:

  1. Return a tuple (Salt, Vec<Salt>) where the second element lists successfully invalidated salts
  2. Log a warning for salts that couldn't be invalidated
  3. Add a separate query method to check salt validity before invalidation

Example for approach 1:

-    fn invalidate_salts(&mut self, salts: Vec<Salt>) -> Salt;
+    fn invalidate_salts(&mut self, salts: Vec<Salt>) -> (Salt, Vec<Salt>);
         let invalidated = salts
             .into_iter()
             .filter(|s| self.salts.invalidate(*s))
             .collect();

         let current = self.salts.current();

         SaltRotationEvent {
             current,
             invalidated,
         }
         .emit();

-        current
+        (current, invalidated)

17-18: Document the return value semantics of set_new.

According to a previous comment, set_new returns the previous salt, but the return value is discarded here. This could confuse readers who expect it to return the new salt.

Add a comment to clarify:

-        self.salts.set_new(false);
+        self.salts.set_new(false); // Returns previous salt (unused here)
         let current = self.salts.current();

Or if the return value is never needed in this context, consider requesting a set_new variant that returns nothing to make the intent clearer.

defuse/src/contract/accounts/account/mod.rs (1)

152-154: Fix grammar in comment.

The comment contains a grammatical error: "the all nonces" should be "all nonces".

Apply this diff:

-    /// Clears the all nonces with corresponding prefix if it was expired/invalidated.
+    /// Clears all nonces with the corresponding prefix if they were expired/invalidated.
core/src/nonce/versioned.rs (1)

92-102: Stabilize test: handle rare V1 prefix collision.

A random 32-byte Nonce has a ~1/2^32 chance of starting with VERSIONED_MAGIC_PREFIX, which would cause the test to expect VersionedNonce::V1 instead of Legacy. While extremely rare, this could make the test flaky.

Apply this diff to ensure the generated nonce doesn't match the V1 prefix:

     fn legacy_roundtrip_layout(random_bytes: Vec<u8>) {
         let mut u = Unstructured::new(&random_bytes);
-        let nonce: Nonce = u.arbitrary().unwrap();
+        let mut nonce: Nonce = u.arbitrary().unwrap();
+        // Ensure nonce doesn't start with V1_MAGIC_PREFIX to avoid rare collision
+        if nonce[..4] == MaybeVersionedNonce::VERSIONED_MAGIC_PREFIX {
+            nonce[0] ^= 0x01;
+        }
 
         let expected = VersionedNonce::from(nonce);
         assert_eq!(expected, VersionedNonce::Legacy(nonce));
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 56da699 and 28d9174.

📒 Files selected for processing (13)
  • bitmap/src/lib.rs (1 hunks)
  • core/src/engine/state/cached.rs (4 hunks)
  • core/src/engine/state/deltas.rs (3 hunks)
  • core/src/engine/state/mod.rs (3 hunks)
  • core/src/nonce/mod.rs (1 hunks)
  • core/src/nonce/salted.rs (1 hunks)
  • core/src/nonce/versioned.rs (1 hunks)
  • defuse/src/contract/accounts/account/mod.rs (2 hunks)
  • defuse/src/contract/accounts/account/nonces.rs (6 hunks)
  • defuse/src/contract/accounts/mod.rs (3 hunks)
  • defuse/src/contract/intents/state.rs (3 hunks)
  • defuse/src/contract/salts.rs (1 hunks)
  • defuse/src/contract/state/mod.rs (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • defuse/src/contract/intents/state.rs
  • core/src/nonce/salted.rs
  • core/src/nonce/mod.rs
🧰 Additional context used
🧬 Code graph analysis (10)
defuse/src/contract/accounts/account/mod.rs (4)
core/src/engine/state/cached.rs (2)
  • cleanup_nonce_by_prefix (182-196)
  • cleanup_nonce_by_prefix (422-424)
core/src/engine/state/deltas.rs (1)
  • cleanup_nonce_by_prefix (126-132)
core/src/engine/state/mod.rs (1)
  • cleanup_nonce_by_prefix (65-69)
defuse/src/contract/intents/state.rs (1)
  • cleanup_nonce_by_prefix (132-145)
defuse/src/contract/state/mod.rs (3)
core/src/nonce/salted.rs (2)
  • new (105-113)
  • new (163-165)
defuse/src/contract/mod.rs (1)
  • new (99-108)
defuse/src/contract/state/v0.rs (1)
  • new (50-62)
core/src/engine/state/deltas.rs (6)
core/src/engine/state/cached.rs (3)
  • is_valid_salt (123-125)
  • cleanup_nonce_by_prefix (182-196)
  • cleanup_nonce_by_prefix (422-424)
core/src/engine/state/mod.rs (2)
  • is_valid_salt (46-46)
  • cleanup_nonce_by_prefix (65-69)
defuse/src/contract/intents/state.rs (2)
  • is_valid_salt (94-96)
  • cleanup_nonce_by_prefix (132-145)
defuse/src/contract/salts.rs (1)
  • is_valid_salt (50-52)
defuse/src/salts.rs (1)
  • is_valid_salt (17-17)
defuse/src/contract/accounts/account/mod.rs (1)
  • cleanup_nonce_by_prefix (156-158)
bitmap/src/lib.rs (2)
core/src/nonce/mod.rs (1)
  • cleanup_by_prefix (51-53)
defuse/src/contract/accounts/account/nonces.rs (1)
  • cleanup_by_prefix (72-74)
core/src/engine/state/mod.rs (7)
core/src/engine/state/cached.rs (3)
  • is_valid_salt (123-125)
  • cleanup_nonce_by_prefix (182-196)
  • cleanup_nonce_by_prefix (422-424)
core/src/engine/state/deltas.rs (2)
  • is_valid_salt (101-103)
  • cleanup_nonce_by_prefix (126-132)
defuse/src/contract/intents/state.rs (2)
  • is_valid_salt (94-96)
  • cleanup_nonce_by_prefix (132-145)
defuse/src/contract/salts.rs (1)
  • is_valid_salt (50-52)
tests/src/tests/defuse/state/extensions/salt.rs (3)
  • is_valid_salt (14-18)
  • is_valid_salt (51-61)
  • is_valid_salt (88-96)
defuse/src/salts.rs (1)
  • is_valid_salt (17-17)
defuse/src/contract/accounts/account/mod.rs (1)
  • cleanup_nonce_by_prefix (156-158)
core/src/engine/state/cached.rs (6)
core/src/engine/state/deltas.rs (2)
  • is_valid_salt (101-103)
  • cleanup_nonce_by_prefix (126-132)
core/src/engine/state/mod.rs (2)
  • is_valid_salt (46-46)
  • cleanup_nonce_by_prefix (65-69)
defuse/src/contract/intents/state.rs (2)
  • is_valid_salt (94-96)
  • cleanup_nonce_by_prefix (132-145)
defuse/src/contract/salts.rs (1)
  • is_valid_salt (50-52)
defuse/src/salts.rs (1)
  • is_valid_salt (17-17)
defuse/src/contract/accounts/account/mod.rs (1)
  • cleanup_nonce_by_prefix (156-158)
defuse/src/contract/salts.rs (3)
tests/src/tests/defuse/state/extensions/salt.rs (12)
  • update_current_salt (6-6)
  • update_current_salt (24-33)
  • update_current_salt (72-76)
  • invalidate_salts (8-12)
  • invalidate_salts (35-49)
  • invalidate_salts (78-86)
  • is_valid_salt (14-18)
  • is_valid_salt (51-61)
  • is_valid_salt (88-96)
  • current_salt (20-20)
  • current_salt (63-68)
  • current_salt (98-100)
defuse/src/salts.rs (4)
  • update_current_salt (9-9)
  • invalidate_salts (14-14)
  • is_valid_salt (17-17)
  • current_salt (20-20)
core/src/nonce/salted.rs (3)
  • current (144-146)
  • new (105-113)
  • new (163-165)
core/src/nonce/versioned.rs (2)
core/src/nonce/salted.rs (5)
  • size_of (27-27)
  • from (179-183)
  • random (26-34)
  • new (105-113)
  • new (163-165)
core/src/nonce/expirable.rs (1)
  • new (25-27)
defuse/src/contract/accounts/account/nonces.rs (2)
bitmap/src/lib.rs (1)
  • cleanup_by_prefix (45-47)
core/src/nonce/mod.rs (1)
  • cleanup_by_prefix (51-53)
defuse/src/contract/accounts/mod.rs (9)
defuse/src/accounts.rs (1)
  • cleanup_nonces_by_prefix (35-35)
tests/src/tests/defuse/accounts/mod.rs (3)
  • cleanup_nonces_by_prefix (26-29)
  • cleanup_nonces_by_prefix (93-118)
  • cleanup_nonces_by_prefix (204-209)
core/src/nonce/salted.rs (1)
  • from (179-183)
core/src/nonce/versioned.rs (2)
  • from (28-30)
  • from (36-44)
core/src/engine/state/cached.rs (2)
  • cleanup_nonce_by_prefix (182-196)
  • cleanup_nonce_by_prefix (422-424)
core/src/engine/state/deltas.rs (1)
  • cleanup_nonce_by_prefix (126-132)
core/src/engine/state/mod.rs (1)
  • cleanup_nonce_by_prefix (65-69)
defuse/src/contract/accounts/account/mod.rs (1)
  • cleanup_nonce_by_prefix (156-158)
defuse/src/contract/intents/state.rs (1)
  • cleanup_nonce_by_prefix (132-145)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: Security Audit - deny
  • GitHub Check: Build
  • GitHub Check: Shared / Static Analysis (intents-poa-factory, contract, near, poa-factory, 1, ., 1.18, false, --filter-pa...
  • GitHub Check: Shared / Static Analysis (intents-poa-token, contract, near, poa-token, 1, ., 1.18, false, --filter-paths ...
  • GitHub Check: Shared / Static Analysis (intents-contract, contract, near, defuse, 1, ., 1.18, false, --filter-paths "nod...
  • GitHub Check: Shared security analysis / Static Analysis (intents-poa-factory, contract, near, poa-factory, 1, ., 1.18, false, --filter-pa...
  • GitHub Check: Shared security analysis / Static Analysis (intents-poa-token, contract, near, poa-token, 1, ., 1.18, false, --filter-paths ...
  • GitHub Check: Shared security analysis / Static Analysis (intents-contract, contract, near, defuse, 1, ., 1.18, false, --filter-paths "nod...
  • GitHub Check: Security Audit - report
🔇 Additional comments (14)
bitmap/src/lib.rs (1)

45-47: LGTM—internal API usage is consistent. Ran rg -n "clear_by_prefix": no results; all callers use cleanup_by_prefix. As this is a public API change, ensure any external/downstream crates are updated to call the renamed method.

defuse/src/contract/salts.rs (2)

14-27: Clarify that update_current_salt does not invalidate the previous salt.

The method calls set_new(false) and emits an event with an empty invalidated set, meaning the previous salt remains valid. This is correct behavior, but the method name alone doesn't convey this nuance.

Consider adding a doc comment to make the behavior explicit:

/// Rotates to a new current salt without invalidating the previous one.
/// 
/// Returns the newly generated salt.
#[access_control_any(roles(Role::DAO, Role::SaltManager))]
#[payable]
fn update_current_salt(&mut self) -> Salt {

Note: A previous reviewer suggested adding an invalidate_current: bool parameter to allow optional invalidation. If this flexibility is needed in the future, consider that enhancement.


40-44: LGTM: Event emission correctly reflects the operation.

The SaltRotationEvent with current and invalidated fields accurately represents the state change from invalidate_salts. This addresses the concern from a previous review about unclear event semantics.

defuse/src/contract/accounts/mod.rs (1)

72-73: Nonce prefix extraction is correct. The pattern [prefix @ .., _] yields a 31-byte slice, matching NoncePrefix = U248, and cleanup_nonce_by_prefix operates on the expected prefix length.

defuse/src/contract/accounts/account/nonces.rs (1)

72-74: LGTM!

The method signature change is consistent with the broader prefix-based cleanup refactor, and the implementation correctly delegates to the underlying nonces cleanup method.

core/src/nonce/versioned.rs (1)

26-30: Verify unreachable assumptions hold.

The unreachable!() assumes that deserializing any 32-byte Nonce as a VersionedNonce never fails. This is safe because:

  • Legacy path: any 32 bytes not starting with the magic prefix deserialize as Legacy(Nonce)
  • Versioned path: bytes starting with the magic prefix deserialize as the versioned format

However, ensure that deserialization errors from malformed versioned nonces are properly handled elsewhere in the codebase, as this conversion would panic on such input.

core/src/engine/state/mod.rs (2)

45-46: LGTM!

The addition of is_valid_salt to the StateView trait is consistent with the salted nonce implementation and provides the necessary validation interface.


65-69: Engine-level commit_nonce defers cleanup to the contract layer
Cleanup is invoked in defuse/src/contract/accounts/mod.rs:73 immediately after commit_nonce, so expired nonces are purged per transaction.

Likely an incorrect or invalid review comment.

defuse/src/contract/state/mod.rs (1)

39-39: Salt prefix nesting is consistent. Both v0 migration and the new state use .nest(Prefix::Salts) to initialize SaltRegistry, so no changes are required.

core/src/engine/state/deltas.rs (2)

100-103: LGTM!

The delegation to the underlying state is correct for salt validation in the wrapper pattern.


126-131: LGTM!

The refactored single-prefix cleanup approach aligns with the PR's nonce versioning objectives. The delegation is straightforward and correct.

core/src/engine/state/cached.rs (3)

123-125: LGTM!

Correct delegation through the view layer for salt validation.


182-193: LGTM!

The refactored cleanup method correctly delegates to the account level, and the use of to_owned() on line 190 is idiomatic for borrowed types.


422-423: LGTM!

The method signature now uses NoncePrefix for explicit typing, addressing previous review feedback. The delegation to the nonces collection is correct.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (5)
core/src/nonce/salted.rs (4)

29-42: Compile fix: qualify size_of.

size_of isn’t in scope. Use mem::size_of::().

Apply:

-        const SIZE: usize = size_of::<Salt>();
+        const SIZE: usize = mem::size_of::<Salt>();

216-222: Tests: set testing env before SaltRegistry::new to prevent panic.

Seed the VM before constructing SaltRegistry.

-    fn contains_salt_test(random_bytes: Vec<u8>) {
-        let random_salt: Salt = Unstructured::new(&random_bytes).arbitrary().unwrap();
-        let salts = SaltRegistry::new(random_bytes);
+    fn contains_salt_test(random_bytes: Vec<u8>, mut rng: impl Rng) {
+        let random_salt: Salt = Unstructured::new(&random_bytes).arbitrary().unwrap();
+        let _ = set_random_seed(&mut rng);
+        let salts = SaltRegistry::new(random_bytes);

239-261: Tests: seed VM before constructing registry (reset_salt_test).

Same panic risk; move seeding first.

-    fn reset_salt_test(random_bytes: Vec<u8>, mut rng: impl Rng) {
-        let mut salts = SaltRegistry::new(random_bytes);
+    fn reset_salt_test(random_bytes: Vec<u8>, mut rng: impl Rng) {
+        let _ = set_random_seed(&mut rng);
+        let mut salts = SaltRegistry::new(random_bytes);

225-237: Tests: seed VM before constructing registry.

Avoid early env access panics by moving set_random_seed earlier.

-    fn update_current_salt_test(random_bytes: Vec<u8>, mut rng: impl Rng) {
-        let mut salts = SaltRegistry::new(random_bytes);
-
-        let current = set_random_seed(&mut rng);
+    fn update_current_salt_test(random_bytes: Vec<u8>, mut rng: impl Rng) {
+        let current = set_random_seed(&mut rng);
+        let mut salts = SaltRegistry::new(random_bytes);
defuse/src/contract/accounts/mod.rs (1)

55-55: Enforce a 1 yocto guard on cleanup_nonces.

This public, state-changing method must be marked #[payable] and begin with assert_one_yocto() to prevent accidental cross-contract calls or malicious invocations without attached deposits.

Apply this diff:

+    #[payable]
     fn cleanup_nonces(&mut self, nonces: Vec<(AccountId, Vec<AsBase64<Nonce>>)>) {
+        assert_one_yocto();
         for (account_id, nonces) in nonces {
🧹 Nitpick comments (3)
core/src/nonce/salted.rs (1)

32-39: Salt predictability and collision bounds; add entropy/counter.

Derivation uses only block seed + 1-byte attempt; predictable within a block and capped to 256 candidates. Consider mixing a persisted rotation counter and/or timestamp/caller into the hash to improve uniqueness and unpredictability, e.g., sha256(seed || counter || attempt) with counter stored in state.

If desired, I can draft an update using a u64 counter persisted alongside current/previous salts.

defuse/src/contract/salts.rs (1)

32-49: Graceful invalidation with event emission is good; minor note on unwrap_or_panic.

Filtering by is_ok() is fine. Consider propagating SaltGenerationFailed instead of unwrap_or_panic in update_current_salt for resilience, if you expect extreme rotations.

defuse/src/contract/accounts/mod.rs (1)

58-65: Simplify pattern matching for legacy nonces.

The false assignment for legacy nonces followed by an if !cleanable { continue; } check is verbose. Applying the early-continue pattern directly in the match arm improves clarity.

Based on learnings from past reviews, apply this diff:

                 let cleanable = match VersionedNonce::from(nonce) {
                     VersionedNonce::V1(SaltedNonce {
                         salt,
                         nonce: ExpirableNonce { deadline, .. },
-                    }) => deadline.has_expired() || !self.is_valid_salt(salt),
-                    // NOTE: legacy nonces can't be cleared before a complete prohibition on its usage
-                    VersionedNonce::Legacy(_) => false,
+                    }) => deadline.has_expired() || !self.is_valid_salt(salt),
+                    VersionedNonce::Legacy(_) => continue,
                 };
 
-                if !cleanable {
-                    continue;
-                }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 28d9174 and 494abdf.

📒 Files selected for processing (8)
  • CHANGELOG.md (1 hunks)
  • core/src/error.rs (2 hunks)
  • core/src/nonce/salted.rs (1 hunks)
  • defuse/src/accounts.rs (1 hunks)
  • defuse/src/contract/accounts/mod.rs (3 hunks)
  • defuse/src/contract/salts.rs (1 hunks)
  • tests/src/tests/defuse/accounts/mod.rs (4 hunks)
  • tests/src/tests/defuse/accounts/nonces.rs (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • CHANGELOG.md
🧰 Additional context used
🧬 Code graph analysis (6)
tests/src/tests/defuse/accounts/mod.rs (2)
defuse/src/accounts.rs (1)
  • cleanup_nonces (35-35)
defuse/src/contract/accounts/mod.rs (1)
  • cleanup_nonces (55-76)
defuse/src/contract/salts.rs (4)
defuse/src/salts.rs (4)
  • update_current_salt (9-9)
  • invalidate_salts (14-14)
  • is_valid_salt (17-17)
  • current_salt (20-20)
core/src/nonce/salted.rs (3)
  • current (158-160)
  • new (113-121)
  • new (177-179)
defuse/src/contract/state/mod.rs (1)
  • new (27-41)
defuse/src/contract/intents/state.rs (1)
  • is_valid_salt (94-96)
tests/src/tests/defuse/accounts/nonces.rs (4)
test-utils/src/random.rs (3)
  • random (82-84)
  • random_bytes (143-151)
  • rng (138-140)
defuse/src/contract/salts.rs (1)
  • current_salt (58-60)
tests/src/tests/defuse/state/extensions/salt.rs (3)
  • current_salt (20-20)
  • current_salt (63-68)
  • current_salt (98-100)
defuse/src/salts.rs (1)
  • current_salt (20-20)
defuse/src/contract/accounts/mod.rs (7)
tests/src/tests/defuse/accounts/mod.rs (3)
  • cleanup_nonces (26-26)
  • cleanup_nonces (90-112)
  • cleanup_nonces (198-200)
defuse/src/accounts.rs (1)
  • cleanup_nonces (35-35)
core/src/engine/state/mod.rs (1)
  • cleanup_nonce_by_prefix (65-69)
core/src/engine/state/cached.rs (2)
  • cleanup_nonce_by_prefix (182-196)
  • cleanup_nonce_by_prefix (422-424)
defuse/src/contract/accounts/account/mod.rs (1)
  • cleanup_nonce_by_prefix (156-158)
defuse/src/contract/intents/state.rs (1)
  • cleanup_nonce_by_prefix (132-145)
core/src/engine/state/deltas.rs (1)
  • cleanup_nonce_by_prefix (126-132)
core/src/nonce/salted.rs (2)
defuse/src/contract/state/mod.rs (1)
  • new (27-41)
defuse/src/contract/mod.rs (1)
  • new (99-108)
defuse/src/accounts.rs (2)
tests/src/tests/defuse/accounts/mod.rs (3)
  • cleanup_nonces (26-26)
  • cleanup_nonces (90-112)
  • cleanup_nonces (198-200)
defuse/src/contract/accounts/mod.rs (1)
  • cleanup_nonces (55-76)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: Build
  • GitHub Check: Shared security analysis / Static Analysis (intents-poa-factory, contract, near, poa-factory, 1, ., 1.18, false, --filter-pa...
  • GitHub Check: Shared security analysis / Static Analysis (intents-poa-token, contract, near, poa-token, 1, ., 1.18, false, --filter-paths ...
  • GitHub Check: Shared security analysis / Static Analysis (intents-contract, contract, near, defuse, 1, ., 1.18, false, --filter-paths "nod...
  • GitHub Check: Shared / Static Analysis (intents-contract, contract, near, defuse, 1, ., 1.18, false, --filter-paths "nod...
  • GitHub Check: Shared / Static Analysis (intents-poa-token, contract, near, poa-token, 1, ., 1.18, false, --filter-paths ...
  • GitHub Check: Shared / Static Analysis (intents-poa-factory, contract, near, poa-factory, 1, ., 1.18, false, --filter-pa...
  • GitHub Check: Security Audit - report
🔇 Additional comments (10)
core/src/nonce/salted.rs (1)

101-121: Construction reads env; document/guard in tests.

SaltRegistry::new calls Salt::derive(0) → env::random_seed_array(); requires testing_env in unit tests. Ensure tests set the VM context before constructing.

defuse/src/contract/salts.rs (1)

15-28: Rotation flow looks correct.

assert_one_yocto + role guard + set_new + event emission + return current are consistent.

tests/src/tests/defuse/accounts/mod.rs (3)

26-26: Trait rename aligns with contract changes.


90-112: Call target updated to cleanup_nonces; looks good.


198-200: Forwarder method rename consistent.

core/src/error.rs (1)

58-60: Verify exhaustive matches and serialization compatibility

Update every downstream match on DefuseError to include the new variants (InvalidNonce, InvalidSalt, SaltGenerationFailed), and confirm external/ABI consumers tolerate these additions.

defuse/src/accounts.rs (1)

32-35: API rename complete; no stale callers found.

tests/src/tests/defuse/accounts/nonces.rs (3)

29-38: LGTM! Well-designed test helper.

The create_random_salted_nonce helper cleanly encapsulates V1 nonce creation with salt, deadline, and random bytes. This promotes DRY principles and makes test intent clear.


42-203: LGTM! Comprehensive nonce lifecycle coverage.

The test scenarios thoroughly validate:

  • Legacy nonce backward compatibility (lines 50-71)
  • Invalid salt rejection (lines 73-89)
  • Deadline ordering constraints (lines 92-106)
  • Expiration enforcement (lines 108-123)
  • Successful commit flow (lines 126-147)
  • Salt rotation with old salt acceptance (lines 149-180)
  • Invalidated salt rejection (lines 182-203)

This provides excellent coverage of the salted nonce state machine.


208-334: LGTM! Thorough cleanup edge case validation.

The test effectively verifies cleanup behavior for:

  • Expired nonce removal (lines 268-280)
  • Legacy nonce preservation (line 289)
  • Non-expired nonce preservation (line 290)
  • Unknown user handling (line 291)
  • Invalid salt cleanup (lines 312-333)

This ensures cleanup_nonces behaves correctly across boundary conditions.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
defuse/src/contract/accounts/mod.rs (2)

55-55: Add 1 yocto deposit guard to prevent accidental calls.

This public state-changing method must be marked #[payable] and call assert_one_yocto() at the start to prevent accidental cross-contract invocations.

Apply this diff:

+    #[payable]
     fn cleanup_nonces(&mut self, nonces: Vec<(AccountId, Vec<AsBase64<Nonce>>)>) {
+        assert_one_yocto();
         for (account_id, nonces) in nonces {

Based on past review comments, ensure all test files (e.g., tests/src/tests/defuse/accounts/…) are updated to attach 1 yocto when calling cleanup_nonces.


72-73: Guard against empty nonce panic.

Line 72 uses let [prefix @ .., _] = nonce; which will panic if nonce is a zero-length array. Add a defensive check before prefix extraction.

Apply this diff:

                 // NOTE: all errors are omitted
+                if nonce.is_empty() {
+                    continue;
+                }
                 let [prefix @ .., _] = nonce;
                 let _ = State::cleanup_nonce_by_prefix(self, &account_id, prefix);

This prevents runtime panics from malformed or empty nonces.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2961a7e and bb28ed3.

📒 Files selected for processing (6)
  • core/src/engine/mod.rs (3 hunks)
  • core/src/nonce/versioned.rs (1 hunks)
  • defuse/src/contract/accounts/mod.rs (3 hunks)
  • defuse/src/contract/mod.rs (5 hunks)
  • defuse/src/contract/tokens/mod.rs (5 hunks)
  • defuse/src/contract/versioned/v0.rs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • defuse/src/contract/tokens/mod.rs
🧰 Additional context used
🧬 Code graph analysis (5)
defuse/src/contract/accounts/mod.rs (7)
defuse/src/accounts.rs (1)
  • cleanup_nonces (35-35)
tests/src/tests/defuse/accounts/mod.rs (3)
  • cleanup_nonces (26-26)
  • cleanup_nonces (90-112)
  • cleanup_nonces (198-200)
serde-utils/src/base64.rs (1)
  • into_inner (77-79)
core/src/nonce/versioned.rs (1)
  • maybe_from (25-28)
defuse/src/contract/intents/state.rs (1)
  • cleanup_nonce_by_prefix (132-144)
core/src/engine/state/mod.rs (1)
  • cleanup_nonce_by_prefix (65-69)
defuse/src/contract/accounts/account/mod.rs (1)
  • cleanup_nonce_by_prefix (156-158)
defuse/src/contract/mod.rs (4)
core/src/nonce/salted.rs (3)
  • derive (29-42)
  • new (113-121)
  • new (179-181)
defuse/src/contract/accounts/mod.rs (1)
  • new (109-119)
defuse/src/contract/state/mod.rs (1)
  • new (27-41)
defuse/src/contract/state/v0.rs (2)
  • new (50-62)
  • migrate (21-38)
defuse/src/contract/versioned/v0.rs (3)
defuse/src/contract/versioned/mod.rs (3)
  • from (32-39)
  • from (44-47)
  • from (52-54)
defuse/src/contract/mod.rs (1)
  • migrate (146-148)
defuse/src/contract/state/v0.rs (1)
  • migrate (21-38)
core/src/engine/mod.rs (2)
core/src/payload/mod.rs (1)
  • nonce (68-70)
core/src/nonce/versioned.rs (1)
  • maybe_from (25-28)
core/src/nonce/versioned.rs (2)
core/src/nonce/salted.rs (4)
  • derive (29-42)
  • new (113-121)
  • new (179-181)
  • from (195-199)
core/src/nonce/expirable.rs (1)
  • new (25-27)
🪛 GitHub Check: Check
core/src/nonce/versioned.rs

[failure] 66-66:
std::convert::Fromnonce::versioned::VersionedNonce` is not satisfied


[failure] 62-62:
mismatched types


[failure] 61-61:
std::convert::Fromnonce::versioned::VersionedNonce` is not satisfied


[failure] 49-49:
std::convert::Fromnonce::versioned::VersionedNonce` is not satisfied


[failure] 47-47:
no variant or associated item named Legacy found for enum nonce::versioned::VersionedNonce in the current scope


[failure] 46-46:
mismatched types

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (7)
  • GitHub Check: Shared security analysis / Static Analysis (intents-poa-token, contract, near, poa-token, 1, ., 1.18, false, --filter-paths ...
  • GitHub Check: Shared security analysis / Static Analysis (intents-contract, contract, near, defuse, 1, ., 1.18, false, --filter-paths "nod...
  • GitHub Check: Shared security analysis / Static Analysis (intents-poa-factory, contract, near, poa-factory, 1, ., 1.18, false, --filter-pa...
  • GitHub Check: Shared / Static Analysis (intents-poa-factory, contract, near, poa-factory, 1, ., 1.18, false, --filter-pa...
  • GitHub Check: Shared / Static Analysis (intents-poa-token, contract, near, poa-token, 1, ., 1.18, false, --filter-paths ...
  • GitHub Check: Shared / Static Analysis (intents-contract, contract, near, defuse, 1, ., 1.18, false, --filter-paths "nod...
  • GitHub Check: Security Audit - report
🔇 Additional comments (8)
defuse/src/contract/versioned/v0.rs (2)

11-24: LGTM!

The V0 storage structure is correctly set up for migration. The _postponed_burns field is appropriately marked with #[borsh(skip)] to indicate it's runtime-only and won't be persisted, which aligns with the architectural decision to keep it unversioned.


26-41: LGTM!

The migration implementation correctly transforms V0 storage to the current version. The state migration via ContractState::migrate(state, Prefix::State) will add the new salts field (as shown in the relevant code snippet from defuse/src/contract/state/v0.rs), while preserving accounts and relayer_keys. Dropping _postponed_burns is appropriate since it's runtime-only.

defuse/src/contract/accounts/mod.rs (1)

57-69: LGTM!

The versioned nonce handling logic is correct:

  • V1 salted nonces are cleanable if expired or their salt is invalid.
  • Legacy nonces (non-versioned) are preserved until their usage is completely prohibited, as documented in the inline comment.
defuse/src/contract/mod.rs (5)

9-13: LGTM!

The new salts and versioned modules are correctly declared to support the salt management and storage versioning features introduced in this PR.


69-80: LGTM!

The Contract structure refactoring correctly enables versioned storage:

  • Custom (de)serialization via MaybeVersionedContractStorage handles version detection and automatic migration from V0 to the latest version.
  • postponed_burns is correctly marked as runtime-only with #[borsh(skip)], consistent with the design in ContractStorageV0.
  • Deref/DerefMut implementations provide ergonomic access to the inner storage and state.

82-92: LGTM!

The ContractStorage structure is correctly defined as the core storage primitive, containing the essential state components (accounts, state, relayer_keys). The Deref/DerefMut implementations provide convenient access to the nested state.


145-149: LGTM!

The MigrateStorageWithPrefix trait provides a clean, prefix-aware migration API. This pattern is correctly applied in ContractState::migrate (as shown in relevant code snippets) to add the new salts field while preserving existing state during V0→current migration.


100-110: Verify ContractState::new salt registry initialization
Implementation of ContractState::new wasn’t located in defuse/src/contract/state; ensure the salts field is initialized with SaltRegistry::new(prefix.nest(Prefix::Salts)).

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
defuse/src/contract/runtime.rs (2)

5-8: Clean encapsulation.

The struct design is sound with proper encapsulation (private field, public API). The Default derive aligns well with the removal of manual constructors mentioned in the AI summary.

Consider adding documentation to describe the Runtime's purpose and its role in managing postponed burn events:

+/// Manages runtime state for the contract, including postponed multi-token burn events.
 #[derive(Debug, Default)]
 pub struct Runtime {
     postponed_burns: PostponedMtBurnEvents,
 }

10-14: Straightforward delegation.

The mt_burn method correctly delegates to the internal postponed_burns field. The 'static lifetime constraint on MtBurnEvent is appropriate for events that need to be stored beyond the current call stack.

Consider adding documentation to clarify the method's purpose:

 impl Runtime {
+    /// Records a multi-token burn event to be processed later.
     pub fn mt_burn(&mut self, event: MtBurnEvent<'static>) {
         self.postponed_burns.mt_burn(event);
     }
 }
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 731224d and e550ff1.

📒 Files selected for processing (1)
  • defuse/src/contract/runtime.rs (1 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: Security Audit - deny
  • GitHub Check: Build
  • GitHub Check: Shared / Static Analysis (intents-poa-token, contract, near, poa-token, 1, ., 1.18, false, --filter-paths ...
  • GitHub Check: Shared / Static Analysis (intents-contract, contract, near, defuse, 1, ., 1.18, false, --filter-paths "nod...
  • GitHub Check: Shared / Static Analysis (intents-poa-factory, contract, near, poa-factory, 1, ., 1.18, false, --filter-pa...
  • GitHub Check: Shared security analysis / Static Analysis (intents-contract, contract, near, defuse, 1, ., 1.18, false, --filter-paths "nod...
  • GitHub Check: Shared security analysis / Static Analysis (intents-poa-token, contract, near, poa-token, 1, ., 1.18, false, --filter-paths ...
  • GitHub Check: Shared security analysis / Static Analysis (intents-poa-factory, contract, near, poa-factory, 1, ., 1.18, false, --filter-pa...
  • GitHub Check: Security Audit - report
🔇 Additional comments (1)
defuse/src/contract/runtime.rs (1)

1-3: LGTM!

The imports are correctly structured and necessary for the Runtime implementation.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (4)
core/src/nonce/salted.rs (4)

30-30: Compilation error: qualify size_of.

size_of is not in scope. The import at line 1 only brings in the mem module, not the size_of function directly.

Apply this diff:

-        const SIZE: usize = size_of::<Salt>();
+        const SIZE: usize = mem::size_of::<Salt>();

218-224: Tests will panic without VM context setup.

SaltRegistry::new() calls Salt::derive(0) which uses env::random_seed_array(), requiring a VM testing context. Line 220 constructs the registry before any context is set up, which will cause a panic.

Apply this diff:

     #[rstest]
     fn contains_salt_test(random_bytes: Vec<u8>) {
+        let mut rng = rng();
+        set_random_seed(&mut rng);
         let random_salt: Salt = Unstructured::new(&random_bytes).arbitrary().unwrap();
         let salts = SaltRegistry::new(random_bytes);

Based on learnings from past reviews.


227-239: Initialize VM context before constructing SaltRegistry.

Similar to contains_salt_test, line 228 constructs the registry before setting up the VM context. The set_random_seed call on line 230 comes too late.

Apply this diff:

     #[rstest]
     fn update_current_salt_test(random_bytes: Vec<u8>, mut rng: impl Rng) {
+        set_random_seed(&mut rng);
         let mut salts = SaltRegistry::new(random_bytes);
 
-        let current = set_random_seed(&mut rng);
+        let current = rng.random();
+        let context = VMContextBuilder::new().random_seed(current).build();
+        testing_env!(context);
         let previous = salts.set_new().expect("should set new salt");

Based on learnings from past reviews.


242-263: Initialize VM context before constructing SaltRegistry.

Line 243 constructs the registry before any VM context is set up. Move the set_random_seed call before SaltRegistry::new().

Apply this diff:

     #[rstest]
     fn reset_salt_test(random_bytes: Vec<u8>, mut rng: impl Rng) {
+        set_random_seed(&mut rng);
         let mut salts = SaltRegistry::new(random_bytes);
         let random_salt = rng.random::<[u8; 4]>().as_slice().into();
 
-        let seed = set_random_seed(&mut rng);
+        let seed = rng.random();
+        let context = VMContextBuilder::new().random_seed(seed).build();
+        testing_env!(context);
         let current = seed_to_salt(&seed, 0);

Based on learnings from past reviews.

🧹 Nitpick comments (1)
defuse/src/contract/mod.rs (1)

149-153: Unify migration trait usage across versions

Consider implementing MigrateStorageWithPrefix<T> for each versioned state (e.g., v0→current) and using it inside the versioned deserializer. Improves cohesion and discoverability.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e550ff1 and 3acfda8.

📒 Files selected for processing (3)
  • core/src/nonce/salted.rs (1 hunks)
  • defuse/src/contract/mod.rs (5 hunks)
  • defuse/src/contract/tokens/mod.rs (6 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
core/src/nonce/salted.rs (3)
defuse/src/contract/mod.rs (1)
  • new (103-114)
defuse/src/contract/state/mod.rs (1)
  • new (27-41)
defuse/src/contract/state/v0.rs (1)
  • new (50-62)
defuse/src/contract/mod.rs (4)
core/src/nonce/salted.rs (3)
  • derive (29-42)
  • new (113-121)
  • new (179-181)
defuse/src/contract/accounts/mod.rs (1)
  • new (108-118)
defuse/src/contract/state/mod.rs (1)
  • new (27-41)
defuse/src/contract/state/v0.rs (2)
  • new (50-62)
  • migrate (21-38)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (9)
  • GitHub Check: Security Audit - deny
  • GitHub Check: Build
  • GitHub Check: Shared security analysis / Static Analysis (intents-poa-token, contract, near, poa-token, 1, ., 1.18, false, --filter-paths ...
  • GitHub Check: Shared security analysis / Static Analysis (intents-contract, contract, near, defuse, 1, ., 1.18, false, --filter-paths "nod...
  • GitHub Check: Shared security analysis / Static Analysis (intents-poa-factory, contract, near, poa-factory, 1, ., 1.18, false, --filter-pa...
  • GitHub Check: Shared / Static Analysis (intents-poa-factory, contract, near, poa-factory, 1, ., 1.18, false, --filter-pa...
  • GitHub Check: Shared / Static Analysis (intents-contract, contract, near, defuse, 1, ., 1.18, false, --filter-paths "nod...
  • GitHub Check: Shared / Static Analysis (intents-poa-token, contract, near, poa-token, 1, ., 1.18, false, --filter-paths ...
  • GitHub Check: Security Audit - report
🔇 Additional comments (10)
core/src/nonce/salted.rs (5)

29-42: Verify salt predictability is acceptable for your threat model.

Salt::derive uses env::random_seed_array() which is constant within a block and publicly derivable from the block hash. While the num parameter prevents identical salts within the same rotation sequence, an observer who knows the block hash and current valid salts can predict the next salt. Consider whether this predictability is acceptable for your use case, or if additional entropy sources (e.g., a persisted counter, timestamp, or account ID) should be mixed in.

Based on learnings from past reviews.


45-62: LGTM!

The Display/Debug implementations correctly use hex encoding, and the FromStr implementation properly delegates to FromHex::from_hex, handling errors appropriately.


65-99: LGTM!

The JsonSchema implementation correctly represents Salt as a hex-encoded string with appropriate schema metadata, and is properly feature-gated.


123-163: LGTM!

The salt rotation logic is sound:

  • derive_next_salt correctly uses the inclusive range 0..=u8::MAX to find an unused salt
  • set_new properly rotates the salt and maintains the previous salt as valid
  • invalidate handles the edge case of invalidating the current salt by rotating first
  • is_valid correctly checks both current and previous salts

165-183: LGTM!

The SaltedNonce structure is clean and straightforward, with appropriate Borsh serialization support and a simple constructor.

defuse/src/contract/mod.rs (4)

93-96: Runtime separation for postponed burns looks good

Keeping postponed_burns in Runtime with #[borsh(skip)] avoids persisting transient data. LGTM.


105-111: Initialization aligns with new storage layout

Accounts/State/RelayerKeys prefixes are correct; runtime defaults are clear. LGTM.


68-79: Contract state layout change: ensure backward-compatible deserialization fully consumes old bytes

You collapsed top-level fields into storage and moved postponed_burns to runtime (skipped). This changes the Borsh layout of the contract state. The custom deserialize_with = As::<MaybeVersionedContractStorage>::deserialize must:

  • Correctly read previous versions where accounts, state, relayer_keys, and possibly postponed_burns were serialized at the top level, and
  • Fully consume any trailing bytes (e.g., old postponed_burns) so try_from_slice doesn’t fail on leftover data.

If MaybeVersionedContractStorage::deserialize only reads the new storage triple and leaves old bytes unread, on-chain upgrade will break.

Action:

  • Confirm that the deserializer accounts for the old top-level layout and drains obsolete fields.
  • Add/ensure an integration test that loads a v0 snapshot (with postponed_burns) and successfully deserializes into the new Contract.

52-53: Add SaltManager role usage checks

SaltManager added. Verify that salt management APIs are gated with this role and that RolesConfig/tests include it.

defuse/src/contract/tokens/mod.rs (1)

21-25: Access path refactor to storage/runtime is correct

  • Accounts/state routed via self.storage.*.
  • Burn scheduling via self.runtime.postponed_burns.

No behavioral change detected. LGTM.

Also applies to: 42-47, 78-83, 106-108, 118-119

Copy link
Collaborator

@fusede fusede left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very well done! This is a final batch of comments, most of them are just nitpicking, so it's up to you. But please do add Role::GarbageCollector part for sure

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (3)
core/src/nonce/salted.rs (1)

212-219: Critical: Test will panic without VM context.

SaltRegistry::new(random_bytes) calls Salt::derive(0) which uses env::random_seed_array() and will panic unless a testing context is set up first. Call set_random_seed(&mut rng()) before line 215.

Apply this diff:

 #[rstest]
 fn contains_salt_test(random_bytes: Vec<u8>) {
     let random_salt: Salt = Unstructured::new(&random_bytes).arbitrary().unwrap();
+    let mut entropy = rng();
+    set_random_seed(&mut entropy);
     let salts = SaltRegistry::new(random_bytes);

Based on past review comments that flagged this exact issue.

core/src/nonce/versioned.rs (1)

31-42: Critical: Unqualified size_of will not compile.

Line 33 uses size_of::<Nonce>() without qualification or import, which will cause a compilation error.

Apply this diff:

 impl From<VersionedNonce> for Nonce {
     fn from(value: VersionedNonce) -> Self {
-        const SIZE: usize = size_of::<Nonce>();
+        const SIZE: usize = core::mem::size_of::<Nonce>();
         let mut result = [0u8; SIZE];

Based on past review comments that flagged this issue.

defuse/src/contract/accounts/mod.rs (1)

56-57: Critical: Add #[payable] guard to state-changing method.

While access control restricts who can call this method, it should still follow the contract's pattern of requiring a 1 yocto deposit for all state-changing operations (see lines 38-40, 45-47, 76-77 for examples).

Apply this diff:

     #[access_control_any(roles(Role::DAO, Role::GarbageCollector))]
+    #[payable]
     fn cleanup_nonces(&mut self, nonces: Vec<(AccountId, Vec<AsBase64<Nonce>>)>) {
+        assert_one_yocto();

Based on past review comments and consistency with other state-changing methods in this file.

🧹 Nitpick comments (9)
core/src/nonce/versioned.rs (1)

44-73: Consider making test more robust against prefix collision.

The test is functionally correct, but could be more explicit by ensuring the random legacy_nonce doesn't accidentally start with VERSIONED_MAGIC_PREFIX. While the probability is 1/2^32 and deserialization would likely fail anyway, forcing the prefix to differ makes the test more deterministic.

Optional refinement:

 #[rstest]
 fn maybe_from_test(random_bytes: Vec<u8>) {
     let mut u = Unstructured::new(&random_bytes);
-    let legacy_nonce: Nonce = u.arbitrary().unwrap();
+    let mut legacy_nonce: Nonce = u.arbitrary().unwrap();
+    // Ensure it's not accidentally treated as versioned
+    if legacy_nonce[..4] == VersionedNonce::VERSIONED_MAGIC_PREFIX {
+        legacy_nonce[0] ^= 0x01;
+    }

Based on past review suggestions about handling rare prefix collisions.

defuse/src/contract/accounts/mod.rs (2)

58-69: Per-nonce prefix cleanup is correct.

The implementation iterates through nonces, validates cleanability, and performs prefix-based cleanup. The pattern let [prefix @ .., _] = nonce; is safe for fixed-size Nonce arrays (always 32 bytes) and extracts all bytes except the last.

Optional: Consider logging ignored errors for observability:

                 // NOTE: all errors are omitted
                 let [prefix @ .., _] = nonce;
-                let _ = State::cleanup_nonce_by_prefix(self, &account_id, prefix);
+                if let Err(e) = State::cleanup_nonce_by_prefix(self, &account_id, prefix) {
+                    near_sdk::log!("Cleanup failed for account {}: {:?}", account_id, e);
+                }

84-102: Nonce cleanability logic is sound.

The method correctly identifies nonces that should be cleaned up:

  • Legacy nonces are preserved (not cleanable)
  • V1 nonces are cleanable if expired OR have invalid salt

Optional: Simplify the double-negative for readability:

             VersionedNonce::V1(SaltedNonce {
                 salt,
                 nonce: ExpirableNonce { deadline, .. },
             }) => {
-                if !deadline.has_expired() && self.is_valid_salt(salt) {
-                    return false;
-                }
+                deadline.has_expired() || !self.is_valid_salt(salt)
             }
         }
-
-        true
tests/src/tests/defuse/accounts/nonces.rs (5)

73-90: Avoid rare flake: ensure “invalid salt” is not equal to current/previous

Arbitrary salt could (extremely rarely) match the current (or previous) salt. Guard to ensure it’s truly invalid.

-        let random_salt = Salt::arbitrary(u).unwrap();
+        // Ensure we pick a salt that is not the current one (and unlikely to be previous)
+        let random_salt = loop {
+            let s = Salt::arbitrary(u).unwrap();
+            if s != current_salt {
+                break s;
+            }
+        };

91-106: Naming nit: this nonce isn’t “expired”

The variable expired_nonce is misleading here (it’s only inconsistent vs provided deadline). Consider a clearer name.

-        let expired_nonce = create_random_salted_nonce(current_salt, deadline, &mut rng);
+        let nonce_with_smaller_deadline =
+            create_random_salted_nonce(current_salt, deadline, &mut rng);
...
-                expired_nonce,
+                nonce_with_smaller_deadline,

235-260: Reduce Arbitrary() decode risk: feed more than 1 byte

Using only 1 byte for Unstructured can cause Arbitrary to fail for some types. Feed a small random slice to reduce flakiness.

-                env.user1.sign_defuse_message(
-                    SigningStandard::arbitrary(&mut Unstructured::new(&rng.random::<[u8; 1]>()))
-                        .unwrap(),
+                env.user1.sign_defuse_message(
+                    SigningStandard::arbitrary(
+                        &mut Unstructured::new(&random_bytes(8..64, &mut rng)),
+                    )
+                    .unwrap(),
                 ...
-                env.user1.sign_defuse_message(
-                    SigningStandard::arbitrary(&mut Unstructured::new(&rng.random::<[u8; 1]>()))
-                        .unwrap(),
+                env.user1.sign_defuse_message(
+                    SigningStandard::arbitrary(
+                        &mut Unstructured::new(&random_bytes(8..64, &mut rng)),
+                    )
+                    .unwrap(),
                 ...
-                env.user1.sign_defuse_message(
-                    SigningStandard::arbitrary(&mut Unstructured::new(&rng.random::<[u8; 1]>()))
-                        .unwrap(),
+                env.user1.sign_defuse_message(
+                    SigningStandard::arbitrary(
+                        &mut Unstructured::new(&random_bytes(8..64, &mut rng)),
+                    )
+                    .unwrap(),

Based on learnings


338-342: Fix log message: invalidation vs rotation

The expect message says “rotate” but the action is invalidate_salts. Adjust to avoid confusion.

-            .expect("unable to rotate salt");
+            .expect("unable to invalidate salt");

265-265: Add expiry margin to avoid timing flakiness

Sleeping exactly WAITING_TIME can be borderline for the last committed nonce(s). Add a small margin.

-    sleep(Duration::from_secs_f64(WAITING_TIME.as_seconds_f64())).await;
+    sleep(Duration::from_secs_f64(WAITING_TIME.as_seconds_f64() + 0.5)).await;

Also applies to: 402-402

defuse/src/contract/mod.rs (1)

151-155: Document migration trait intent and usage

Consider a brief doc comment on MigrateStorageWithPrefix to clarify it’s for moving legacy on-chain collections under a new prefix during deserialization/upgrades. Helps future maintainers align implementations across versioned modules.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3acfda8 and 5385e75.

📒 Files selected for processing (10)
  • CHANGELOG.md (1 hunks)
  • core/src/engine/mod.rs (3 hunks)
  • core/src/nonce/salted.rs (1 hunks)
  • core/src/nonce/versioned.rs (1 hunks)
  • defuse/src/contract/accounts/mod.rs (4 hunks)
  • defuse/src/contract/intents/mod.rs (1 hunks)
  • defuse/src/contract/mod.rs (5 hunks)
  • defuse/src/intents.rs (3 hunks)
  • tests/src/tests/defuse/accounts/mod.rs (4 hunks)
  • tests/src/tests/defuse/accounts/nonces.rs (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (7)
core/src/nonce/salted.rs (3)
defuse/src/contract/accounts/mod.rs (1)
  • new (122-132)
defuse/src/contract/state/mod.rs (1)
  • new (27-41)
defuse/src/contract/state/v0.rs (1)
  • new (50-62)
tests/src/tests/defuse/accounts/nonces.rs (6)
test-utils/src/random.rs (3)
  • random (82-84)
  • random_bytes (143-151)
  • rng (138-140)
core/src/nonce/salted.rs (3)
  • rng (239-239)
  • new (108-116)
  • new (174-176)
defuse/src/contract/mod.rs (1)
  • new (105-116)
defuse/src/contract/salts.rs (1)
  • current_salt (58-60)
tests/src/tests/defuse/state/extensions/salt.rs (3)
  • current_salt (20-20)
  • current_salt (63-68)
  • current_salt (98-100)
tests/src/tests/defuse/accounts/mod.rs (3)
  • is_nonce_used (45-45)
  • is_nonce_used (147-156)
  • is_nonce_used (237-239)
core/src/engine/mod.rs (2)
core/src/payload/mod.rs (1)
  • nonce (68-70)
core/src/nonce/versioned.rs (1)
  • maybe_from (25-28)
core/src/nonce/versioned.rs (3)
core/src/payload/mod.rs (1)
  • nonce (68-70)
core/src/nonce/salted.rs (4)
  • derive (24-37)
  • from (190-194)
  • new (108-116)
  • new (174-176)
core/src/nonce/expirable.rs (1)
  • new (25-27)
defuse/src/contract/mod.rs (4)
core/src/nonce/salted.rs (3)
  • derive (24-37)
  • new (108-116)
  • new (174-176)
defuse/src/contract/accounts/mod.rs (1)
  • new (122-132)
defuse/src/contract/state/mod.rs (1)
  • new (27-41)
defuse/src/contract/state/v0.rs (2)
  • new (50-62)
  • migrate (21-38)
tests/src/tests/defuse/accounts/mod.rs (2)
defuse/src/contract/accounts/mod.rs (1)
  • cleanup_nonces (57-69)
defuse/src/accounts.rs (1)
  • cleanup_nonces (35-35)
defuse/src/contract/accounts/mod.rs (8)
tests/src/tests/defuse/accounts/mod.rs (3)
  • cleanup_nonces (26-30)
  • cleanup_nonces (94-120)
  • cleanup_nonces (206-214)
defuse/src/accounts.rs (1)
  • cleanup_nonces (35-35)
core/src/engine/state/mod.rs (1)
  • cleanup_nonce_by_prefix (65-69)
core/src/engine/state/cached.rs (2)
  • cleanup_nonce_by_prefix (182-194)
  • cleanup_nonce_by_prefix (420-422)
defuse/src/contract/intents/state.rs (1)
  • cleanup_nonce_by_prefix (132-144)
core/src/engine/state/deltas.rs (1)
  • cleanup_nonce_by_prefix (126-132)
defuse/src/contract/accounts/account/mod.rs (1)
  • cleanup_nonce_by_prefix (156-158)
core/src/nonce/versioned.rs (1)
  • maybe_from (25-28)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: Build
  • GitHub Check: Shared security analysis / Static Analysis (intents-contract, contract, near, defuse, 1, ., 1.18, false, --filter-paths "nod...
  • GitHub Check: Shared security analysis / Static Analysis (intents-poa-token, contract, near, poa-token, 1, ., 1.18, false, --filter-paths ...
  • GitHub Check: Shared security analysis / Static Analysis (intents-poa-factory, contract, near, poa-factory, 1, ., 1.18, false, --filter-pa...
  • GitHub Check: Shared / Static Analysis (intents-poa-factory, contract, near, poa-factory, 1, ., 1.18, false, --filter-pa...
  • GitHub Check: Shared / Static Analysis (intents-poa-token, contract, near, poa-token, 1, ., 1.18, false, --filter-paths ...
  • GitHub Check: Shared / Static Analysis (intents-contract, contract, near, defuse, 1, ., 1.18, false, --filter-paths "nod...
  • GitHub Check: Security Audit - report
🔇 Additional comments (31)
CHANGELOG.md (1)

1-22: LGTM! Clear and comprehensive changelog.

The changelog follows standard conventions and clearly documents all major features and changes in version 0.3.0, including salt management, nonce versioning, and cleanup functionality.

core/src/nonce/salted.rs (14)

1-21: LGTM! Clean imports and type definition.

The Salt struct is well-defined with appropriate serialization support and optional fuzzing integration via the arbitrary feature.


23-38: Salt derivation strategy is sound.

The implementation combines the block seed with an attempt number (num), which provides sufficient entropy when called from derive_next_salt (lines 118-123) that iterates through 0..=255. This addresses the block-collision concern raised in past reviews.


40-50: LGTM! Hex encoding for display.

The Debug and Display implementations correctly encode the 4-byte salt as hex for human-readable output.


52-58: LGTM! Safe hex parsing.

The FromHex::from_hex implementation validates the decoded length and returns an error if the input doesn't decode to exactly 4 bytes, preventing the panic risk mentioned in past reviews.


60-94: LGTM! Proper JSON schema for ABI.

The JsonSchema implementation correctly defines Salt as a hex-encoded string with appropriate content encoding metadata.


96-116: LGTM! Well-structured salt registry.

The SaltRegistry maintains current and previous salts with validity tracking. Initializing with Salt::derive(0) provides a deterministic starting point.


118-123: LGTM! Exhaustive salt generation.

The method correctly tries all 256 possible salt values and returns the first unused one. The error case handles the (unlikely) scenario where all salts are already registered.


125-134: LGTM! Clean salt rotation logic.

The rotation correctly moves the current salt to previous (keeping it valid), generates a new current salt, and returns the rotated salt for event emission.


136-147: LGTM! Safe invalidation with automatic rotation.

The method prevents directly invalidating the current salt by rotating first, which ensures there's always a valid salt available for new operations.


149-158: LGTM! Clean accessor methods.

The validation and accessor methods are straightforward and correct.


160-177: LGTM! Simple and effective wrapper.

The SaltedNonce struct cleanly wraps any nonce type with a salt, maintaining generic flexibility.


179-210: LGTM! Comprehensive test utilities.

The test helpers properly set up VM context (addressing past review concerns) and provide useful utilities for deterministic salt generation in tests.


221-234: LGTM! Proper test setup and comprehensive rotation verification.

The test correctly initializes the VM context before creating the registry and thoroughly validates the salt rotation mechanism.


236-258: LGTM! Thorough invalidation testing.

The test covers both invalidation scenarios (previous and current salts) and properly sets up VM context throughout.

defuse/src/contract/intents/mod.rs (1)

53-56: LGTM! Current salt now exposed in simulation output.

The addition of current_salt to StateOutput aligns with the SaltManager trait integration and provides necessary salt information to clients.

defuse/src/intents.rs (3)

1-14: LGTM! Clean salt-related imports.

The imports correctly bring in Salt type and SaltManager trait needed for the trait bound and struct field changes.


17-17: LGTM! Trait bound reflects salt management dependency.

Adding SaltManager as a supertrait ensures implementers provide salt management capabilities required by the intents system.


58-64: LGTM! StateOutput extended with salt information.

The current_salt field provides clients with the necessary information to construct valid salted nonces, completing the salt management API surface.

core/src/nonce/versioned.rs (2)

1-19: LGTM! Well-structured versioned nonce enum.

The V1 variant correctly composes SaltedNonce and ExpirableNonce with 15 bytes for the inner nonce, which totals to 32 bytes when serialized with the magic prefix (4) + version (1) + salt (4) + deadline (8) + nonce (15).


21-29: LGTM! Safe versioned nonce detection.

The maybe_from method correctly handles both versioned and legacy nonces by checking for the magic prefix, ensuring backward compatibility.

core/src/engine/mod.rs (3)

9-9: LGTM! Complete imports for nonce verification.

All necessary types are imported to support the new versioned nonce validation logic.


77-79: LGTM! Pre-commit validation prevents invalid nonces.

Calling verify_intent_nonce before commit_nonce ensures that only valid, unexpired nonces with correct salts are stored in state.


87-113: LGTM! Comprehensive nonce validation logic.

The verification method correctly handles:

  • Legacy nonce compatibility (early return)
  • Salt validity checking
  • Deadline ordering (intent ≤ nonce deadline)
  • Expiration validation

The defensive checks prevent multiple attack vectors including replay attacks with rotated salts and deadline manipulation.

defuse/src/contract/accounts/mod.rs (2)

9-10: LGTM! Complete imports for access control and nonce validation.

The imports properly support both the access control decorator and the versioned nonce handling in the cleanup logic.

Also applies to: 17-17, 25-25


122-131: LGTM! Minor formatting improvement.

The added blank line improves code readability without any functional changes.

tests/src/tests/defuse/accounts/mod.rs (3)

26-30: LGTM! Test helper signature matches contract API.

The trait method correctly reflects the renamed cleanup_nonces API with the required defuse_contract_id parameter.


94-120: LGTM! Test implementation correctly calls renamed method.

The implementation properly updates the contract call from cleanup_expired_nonces to cleanup_nonces, maintaining all other test helper functionality.


206-214: LGTM! Contract implementation correctly delegates to Account.

The forwarding implementation maintains the clean delegation pattern for the renamed method.

tests/src/tests/defuse/accounts/nonces.rs (1)

29-38: Helper looks good; confirm inner nonce length expectation

create_random_salted_nonce is clear and uses host RNG (good). Please confirm that [u8; 15] matches the expected inner nonce size across validation paths (e.g., is_nonce_used, signature domain). If a fixed size is required (e.g., 16 or 32), align here to avoid accidental acceptance/rejection edge cases.

defuse/src/contract/mod.rs (1)

70-81: Storage versioning wiring looks correct

Using As:: for both serialize_with and deserialize_with is the right pattern; runtime is borsh-skipped, avoiding on-chain format changes. No issues spotted.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
defuse/src/contract/accounts/mod.rs (1)

61-72: Guard against empty nonce panic in prefix extraction.

Line 68's destructuring let [prefix @ .., _] = nonce; will panic if the nonce array is empty. While Nonce may be non-empty by construction, defensive validation prevents runtime panics from malformed input or future type changes.

Apply this diff to add a length check:

             for nonce in nonces.into_iter().map(AsBase64::into_inner) {
                 if !self.is_nonce_cleanable(nonce) {
                     continue;
                 }
 
                 // NOTE: all errors are omitted
+                if nonce.is_empty() {
+                    continue;
+                }
                 let [prefix @ .., _] = nonce;
                 let _ = State::cleanup_nonce_by_prefix(self, &account_id, prefix);
             }
🧹 Nitpick comments (1)
defuse/src/contract/accounts/mod.rs (1)

87-105: Consider unifying nonce validation logic.

This logic partially duplicates nonce verification in Engine::verify_intent_nonce(). Consider extracting shared validation into a StateViewExt trait method as suggested in previous reviews to improve maintainability.

Based on learnings from past review comments.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5385e75 and 6087055.

📒 Files selected for processing (1)
  • defuse/src/contract/accounts/mod.rs (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
defuse/src/contract/accounts/mod.rs (7)
tests/src/tests/defuse/accounts/mod.rs (3)
  • cleanup_nonces (26-30)
  • cleanup_nonces (94-120)
  • cleanup_nonces (206-214)
defuse/src/accounts.rs (1)
  • cleanup_nonces (35-35)
core/src/engine/state/mod.rs (1)
  • cleanup_nonce_by_prefix (65-69)
core/src/engine/state/cached.rs (2)
  • cleanup_nonce_by_prefix (182-194)
  • cleanup_nonce_by_prefix (420-422)
defuse/src/contract/intents/state.rs (1)
  • cleanup_nonce_by_prefix (132-144)
defuse/src/contract/accounts/account/mod.rs (1)
  • cleanup_nonce_by_prefix (156-158)
core/src/nonce/versioned.rs (1)
  • maybe_from (25-28)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (8)
  • GitHub Check: Build
  • GitHub Check: Shared security analysis / Static Analysis (intents-poa-factory, contract, near, poa-factory, 1, ., 1.18, false, --filter-pa...
  • GitHub Check: Shared security analysis / Static Analysis (intents-poa-token, contract, near, poa-token, 1, ., 1.18, false, --filter-paths ...
  • GitHub Check: Shared security analysis / Static Analysis (intents-contract, contract, near, defuse, 1, ., 1.18, false, --filter-paths "nod...
  • GitHub Check: Shared / Static Analysis (intents-poa-token, contract, near, poa-token, 1, ., 1.18, false, --filter-paths ...
  • GitHub Check: Shared / Static Analysis (intents-contract, contract, near, defuse, 1, ., 1.18, false, --filter-paths "nod...
  • GitHub Check: Shared / Static Analysis (intents-poa-factory, contract, near, poa-factory, 1, ., 1.18, false, --filter-pa...
  • GitHub Check: Security Audit - report
🔇 Additional comments (1)
defuse/src/contract/accounts/mod.rs (1)

56-59: Ensure test calls include exactly 1 yocto deposit.

Verify that every cleanup_nonces test invocation attaches a 1 yocto deposit (e.g., via .deposit(1) or .deposit_yocto(1)); otherwise assert_one_yocto() will fail.

@hlgltvnnk hlgltvnnk merged commit ad29e01 into main Oct 9, 2025
15 checks passed
@hlgltvnnk hlgltvnnk deleted the feature/salted-nonces branch October 9, 2025 12:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants