Skip to content

Conversation

@pityjllk
Copy link
Collaborator

@pityjllk pityjllk commented Jan 16, 2026

Summary by CodeRabbit

  • New Features

    • Added sandbox fast-forward blocks for faster testing.
    • Validated refund-log size to prevent oversized logs; exposed TOTAL_LOG_LENGTH_LIMIT and REFUND_MEMO constants.
    • Extended multi-token receiver with new modes to exercise refunds and raw-byte returns.
  • Bug Fixes

    • Truncate large byte-array debug output to reduce log clutter.
  • Tests

    • Added extensive gas and refund-log tests for multi-token deposit/transfer scenarios.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Jan 16, 2026

📝 Walkthrough

Walkthrough

The PR adds refund-log length validation to NEP-245 event emission, introduces a raw mt_on_transfer API returning ExecutionFinalResult for receipt inspection, extends test stubs and extensive gas-resolution tests (including binary-search helpers and large-blob scenarios), updates sandbox APIs and fixture handling, and adds CI/test feature orchestration for long tests.

Changes

Cohort / File(s) Summary
Build & CI
sandbox/Cargo.toml, nep245/Cargo.toml, tests/Cargo.toml, Makefile.toml, .cargo/audit.toml, .github/workflows/ci.yml
Added libc and thiserror deps, dev-dep near-sdk (unit-testing), new long feature, run-tests-long task, cargo-audit suppress config, and CI step to run long tests / upgraded cargo-audit version.
Sandbox API & fixtures
sandbox/src/extensions/mt.rs, sandbox/src/lib.rs, sandbox/src/tx/wrappers.rs
Added mt_on_transfer_raw returning ExecutionFinalResult and refactored mt_on_transfer to use it; exposed extensions and tx builders; added fast_forward and shared singleton sandbox init; truncated long byte arrays in TestExecutionOutcome Debug.
NEP-245 events & validation
nep245/src/events.rs, defuse/src/contract/tokens/nep245/core.rs
Introduced ErrorRefundLogTooLong, TOTAL_LOG_LENGTH_LIMIT, EmitChecked trait and emit_with_refund_log_checked() usage; added refund-log size computation and enforced checked emission in two event sites.
near-utils constants & exports
near-utils/src/event.rs, near-utils/src/lib.rs
Added TOTAL_LOG_LENGTH_LIMIT (16384) and REFUND_MEMO ("refund") constants and re-exported them from the crate root.
Contracts: memo constant usage
defuse/src/contract/tokens/mod.rs, defuse/src/contract/tokens/nep141/withdraw.rs, defuse/src/contract/tokens/nep171/withdraw.rs, defuse/src/contract/tokens/nep245/resolver.rs, defuse/src/contract/tokens/nep245/withdraw.rs
Replaced hard-coded "refund" memo strings with centralized REFUND_MEMO constant across multiple refund-emission sites.
Test receiver stub
tests/contracts/multi-token-receiver-stub/src/lib.rs
Added a wasm-only stub_return_bytes returning byte blobs, ReturnValueExt trait for Promise wiring, and expanded MTReceiverMode with RefundAll, MaliciousRefund, and ReturnBytes variants; updated mt_on_transfer behaviors.
Tests: helpers & new gas tests
tests/src/tests/defuse/tokens/nep245/mod.rs, tests/src/tests/defuse/tokens/nep245/mt_deposit_resolve_gas.rs, tests/src/tests/defuse/tokens/nep245/mt_transfer_resolve_gas.rs
Added binary_search_max helper, new comprehensive mt_deposit_resolve_gas module (token-id generation modes, generators, validators, resolve-gas assertions), moved/refactored transfer tests, added calculate_log_sizes and a test ensuring transfers are rejected when refund log exceeds limit.

Sequence Diagram(s)

(omitted)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • mitinarseny
  • hlgltvnnk

Poem

🐇 A tiny hop through logs and code,
I chew the bytes and bear the load.
Refunds confined, events in line,
Raw calls revealed — the flow’s divine.
Tests stretch long; the sandbox sings in mode.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 inconclusive)
Check name Status Explanation Resolution
Title check ❓ Inconclusive The PR title 'Feat/mt resolve deposit tests' is vague and does not clearly communicate the main changes. While it mentions 'tests', the changeset includes significant infrastructure updates beyond just adding tests. Consider a more descriptive title that captures the main changes, such as 'Add MT deposit resolution gas tests and refund log validation infrastructure' or 'Implement refund log size limits for MT transfers and deposits'.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed Docstring coverage is 80.43% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

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: 1

🤖 Fix all issues with AI agents
In `@tests/src/tests/defuse/tokens/nep245/mod.rs`:
- Around line 17-28: The binary-search loop can underflow when mid == 0 and
test(mid) returns Err; replace the direct subtraction hi = mid - 1 with a
guarded update so that if mid == 0 you break the loop (no lower index exists)
otherwise set hi = mid - 1 (or hi = mid.saturating_sub(1) combined with an if
mid == 0 { break; } check). Update the match arm for Err(_) to use this guard
referencing lo, hi, mid, test and best so the loop cannot wrap to usize::MAX.
🧹 Nitpick comments (4)
tests/src/tests/defuse/tokens/nep245/mt_transfer_resolve_gas.rs (1)

278-291: Test coverage for binary_search_max is good but doesn't cover edge case.

The test validates all values from 0 to 99, which is comprehensive. However, note that this test only covers cases where at least test(0) succeeds (since limit ranges from 0 to 99 and test(x) succeeds when x <= limit).

Consider adding a test case where all values fail to ensure the underflow issue mentioned in mod.rs is covered:

✨ Additional test case
// Test case where all values fail
let test_all_fail = |_x| async { Err(anyhow::anyhow!("always fail")) };
assert_eq!(binary_search_max(0, max, test_all_fail).await, None);
sandbox/Cargo.toml (1)

21-21: Add libc to workspace dependencies for consistency.

The libc crate is used in the sandbox code (libc::atexit in sandbox/src/lib.rs) but is specified with a direct version instead of using the workspace = true pattern like all other dependencies. Declare it in the workspace [dependencies] section to maintain consistency across the codebase.

sandbox/src/lib.rs (1)

115-140: Potential cascading test failures on poisoned mutex.

If a test panics while the sandbox fixture is being created (before the lock is released), subsequent tests will fail with a panic on line 126's .unwrap() due to mutex poisoning. Consider recovering from a poisoned mutex or documenting this behavior.

That said, for test infrastructure where a panic during setup indicates a fundamental issue, this is often acceptable.

♻️ Optional: Handle poisoned mutex gracefully
     let (sandbox_arc, root_account) = mutex
         .lock()
-        .unwrap()
+        .unwrap_or_else(|poisoned| poisoned.into_inner())
         .as_ref()
         .map(|shared| (shared.sandbox.clone(), shared.root.clone()))
         .unwrap();
tests/src/tests/defuse/tokens/nep245/mt_deposit_resolve_gas.rs (1)

74-85: Potential underflow in account name length calculation.

If root_id_len >= 63, the subtraction TARGET_LEN - 1 - root_id_len will underflow (wrap around to a large value), causing the subaccount creation to likely fail or behave unexpectedly.

In practice, the sandbox root ID (test or similar) is short, so this won't trigger. However, adding a guard would make the code more robust.

♻️ Optional: Add a guard for name length calculation
         TokenIdGenerationMode::Medium => {
             // Use a 64-char named account: {name}.{root_id} = 64 chars total
             const TARGET_LEN: usize = 64;
             let root_id_len = env.root().id().as_str().len();
             // name_len + 1 (dot) + root_id_len = TARGET_LEN
+            assert!(
+                root_id_len < TARGET_LEN - 1,
+                "root_id too long for medium mode"
+            );
             let name_len = TARGET_LEN - 1 - root_id_len;
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d38a46a and 7e0b65d.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (8)
  • sandbox/Cargo.toml
  • sandbox/src/extensions/mt.rs
  • sandbox/src/lib.rs
  • sandbox/src/tx/wrappers.rs
  • tests/contracts/multi-token-receiver-stub/src/lib.rs
  • tests/src/tests/defuse/tokens/nep245/mod.rs
  • tests/src/tests/defuse/tokens/nep245/mt_deposit_resolve_gas.rs
  • tests/src/tests/defuse/tokens/nep245/mt_transfer_resolve_gas.rs
🧰 Additional context used
🧬 Code graph analysis (3)
tests/src/tests/defuse/tokens/nep245/mt_deposit_resolve_gas.rs (3)
tests/src/tests/defuse/tokens/nep245/mod.rs (4)
  • binary_search_max (8-31)
  • None (996-996)
  • None (1187-1187)
  • None (1558-1558)
test-utils/src/random.rs (2)
  • random (82-84)
  • gen_random_string (121-127)
sandbox/src/extensions/mt.rs (2)
  • tx (114-127)
  • tx (170-179)
tests/src/tests/defuse/tokens/nep245/mod.rs (2)
tests/src/tests/defuse/tokens/nep245/mt_deposit_resolve_gas.rs (2)
  • mt_deposit_resolve_gas (312-389)
  • None (336-336)
tests/src/tests/defuse/tokens/nep245/mt_transfer_resolve_gas.rs (1)
  • mt_transfer_resolve_gas (232-276)
tests/src/tests/defuse/tokens/nep245/mt_transfer_resolve_gas.rs (1)
tests/src/tests/defuse/tokens/nep245/mod.rs (1)
  • binary_search_max (8-31)
⏰ 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). (10)
  • GitHub Check: Build
  • GitHub Check: Build Reproducible
  • GitHub Check: Security Audit - deny
  • 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: 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 (16)
sandbox/src/tx/wrappers.rs (1)

49-58: LGTM!

The truncation logic for large byte payloads is well-implemented. The threshold of 32 bytes and showing first/last 16 bytes provides a good balance between readability and debugging utility.

sandbox/src/extensions/mt.rs (1)

184-206: LGTM with minor duplication note.

The implementation correctly mirrors mt_on_transfer while returning the raw ExecutionFinalResult. The code duplication between mt_on_transfer and mt_on_transfer_raw (lines 165-177 vs 191-203) is acceptable given the different return paths, though a shared helper could reduce this in the future if more *_raw variants are added.

tests/src/tests/defuse/tokens/nep245/mt_transfer_resolve_gas.rs (1)

1-1: LGTM!

The import correctly references the refactored binary_search_max from the parent module. This consolidation reduces code duplication across test files.

tests/contracts/multi-token-receiver-stub/src/lib.rs (4)

9-16: LGTM!

The raw extern function correctly bypasses SDK serialization to return arbitrary bytes for testing purposes. The conditional compilation to wasm32 is appropriate.


18-32: LGTM!

The extension trait provides a clean way to chain the raw return value call onto Promise objects. Using Gas::from_ggas(0) with GasWeight(1) correctly defers gas allocation to the runtime.


45-58: LGTM!

The new enum variants are well-documented and cover important edge cases for testing: legitimate refunds, malicious overflow attempts, and large return payloads.


94-96: LGTM!

The ReturnBytes handler correctly creates a raw byte response via the stub mechanism. The len.0 as usize cast is safe here since the test uses values well within u32 range (~4MB), and this runs exclusively on wasm32.

sandbox/src/lib.rs (3)

6-20: LGTM!

The expanded imports and public re-exports provide appropriate concurrency primitives and convenient access to the extension traits for test consumers.


92-95: LGTM!

The fast_forward method provides a useful wrapper for time-based testing scenarios. The unwrap() is acceptable for test infrastructure.


97-107: LGTM!

The OnceCell<Mutex<Option<Sandbox>>> pattern correctly enables async initialization while allowing synchronous cleanup via atexit. Using Option::take() ensures proper ownership transfer during cleanup, and silently handling a poisoned mutex during shutdown is pragmatic.

tests/src/tests/defuse/tokens/nep245/mt_deposit_resolve_gas.rs (6)

1-21: LGTM!

The imports and the TOTAL_LOG_LENGTH_LIMIT constant are appropriate for the test module's purpose.


23-51: LGTM!

The ReceiverTestMode enum provides a clean abstraction over MTReceiverMode with appropriate minimum gas values for each test scenario.


113-149: LGTM!

The make_token_ids function correctly generates token IDs of varying lengths. The string slicing is safe because both the index prefix and the random string consist of ASCII characters, ensuring single-byte UTF-8 encoding.


166-199: LGTM!

The log size validation is a valuable pre-check that ensures test parameters won't exceed blockchain event log limits before executing the expensive on-chain operations.


207-302: LGTM!

The test orchestration function is well-structured with clear separation between recoverable errors (for binary search) and critical failures (assertions for contract bugs). The comment on lines 271-273 clearly explains the rationale for the assert! vs bail distinction.


304-389: LGTM!

The parametrized test provides comprehensive coverage of token ID generation modes and receiver behaviors. The binary search approach efficiently finds the maximum token count, and the minimum threshold assertion (50 tokens) ensures the contract maintains acceptable capacity.

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines +17 to +28
while lo <= hi {
let mid = lo + (hi - lo) / 2;
match test(mid).await {
Ok(()) => {
best = Some(mid);
lo = mid + 1; // success -> try higher
}
Err(_) => {
hi = mid - 1; // failure -> try lower
}
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential underflow when mid == 0 and test fails.

If low is 0 and test(0) returns Err, line 25 computes hi = mid - 1 which causes unsigned integer underflow (0 - 1 wraps to usize::MAX). This would cause the loop to continue with invalid bounds.

In the current usage with min_token_count = 1, this isn't triggered, but the function could fail unexpectedly if called with low = 0.

🐛 Proposed fix to handle underflow
         match test(mid).await {
             Ok(()) => {
                 best = Some(mid);
                 lo = mid + 1; // success -> try higher
             }
             Err(_) => {
-                hi = mid - 1; // failure -> try lower
+                if mid == 0 {
+                    break;
+                }
+                hi = mid - 1; // failure -> try lower
             }
         }

Alternatively, use saturating_sub:

             Err(_) => {
-                hi = mid - 1; // failure -> try lower
+                hi = mid.saturating_sub(1); // failure -> try lower
+                if mid == 0 {
+                    break;
+                }
             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
while lo <= hi {
let mid = lo + (hi - lo) / 2;
match test(mid).await {
Ok(()) => {
best = Some(mid);
lo = mid + 1; // success -> try higher
}
Err(_) => {
hi = mid - 1; // failure -> try lower
}
}
}
while lo <= hi {
let mid = lo + (hi - lo) / 2;
match test(mid).await {
Ok(()) => {
best = Some(mid);
lo = mid + 1; // success -> try higher
}
Err(_) => {
if mid == 0 {
break;
}
hi = mid - 1; // failure -> try lower
}
}
}
🤖 Prompt for AI Agents
In `@tests/src/tests/defuse/tokens/nep245/mod.rs` around lines 17 - 28, The
binary-search loop can underflow when mid == 0 and test(mid) returns Err;
replace the direct subtraction hi = mid - 1 with a guarded update so that if mid
== 0 you break the loop (no lower index exists) otherwise set hi = mid - 1 (or
hi = mid.saturating_sub(1) combined with an if mid == 0 { break; } check).
Update the match arm for Err(_) to use this guard referencing lo, hi, mid, test
and best so the loop cannot wrap to usize::MAX.

Comment on lines +97 to +106
/// Shared sandbox instance for test fixtures.
/// Using `OnceCell<Mutex<Option<...>>>` allows async init and taking ownership in atexit.
static SHARED_SANDBOX: OnceCell<Mutex<Option<Sandbox>>> = OnceCell::const_new();

extern "C" fn cleanup_sandbox() {
if let Some(mutex) = SHARED_SANDBOX.get() {
if let Ok(mut guard) = mutex.lock() {
drop(guard.take());
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

Very cool! Though, I thought there is a second reason for it not to be killed: #[tokio::test] creates a separate tokio runtime for each test, while
tokio::process::Command::kill_on_drop() (added here) seems to work within a single tokio runtime instance, but I might be wrong here.

PS: wrapping in Option might be redundant due to existence of OnceCell::take()

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

tried, but

E0596: cannot borrow immutable static item `SHARED_SANDBOX` as mutable

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

tokio::process::Command::kill_on_drop()

could help but since its kept in static variable, the value is never dropped ...

Copy link
Collaborator

Choose a reason for hiding this comment

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

E0596: cannot borrow immutable static item `SHARED_SANDBOX` as mutable

Okay, then maybe get rid of OnceCell entirely? it seems to be redundant, since Mutex<Option<...>> should work, too.

As for kill_on_drop: since you're touching this part of code, can you also evaluate if tokio_shared_rt would help here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

oncecell guaranteeds that it will initialize sandbox only once, but yeah since we are only setting it bac to none after tests are finished it should be fine to live without it

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

fyi: confirmed that near-sandbox is killed in case of successful execution and on ctrl+c

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

actually oncecell is quite helpful here due to get_or_init functionatliy. otherwise you need to manually lock the mutex, then there is async call to create sandbox and you dont want to keep mutex locked between await points so the implementation becomes ugly and complicated IMO. so i decided to keep oncecell since its semantically comaptible with what we want => initialize sanbox only once per test run (binary call)

@pityjllk pityjllk force-pushed the feat/mt-resolve-deposit-tests branch from ec08d28 to 1f2f541 Compare January 20, 2026 06:03
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

🤖 Fix all issues with AI agents
In `@tests/contracts/multi-token-receiver-stub/src/lib.rs`:
- Around line 13-19: The stub_return_bytes function currently trusts the
incoming length and allocates based on it, risking huge allocations and silent
truncation; update stub_return_bytes to parse the length using a fallible
conversion (e.g., TryFrom/checked conversion instead of direct cast), enforce a
sensible upper bound (cap length to a constant MAX_RETURN_BYTES), and reject or
early-return (with a clear env::panic or no-op) when the parsed length is
invalid or exceeds the cap to avoid allocation/overflow; apply the same
defensive change to the similar code block referenced at lines 99-101 so both
places use try_from/checked conversion and the MAX_RETURN_BYTES cap before
allocating the Vec or calling env::value_return.

In `@tests/src/tests/defuse/tokens/nep245/mt_deposit_resolve_gas.rs`:
- Around line 203-223: The test currently only checks the resolve receipt when
defuse_outcomes.len() == 2, which can skip failures when there are more receipts
or fewer; change the condition to assert that defuse_outcomes.len() >= 2 and
always inspect the second receipt: obtain resolve_outcome =
defuse_outcomes[1].clone(), call resolve_result = resolve_outcome.into_result(),
and assert resolve_result.is_ok() with a clear error message including
resolve_result.err(); if the assertion for >= 2 fails, fail the test immediately
with a descriptive message about the missing resolve receipt (use symbols
defuse_outcomes, resolve_outcome, resolve_result, execution_result,
env.defuse.id() to locate the code).
♻️ Duplicate comments (1)
tests/src/tests/defuse/tokens/nep245/mod.rs (1)

17-28: Potential underflow when mid == 0 and test fails.

If lo is 0 and test(0) returns Err, line 25 computes hi = mid - 1 which causes unsigned integer underflow (0 - 1 wraps to usize::MAX). This would cause the loop to continue with invalid bounds.

While current usage with min_token_count = 1 avoids this, the function could fail unexpectedly if called with low = 0.

🐛 Proposed fix to handle underflow
         match test(mid).await {
             Ok(()) => {
                 best = Some(mid);
                 lo = mid + 1; // success -> try higher
             }
             Err(_) => {
+                if mid == 0 {
+                    break;
+                }
                 hi = mid - 1; // failure -> try lower
             }
         }
🧹 Nitpick comments (2)
tests/src/tests/defuse/tokens/nep245/mt_transfer_resolve_gas.rs (1)

395-424: Consider consolidating log size calculation helpers.

There's some duplication between calculate_log_sizes (lines 395-424) and validate_mt_batch_transfer_log_size (lines 83-106). Both construct similar MtTransferEvent structures for size calculation.

♻️ Suggested consolidation

You could refactor to have a single helper that returns both sizes:

-fn validate_mt_batch_transfer_log_size(
-    sender_id: &AccountId,
-    receiver_id: &AccountId,
-    token_ids: &[String],
-    amounts: &[u128],
-) -> anyhow::Result<usize> {
-    let mt_transfer_event = MtEvent::MtTransfer(Cow::Owned(vec![MtTransferEvent {
-        authorized_id: None,
-        old_owner_id: Cow::Borrowed(receiver_id),
-        new_owner_id: Cow::Borrowed(sender_id),
-        token_ids: Cow::Owned(token_ids.to_vec()),
-        amounts: Cow::Owned(amounts.iter().copied().map(U128).collect()),
-        memo: Some(Cow::Borrowed("refund")),
-    }]));
-
-    let longest_transfer_log = mt_transfer_event.to_nep297_event().to_event_log();
-
-    anyhow::ensure!(
-        longest_transfer_log.len() <= TOTAL_LOG_LENGTH_LIMIT,
-        "transfer log will exceed maximum log limit"
-    );
-
-    Ok(longest_transfer_log.len())
-}
+fn validate_mt_batch_transfer_log_size(
+    sender_id: &AccountId,
+    receiver_id: &AccountId,
+    token_ids: &[String],
+    amounts: &[u128],
+) -> anyhow::Result<usize> {
+    let (_, refund_size) = calculate_log_sizes(sender_id, receiver_id, token_ids, amounts);
+    anyhow::ensure!(
+        refund_size <= TOTAL_LOG_LENGTH_LIMIT,
+        "transfer log will exceed maximum log limit"
+    );
+    Ok(refund_size)
+}
sandbox/src/lib.rs (1)

92-94: Consider returning Result from fast_forward instead of panicking.

Since this is a public API, avoid unwrap() so callers can handle errors with context; good to lock this in now while the method is new.

♻️ Suggested signature change
-    pub async fn fast_forward(&self, blocks: u64) {
-        self.sandbox.fast_forward(blocks).await.unwrap();
-    }
+    pub async fn fast_forward(&self, blocks: u64) -> anyhow::Result<()> {
+        self.sandbox.fast_forward(blocks).await.map_err(Into::into)
+    }

@pityjllk pityjllk force-pushed the feat/mt-resolve-deposit-tests branch from 4cf4f66 to 89ea3f2 Compare January 21, 2026 10:54
Comment on lines 202 to 214
let transfer_event = MtTransferEvent {
authorized_id: None,
old_owner_id: sender_id.into(),
new_owner_id: Cow::Borrowed(receiver_id),
token_ids: token_ids.into(),
amounts: amounts.into(),
memo: memo.map(Into::into),
};
require!(
transfer_event.refund_log_size() <= TOTAL_LOG_LENGTH_LIMIT,
"too many tokens: refund log would exceed protocol limit"
);
MtEvent::MtTransfer([transfer_event].as_slice().into()).emit();
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe avoid spending gas on serializing twice?

let event_str = MtEvent::MtTransfer(/* ... */)
    .to_nep297_event()
    .to_event_log();
// check that refund event, if happens, would not exceed log limits
require!(event_str.len() <= TOTAL_LOG_LENGTH_LIMIT - r#","memo":"refund""#.len());
env::log_str(event_str);

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

you would have to substract current memo as well which makes it a bit ugly. also the overhead is 0.5TGas with event that is around TOTAL_LOG_LENGTH_LIMIT (with >100 tokens). If you think its too much i could implement something like size_hint that would do calculations like

fn size_hint(){
"\{".len()+ 
"memo".len() + self.memo.len() + ",\"\"".len() +
"receiver".len() + self.receiver.len() + ",\"\"".len() +
}

i considered it but concluded that 0.5T is ok

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

🤖 Fix all issues with AI agents
In @.github/workflows/ci.yml:
- Around line 165-166: The deny job currently installs cargo-audit without
version pinning causing drift vs the report job; update the cargo install
command in the security_audit_deny job to match the report job by pinning
cargo-audit to "^0.22" and keeping the --locked flag (i.e., replace the unpinned
install invocation with the same `cargo install cargo-audit --version "^0.22"
--locked` used in the report job).

In `@Makefile.toml`:
- Around line 211-213: The run-tests-long cargo task (tasks.run-tests-long)
currently passes --features=long to the entire workspace, which fails because
most crates don't define a long feature; update the task to either scope the
cargo invocation to the crates that actually support the long feature by adding
package selectors (use -p or --package with the crate names that define long) or
remove the workspace-wide --features=long and instead run cargo test with
--features=long per-package for those crates; modify the args array for
tasks.run-tests-long to use targeted package flags (e.g., -p/<crate>) or change
the command to iterate only over the crates that expose the long feature so the
test run no longer fails.

In `@near-utils/src/event.rs`:
- Around line 1-3: Update the doc comment for the constant
TOTAL_LOG_LENGTH_LIMIT to state it is the maximum total length of all log
entries in a single execution (in bytes) rather than a per-entry limit; locate
the pub const TOTAL_LOG_LENGTH_LIMIT: usize = 16384; declaration in event.rs and
replace the misleading comment with a line like "Maximum total length of all log
entries in a single execution (in bytes)." and keep the existing GitHub
reference link.

In `@tests/src/tests/defuse/tokens/nep245/mt_deposit_resolve_gas.rs`:
- Around line 373-374: The test function name has a typo: change the function
identifier
mt_desposit_resolve_can_handle_large_blob_value_returned_from_notification to
mt_deposit_resolve_can_handle_large_blob_value_returned_from_notification;
update the async test function declaration (#[tokio::test] async fn ...) so the
symbol matches everywhere it may be referenced (test runner discovery and any
calls), ensuring only the spelling of "desposit" → "deposit" is modified and
other code and attributes remain unchanged.
♻️ Duplicate comments (1)
tests/src/tests/defuse/tokens/nep245/mt_deposit_resolve_gas.rs (1)

239-248: Ensure resolve receipt is always validated.

The current check only validates when exactly 2 receipts exist. If there are more or fewer, the resolve callback validation is silently skipped. The second test at lines 423-427 uses the correct pattern with assert!(defuse_outcomes.len() >= 2, ...).

🔧 Proposed fix for consistent validation
-    if defuse_outcomes.len() == 2 {
-        let resolve_outcome = defuse_outcomes[1].clone();
-        let resolve_result = resolve_outcome.into_result();
-        assert!(
-            resolve_result.is_ok(),
-            "CRITICAL: mt_resolve_deposit callback failed for token_count={token_count}! \
-            This indicates insufficient gas allocation in the contract. Error: {:?}",
-            resolve_result.err()
-        );
-    }
+    anyhow::ensure!(
+        defuse_outcomes.len() >= 2,
+        "Expected at least 2 defuse receipts, got {}",
+        defuse_outcomes.len()
+    );
+    let resolve_outcome = defuse_outcomes[1].clone();
+    let resolve_result = resolve_outcome.into_result();
+    assert!(
+        resolve_result.is_ok(),
+        "CRITICAL: mt_resolve_deposit callback failed for token_count={token_count}! \
+        This indicates insufficient gas allocation in the contract. Error: {:?}",
+        resolve_result.err()
+    );
🧹 Nitpick comments (1)
tests/src/tests/defuse/tokens/nep245/mt_deposit_resolve_gas.rs (1)

336-336: Use expect() with a descriptive message.

If binary_search_max returns None (meaning even min_token_count=1 failed), the unwrap() will panic without context, making debugging harder.

♻️ Proposed fix
-    let max_deposited_count = max_deposited_count.unwrap();
+    let max_deposited_count = max_deposited_count
+        .expect("Failed to find any successful token count - even min_token_count=1 failed");

Comment on lines +165 to +166
# Require 0.22+ for CVSS 4.0 support (advisory-db now contains CVSS 4.0 entries)
run: cargo install cargo-audit --version "^0.22" --locked
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Locate all cargo-audit installs in the CI file
rg -n "cargo install cargo-audit" .github/workflows/ci.yml

Repository: near/intents

Length of output: 168


🏁 Script executed:

#!/bin/bash
# Get context around cargo-audit installations and identify which job each belongs to
rg -B 5 "cargo install cargo-audit" .github/workflows/ci.yml

Repository: near/intents

Length of output: 589


Align cargo-audit version across audit jobs.

The deny job installs cargo-audit without version pinning while the report job pins it to ^0.22 for CVSS 4.0 support. This creates version drift between the two jobs. Update the deny job to match:

♻️ Suggested update for security_audit_deny
-      - name: Install Cargo Plugins
-        run: cargo install cargo-audit --locked
+      - name: Install Cargo Plugins
+        run: cargo install cargo-audit --version "^0.22" --locked
🤖 Prompt for AI Agents
In @.github/workflows/ci.yml around lines 165 - 166, The deny job currently
installs cargo-audit without version pinning causing drift vs the report job;
update the cargo install command in the security_audit_deny job to match the
report job by pinning cargo-audit to "^0.22" and keeping the --locked flag
(i.e., replace the unpinned install invocation with the same `cargo install
cargo-audit --version "^0.22" --locked` used in the report job).

Comment on lines +211 to +213
[tasks.run-tests-long]
command = "cargo"
args = ["test", "--workspace", "--all-targets", "--features=long", "${@}"]
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

python - <<'PY'
import glob, pathlib, tomllib

root = pathlib.Path("Cargo.toml")
if not root.exists():
    raise SystemExit("Root Cargo.toml not found")

data = tomllib.loads(root.read_text())
members = data.get("workspace", {}).get("members", [])
if not members:
    print("No workspace.members found in Cargo.toml")
    raise SystemExit(0)

def expand(member):
    if any(ch in member for ch in "*?[]"):
        return [pathlib.Path(p) for p in glob.glob(member)]
    return [pathlib.Path(member)]

missing = []
for m in members:
    for path in expand(m):
        cargo = path / "Cargo.toml"
        if not cargo.exists():
            print(f"Missing {cargo}")
            continue
        pkg = tomllib.loads(cargo.read_text())
        features = pkg.get("features", {})
        if "long" not in features:
            missing.append(str(cargo))

if missing:
    print("Packages missing feature 'long':")
    print("\n".join(sorted(set(missing))))
else:
    print("All workspace packages define feature 'long'")
PY

Repository: near/intents

Length of output: 726


Fix run-tests-long task — most workspace members lack the long feature.

The command will fail when executed because 32 of 33 workspace members don't define the long feature (missing from admin-utils, auth-call, bitmap, borsh-utils, controller, core, crypto, deadline, decimal, defuse, erc191, escrow-swap, fees, io-utils, map-utils, near-utils, nep245, nep413, nep461, num-utils, poa-factory, poa-token, randomness, sandbox, sep53, serde-utils, test-utils, tip191, token-id, ton-connect, webauthn, wnear). Either add long feature definitions to all workspace crates, or scope the command to specific crates using -p <crate> or --package <crate>.

🤖 Prompt for AI Agents
In `@Makefile.toml` around lines 211 - 213, The run-tests-long cargo task
(tasks.run-tests-long) currently passes --features=long to the entire workspace,
which fails because most crates don't define a long feature; update the task to
either scope the cargo invocation to the crates that actually support the long
feature by adding package selectors (use -p or --package with the crate names
that define long) or remove the workspace-wide --features=long and instead run
cargo test with --features=long per-package for those crates; modify the args
array for tasks.run-tests-long to use targeted package flags (e.g., -p/<crate>)
or change the command to iterate only over the crates that expose the long
feature so the test run no longer fails.

Comment on lines +1 to +3
/// Maximum length of a single log entry in NEAR runtime.
/// See: <https://github.com/near/nearcore/blob/v2.5.0/runtime/near-vm-runner/src/logic/logic.rs#L42>
pub const TOTAL_LOG_LENGTH_LIMIT: usize = 16384;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

NEAR runtime TOTAL_LOG_LENGTH_LIMIT 16384

💡 Result:

"TOTAL_LOG_LENGTH_LIMIT = 16384" is the protocol/runtime limit on the total bytes of emitted logs for a single execution (a contract call/receipt). If a contract's logs exceed this (16,384 bytes) the runtime will fail the call with TotalLogLengthExceeded. This limit appears in the protocol/limit_config as max_total_log_length = 16384. [1][2]

Sources:

  • NEAR protocol config / RPC example showing max_total_log_length = 16384. [1]
  • Runtime error list including TotalLogLengthExceeded (the error raised when total log length exceeds the limit). [2]

🌐 Web query:

NEAR nearcore v2.5.0 logic.rs log length limit

💡 Result:

I couldn't find a specific reference to a "logic.rs" log-length limit in nearcore v2.5.0 from public docs or the nearcore repo with the search terms given. Could you paste the logic.rs snippet or point me to the file/line (GitHub URL or crate path) you mean? With that I can locate the exact limit and explain it.


🌐 Web query:

nearcore v2.5.0 runtime near-vm-runner logic.rs file structure

💡 Result:

Summary outline of logic.rs (runtime/near-vm-runner/src/logic/logic.rs) in nearcore v2.5.0 — high-level structure:

  • Top-level imports and feature flags. [1]
  • Type/enum definitions used by the runner (e.g., VMOutcome, PromiseResult, VMLogicError variants). [1]
  • Traits & key public items re-exported by the crate (CompiledContract, VM, PreparedContract). [1]
  • VMLogic implementation — the core struct that:
    • stores VMContext and External handle,
    • implements host-function handlers that expose near-vm-logic APIs to WASM (storage read/write, logs, promises, input/output, math/hash helpers, context getters, etc.). [2][3]
  • Helpers for memory/register management and conversion between WASM memory and runtime types (read/write registers, serialize/deserialize). [1][3]
  • Error types and conversion helpers (mapping VM/wasm traps to VMLogic errors). [1]
  • Compilation/preparation glue points referenced by the runner (prepare/compile/cache interactions). [1]
  • Unit tests for host functions and VM interactions (bottom of file or in nearby test modules). [1]

Sources / references:

  • near-vm-runner crate docs (module overview and exported logic module). [1]
  • Nomicon / Runtime spec describing near-vm-runner and VMLogic role. [2]
  • Implementation reference pointer to logic.rs in the nearcore repo. [3]

🌐 Web query:

github.com/near/nearcore blob v2.5.0 near-vm-runner

💡 Result:

The near-vm-runner crate for nearcore v2.5.0 is at runtime/near-vm-runner in the near/nearcore repo — e.g. GitHub blob path: https://github.com/near/nearcore/tree/v2.5.0/runtime/near-vm-runner. [1][2]

Sources:

  • near/nearcore repository. [1]
  • near-vm-runner crate docs (near-vm-runner). [2]

🌐 Web query:

site:github.com/near/nearcore/blob/v2.5.0 runtime/near-vm-runner src/logic logic.rs line 42

💡 Result:

I tried to fetch that file/line from GitHub but ran into an error accessing the repository file. Do you want me to try again (or paste the file contents here)?


🌐 Web query:

NEAR protocol specification max_total_log_length specification

💡 Result:

The NEAR protocol specification sets max_total_log_length = 16384 (bytes). [1][2]

Sources:
[1] NEAR RPC protocol config (limit_config -> max_total_log_length).
[2] NEAR Protocol / Nomicon parameter listing.


🌐 Web query:

NEAR runtime "single log entry" vs "total log length" difference

💡 Result:

Do you mean NEAR Protocol's runtime (smart contract) log limits/fields — e.g., RPC responses that show "single_log_entry" vs "total_log_length" — or something else (explorer, node logs, or an SDK/runtime API)? If you confirm which NEAR component and where you saw those terms (link or JSON snippet), I’ll look up the exact difference and cite sources.


🌐 Web query:

nearcore log length limit single entry total execution

💡 Result:

Short answer: nearcore enforces a maximum of 100 log entries per execution and a maximum total log bytes of 16,384 (i.e. ~16 KiB) across that execution — there is no separate hard limit for a single log entry beyond those totals, so a single log can be as large as the total log budget (subject to UTF‑8 and other limits). [1][2]

Sources:

  • NEAR protocol/nearcore limit_config showing max_number_logs = 100 and max_total_log_length = 16384. [1]
  • NEAR forum discussion noting the 100 logs-per-execution limit. [2]

🏁 Script executed:

cat -n near-utils/src/event.rs | head -10

Repository: near/intents

Length of output: 380


Fix documentation: 16384 is the total log length limit per execution, not per single log entry.

The constant value (16384 bytes) is correct per NEAR protocol spec, but the documentation comment is misleading. NEAR enforces a maximum of 100 log entries per execution with a combined total of 16,384 bytes — there is no separate hard limit on individual log entry size. Update the comment to clarify this is the total log budget for an entire execution:

/// Maximum total length of all log entries in a single execution (in bytes).
/// See: <https://github.com/near/nearcore/blob/v2.5.0/runtime/near-vm-runner/src/logic/logic.rs#L42>
🤖 Prompt for AI Agents
In `@near-utils/src/event.rs` around lines 1 - 3, Update the doc comment for the
constant TOTAL_LOG_LENGTH_LIMIT to state it is the maximum total length of all
log entries in a single execution (in bytes) rather than a per-entry limit;
locate the pub const TOTAL_LOG_LENGTH_LIMIT: usize = 16384; declaration in
event.rs and replace the misleading comment with a line like "Maximum total
length of all log entries in a single execution (in bytes)." and keep the
existing GitHub reference link.

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

🤖 Fix all issues with AI agents
In `@nep245/src/events.rs`:
- Around line 25-26: Remove the duplicate TOTAL_LOG_LENGTH_LIMIT constant
definition from nep245/src/events.rs and instead add an import: use
defuse_near_utils::TOTAL_LOG_LENGTH_LIMIT; so the module uses the centralized
constant; ensure any references to TOTAL_LOG_LENGTH_LIMIT in this file remain
unchanged and compile after removing the local pub const.
🧹 Nitpick comments (2)
nep245/src/events.rs (2)

44-49: Conservative overhead estimation may over-reject valid events.

The function returns max(REFUND_EXTRA_BYTES, memo_length), but when a memo already exists, the actual overhead is the difference between refund memo size and existing memo size, not the full REFUND_EXTRA_BYTES.

For example, if memo = Some("refu") (4 chars):

  • Original includes ,"memo":"refu" (~13 bytes)
  • Refund includes ,"memo":"refund" (~16 bytes)
  • Actual overhead: ~3 bytes, not 16

This conservative approach is safe (won't cause runtime failures) but may reject some valid events unnecessarily. If precision matters, consider computing the actual delta.


136-157: Consider using this accurate method in validate_with_refund_overhead for transfers.

This method computes the exact refund log size, while validate_with_refund_overhead uses the heuristic compute_refund_overhead. For MtTransfer events, using this accurate method would avoid over-rejecting valid events.

The current approach is simpler and applies uniformly to all event types, so this is just a consideration if precision becomes important.

@pityjllk pityjllk force-pushed the feat/mt-resolve-deposit-tests branch 2 times, most recently from eaa27f3 to 0e68e57 Compare January 23, 2026 15:07
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

🤖 Fix all issues with AI agents
In `@nep245/src/events.rs`:
- Around line 44-83: The refund overhead calculation currently returns the memo
length instead of the extra bytes needed, causing overestimation; change
refund_log_extra_bytes_count so that when memo is None it returns
REFUND_EXTRA_BYTES (the full JSON field cost), and when memo is Some(m) it
returns the additional bytes required to replace the existing memo with "refund"
i.e. max(0, REFUND_STR_LEN.saturating_sub(m.len())); update tests that assert
overhead to expect this delta-based value; relevant symbols:
refund_log_extra_bytes_count, REFUND_EXTRA_BYTES, REFUND_STR_LEN,
compute_refund_overhead, and validate_with_refund_overhead.
♻️ Duplicate comments (2)
tests/src/tests/defuse/tokens/nep245/mt_deposit_resolve_gas.rs (2)

208-221: Always validate resolve receipt presence.

The check only runs when defuse_outcomes.len() == 2; if the count differs, resolve failures can be skipped. Assert >= 2 and always inspect the second receipt.

✅ Suggested fix
-    if defuse_outcomes.len() == 2 {
-        let resolve_outcome = defuse_outcomes[1].clone();
-        let resolve_result = resolve_outcome.into_result();
-        assert!(
-            resolve_result.is_ok(),
-            "CRITICAL: mt_resolve_deposit callback failed for token_count={token_count}! \
-            This indicates insufficient gas allocation in the contract. Error: {:?}",
-            resolve_result.err()
-        );
-    }
+    anyhow::ensure!(
+        defuse_outcomes.len() >= 2,
+        "Expected at least 2 defuse receipts, got {}",
+        defuse_outcomes.len()
+    );
+    let resolve_outcome = defuse_outcomes[1].clone();
+    let resolve_result = resolve_outcome.into_result();
+    assert!(
+        resolve_result.is_ok(),
+        "CRITICAL: mt_resolve_deposit callback failed for token_count={token_count}! \
+        This indicates insufficient gas allocation in the contract. Error: {:?}",
+        resolve_result.err()
+    );

351-352: Fix typo in test name: “desposit” → “deposit”.

✏️ Proposed fix
-async fn mt_desposit_resolve_can_handle_large_blob_value_returned_from_notification() {
+async fn mt_deposit_resolve_can_handle_large_blob_value_returned_from_notification() {

@pityjllk pityjllk force-pushed the feat/mt-resolve-deposit-tests branch 2 times, most recently from 41589fe to d2724a6 Compare January 23, 2026 15:25
Comment on lines +211 to +213
[tasks.run-tests-long]
command = "cargo"
args = ["test", "--workspace", "--all-targets", "--features=long", "${@}"]
Copy link
Collaborator

Choose a reason for hiding this comment

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

thanks to ${@} part, the same can be achieved by running cargo make run-tests --features=long

Copy link
Collaborator Author

@pityjllk pityjllk Jan 23, 2026

Choose a reason for hiding this comment

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

sure but since its target to ci i think its nice to have it wrapped it specific target/script 🤔

Comment on lines +40 to +33
pub trait EmitChecked {
fn emit_with_refund_log_checked(self) -> Result<(), ErrorRefundLogTooLong>;
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

As to me, trait seems as an overkill here:

impl MtEvent<'_> {
    pub fn check_refund(&self) -> Result<RefundCheckedMtEvent, ErrorRefundLogTooLong> {
        // serialize, calculate overhead, check length ...
    }
}

pub struct RefundCheckedMtEvent(String);

impl RefundCheckedMtEvent {
    pub fn emit(&self) {
        env::log_str(&self.0);
    }
}

// usage:
event
    .check_refund()
    .emit();

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

its a trait so you can do leverage blanket implementation

so in code you just change emit() to emit_with_refund_log_checked() on types that impl Into

Comment on lines 44 to 42
const REFUND_EXTRA_BYTES: usize = r#","memo":"refund""#.len();
const REFUND_STR_LEN: usize = "refund".len();

fn refund_log_extra_bytes_count(memo: Option<&str>) -> usize {
memo.map_or(REFUND_EXTRA_BYTES, |m| {
std::cmp::max(REFUND_STR_LEN, m.len())
})
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

  1. we should re-use this memo const all over the contract code:
  2. what if first memo was long but others are without any memos?
pub const REFUND_MEMO: &str = "refund"; // <-- reuse this everywhere
const REFUND_MEMO_EXTRA_LEN: usize = r#","memo":"""#.len() + REFUND_MEMO.len();

fn refund_log_length_delta(memo: Option<&str>) -> isize {
    if let Some(memo) = memo {
        REFUND_MEMO.len() - memo.len() // you'll need to fix usize -> isize conversion here
    } else {
        REFUND_MEMO_EXTRA_LEN
    }
}

Copy link
Collaborator Author

@pityjllk pityjllk Jan 23, 2026

Choose a reason for hiding this comment

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

what if first memo was long but others are without any memos?

thats already addressed as i call refund_log_extra_bytes_count on each inner event right ?

Comment on lines 54 to 58
match event {
MtEvent::MtMint(events) => events
.iter()
.map(|e| refund_log_extra_bytes_count(e.memo.as_deref()))
.sum(),
MtEvent::MtBurn(events) => events
.iter()
.map(|e| refund_log_extra_bytes_count(e.memo.as_deref()))
.sum(),
MtEvent::MtTransfer(events) => events
.iter()
.map(|e| refund_log_extra_bytes_count(e.memo.as_deref()))
.sum(),
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

    match event {
        MtEvent::MtMint(events) => events
            .iter()
            .map(|e| e.memo.as_deref()),
        MtEvent::MtBurn(events) => events
            .iter()
            .map(|e| e.memo.as_deref()),
        MtEvent::MtTransfer(events) => events
            .iter()
            .map(|e| e.memo.as_deref()),
    }
        .sum() // <--- btw, there can be an overflow here!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

im pretty sure its not gonna work. you would need something like Either enum but with 3 variants, you could probably use Either(Either()) but its really unreadbale

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

   .sum() // <--- btw, there can be an overflow here!

initially i assumed thats fine cause if its > usize::max then its also above log length limit but fold with capping to usize::max seems more elegant now when i think of it 👍

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

🤖 Fix all issues with AI agents
In `@tests/src/tests/defuse/tokens/nep245/mt_transfer_resolve_gas.rs`:
- Line 26: Remove the local declaration of TOTAL_LOG_LENGTH_LIMIT and import the
shared constant from defuse::nep245; specifically, delete the line "const
TOTAL_LOG_LENGTH_LIMIT: usize = 16384;" in mt_transfer_resolve_gas.rs and add a
use/import for TOTAL_LOG_LENGTH_LIMIT from the defuse::nep245 module (matching
how mt_deposit_resolve_gas.rs imports it) so the test reuses the publicly
exported constant instead of redefining it.
♻️ Duplicate comments (1)
nep245/src/events.rs (1)

16-17: Duplicate constant should be imported from defuse-near-utils.

As flagged in previous reviews, TOTAL_LOG_LENGTH_LIMIT is already exported from near-utils/src/event.rs. Import it instead of redefining:

use defuse_near_utils::TOTAL_LOG_LENGTH_LIMIT;
🧹 Nitpick comments (2)
tests/src/tests/defuse/tokens/nep245/mt_transfer_resolve_gas.rs (2)

320-327: Consider documenting the token count magic numbers.

The generation of 65 max-length tokens plus 2 specific trigger tokens appears carefully calculated to hit the boundary condition. A brief comment explaining why these specific counts/lengths trigger the refund-log-exceeds-limit condition would improve maintainability:

// 65 max-length tokens keep transfer log under limit but push refund log over
// The 2 additional tokens ensure the refund event exceeds TOTAL_LOG_LENGTH_LIMIT
let token_ids: Vec<String> = (1..=65)
    ...

380-384: String-based error matching may be fragile.

If the error message in the contract changes, this assertion will silently pass with incorrect behavior. Consider also checking for a specific error type/code if the contract API supports it, or add a comment noting this coupling.

// NOTE: This string must match the error message in defuse contract's mt_batch_transfer_call
assert!(
    result_str.contains("too many tokens: refund log would exceed protocol limit"),
    ...
);

use std::{borrow::Cow, future::Future};
use strum::IntoEnumIterator;

const TOTAL_LOG_LENGTH_LIMIT: usize = 16384;
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find all definitions of TOTAL_LOG_LENGTH_LIMIT in the codebase
rg -n "TOTAL_LOG_LENGTH_LIMIT" --type rust -B 1 -A 1

Repository: near/intents

Length of output: 4904


🏁 Script executed:

# Check if there are any pub declarations of TOTAL_LOG_LENGTH_LIMIT
rg -n "pub\s+const\s+TOTAL_LOG_LENGTH_LIMIT|pub\s+use.*TOTAL_LOG_LENGTH_LIMIT" --type rust

Repository: near/intents

Length of output: 280


🏁 Script executed:

# Check the structure of the test file and its directory
git ls-files tests/src/tests/defuse/tokens/nep245/ | head -20

Repository: near/intents

Length of output: 277


🏁 Script executed:

# Check the structure of nep245 src directory
git ls-files | grep -E "nep245/src|near.*utils" | head -30

Repository: near/intents

Length of output: 474


Import TOTAL_LOG_LENGTH_LIMIT from defuse::nep245 instead of redefining locally.

The constant is already publicly exported from nep245/src/events.rs and near-utils. Remove the local definition and import it to avoid duplication. A similar test file in the same directory (mt_deposit_resolve_gas.rs) already uses this pattern:

-const TOTAL_LOG_LENGTH_LIMIT: usize = 16384;
+use defuse::nep245::TOTAL_LOG_LENGTH_LIMIT;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const TOTAL_LOG_LENGTH_LIMIT: usize = 16384;
use defuse::nep245::TOTAL_LOG_LENGTH_LIMIT;
🤖 Prompt for AI Agents
In `@tests/src/tests/defuse/tokens/nep245/mt_transfer_resolve_gas.rs` at line 26,
Remove the local declaration of TOTAL_LOG_LENGTH_LIMIT and import the shared
constant from defuse::nep245; specifically, delete the line "const
TOTAL_LOG_LENGTH_LIMIT: usize = 16384;" in mt_transfer_resolve_gas.rs and add a
use/import for TOTAL_LOG_LENGTH_LIMIT from the defuse::nep245 module (matching
how mt_deposit_resolve_gas.rs imports it) so the test reuses the publicly
exported constant instead of redefining it.

@pityjllk pityjllk force-pushed the feat/mt-resolve-deposit-tests branch from ad6f322 to 63dfb0d Compare January 23, 2026 19:40
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

🤖 Fix all issues with AI agents
In @.cargo/audit.toml:
- Around line 5-10: Update the placeholder upstream issue link in the ignore
note: replace "https://github.com/near/nearcore/issues/XXXXX (upstream)" with
the actual upstream PR URL "https://github.com/jeromefroe/lru-rs/pull/224
(merged January 7, 2026)" so the tracked line accurately references the upstream
fix for the lru advisory in the ignore comment near the ignore =
["RUSTSEC-2026-0002"] entry.
♻️ Duplicate comments (7)
Makefile.toml (1)

211-213: run-tests-long still applies --features=long to the entire workspace.

This will fail for workspace crates that don’t define the feature. Scope the run to packages that declare long, or run per-crate with -p/--package filters.

#!/bin/bash
set -euo pipefail

python - <<'PY'
import glob, pathlib, tomllib

root = pathlib.Path("Cargo.toml")
data = tomllib.loads(root.read_text())
members = data.get("workspace", {}).get("members", [])

def expand(member):
    if any(ch in member for ch in "*?[]"):
        return [pathlib.Path(p) for p in glob.glob(member)]
    return [pathlib.Path(member)]

missing = []
for m in members:
    for path in expand(m):
        cargo = path / "Cargo.toml"
        if not cargo.exists():
            continue
        pkg = tomllib.loads(cargo.read_text())
        features = pkg.get("features", {})
        if "long" not in features:
            missing.append(str(cargo))

print("\n".join(sorted(set(missing))) or "All workspace packages define feature 'long'")
PY
tests/contracts/multi-token-receiver-stub/src/lib.rs (1)

13-19: Guard stub_return_bytes length and avoid U128 → u64 truncation/panic.

The stub trusts the input length and uses unchecked casts; this can allocate huge buffers or panic before the intended limit checks.

🛠 Suggested guardrails
 pub extern "C" fn stub_return_bytes() {
     if let Some(input) = near_sdk::env::input() {
         if input.len() >= 8 {
-            let len = u64::from_le_bytes(input[..8].try_into().unwrap()) as usize;
-            let bytes = vec![0xf0u8; len];
+            const MAX_RETURN_BYTES: usize = 4 * 1024 * 1024;
+            let len = u64::from_le_bytes(input[..8].try_into().unwrap());
+            let len = usize::try_from(len)
+                .ok()
+                .filter(|l| *l <= MAX_RETURN_BYTES)
+                .unwrap_or_else(|| env::panic_str("return length too large"));
+            let bytes = vec![0xf0u8; len];
             near_sdk::env::value_return(&bytes);
         }
     }
 }
@@
-            MTReceiverMode::ReturnBytes(len) => Promise::new(env::current_account_id())
-                .stub_return_bytes(len.0.try_into().unwrap())
-                .into(),
+            MTReceiverMode::ReturnBytes(len) => {
+                let len = u64::try_from(len.0)
+                    .unwrap_or_else(|_| env::panic_str("ReturnBytes length overflows u64"));
+                Promise::new(env::current_account_id())
+                    .stub_return_bytes(len)
+                    .into()
+            }

Also applies to: 99-101

tests/src/tests/defuse/tokens/nep245/mod.rs (1)

7-26: Handle mid == 0 to avoid usize underflow.

When the test fails at mid == 0, hi = mid - 1 wraps to usize::MAX.

🛠 Safer update for the Err branch
             Err(_) => {
-                hi = mid - 1; // failure -> try lower
+                if mid == 0 {
+                    break;
+                }
+                hi = mid - 1; // failure -> try lower
             }
#!/bin/bash
# Find call sites to confirm whether low can be 0
rg -n --type=rs -C3 "binary_search_max\s*\(" tests
tests/src/tests/defuse/tokens/nep245/mt_transfer_resolve_gas.rs (1)

341-345: Reuse the shared log-length limit constant.

Line 344 uses TOTAL_LOG_LENGTH_LIMIT; to avoid drift, import it from defuse::nep245 and remove the local constant (Line 26). This was raised before and still applies.

♻️ Proposed fix
 use defuse::{
     core::token_id::{TokenId, nep245::Nep245TokenId},
-    nep245::{MtEvent, MtTransferEvent},
+    nep245::{MtEvent, MtTransferEvent, TOTAL_LOG_LENGTH_LIMIT},
 };
@@
-const TOTAL_LOG_LENGTH_LIMIT: usize = 16384;
nep245/src/events.rs (1)

3-5: Avoid a duplicate TOTAL_LOG_LENGTH_LIMIT definition.

Line 16 redefines a constant already exported by defuse-near-utils. Re-export it here instead of redefining to keep a single source of truth.

♻️ Proposed fix
-use near_sdk::{
+use near_sdk::{
     AccountIdRef, AsNep297Event, FunctionError, json_types::U128, near, serde::Deserialize,
 };
@@
-/// NEAR protocol limit for log messages (16 KiB)
-pub const TOTAL_LOG_LENGTH_LIMIT: usize = 16384;
+/// NEAR protocol limit for log messages (16 KiB)
+pub use defuse_near_utils::TOTAL_LOG_LENGTH_LIMIT;

Also applies to: 16-17

tests/src/tests/defuse/tokens/nep245/mt_deposit_resolve_gas.rs (2)

212-221: Always validate resolve receipt presence.

Line 212 only checks when defuse_outcomes.len() == 2; if the count differs, resolve failures can be silently skipped. This was flagged earlier and is still present.

✅ Suggested fix
-    if defuse_outcomes.len() == 2 {
-        let resolve_outcome = defuse_outcomes[1].clone();
-        let resolve_result = resolve_outcome.into_result();
-        assert!(
-            resolve_result.is_ok(),
-            "CRITICAL: mt_resolve_deposit callback failed for token_count={token_count}! \
-            This indicates insufficient gas allocation in the contract. Error: {:?}",
-            resolve_result.err()
-        );
-    }
+    anyhow::ensure!(
+        defuse_outcomes.len() >= 2,
+        "Expected at least 2 defuse receipts, got {}",
+        defuse_outcomes.len()
+    );
+    let resolve_outcome = defuse_outcomes[1].clone();
+    let resolve_result = resolve_outcome.into_result();
+    assert!(
+        resolve_result.is_ok(),
+        "CRITICAL: mt_resolve_deposit callback failed for token_count={token_count}! \
+        This indicates insufficient gas allocation in the contract. Error: {:?}",
+        resolve_result.err()
+    );

351-352: Fix test name typo (“desposit” → “deposit”).

The misspelling in the test name (Line 351) reduces clarity/discoverability. This is a repeat from earlier.

✏️ Proposed fix
-async fn mt_desposit_resolve_can_handle_large_blob_value_returned_from_notification() {
+async fn mt_deposit_resolve_can_handle_large_blob_value_returned_from_notification() {

Comment on lines +5 to +10
# Ignore the lru unsound advisory - it comes from near-vm-runner which is
# locked to lru ^0.12.3 and cannot be updated to the fixed 0.16.3 version.
# The advisory relates to IterMut's Stacked Borrows violation, which does
# not affect our usage as we don't use IterMut directly.
# Tracked: https://github.com/near/nearcore/issues/XXXXX (upstream)
ignore = ["RUSTSEC-2026-0002"]
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

RUSTSEC-2026-0002 lru upstream issue github

💡 Result:

RUSTSEC-2026-0002 is an unsoundness advisory for the Rust crate lru: its IterMut::next / next_back temporarily create an exclusive reference that violates Stacked Borrows by invalidating an internal pointer held by the HashMap. Affected versions are >= 0.9.0 and < 0.16.3; fixed in lru >= 0.16.3. [1][3]

Upstream GitHub fix (PR):

https://github.com/jeromefroe/lru-rs/pull/224

Merged January 7, 2026. [1][2]

Related alias: GHSA-rhfx-m35p-ff5j. [1][3]

Sources: [1] [2] [3]


Replace the placeholder upstream issue link with the actual upstream PR.

The ignore note references an issues/XXXXX placeholder. Link to the actual upstream fix: jeromefroe/lru-rs#224 (merged January 7, 2026).

🤖 Prompt for AI Agents
In @.cargo/audit.toml around lines 5 - 10, Update the placeholder upstream issue
link in the ignore note: replace "https://github.com/near/nearcore/issues/XXXXX
(upstream)" with the actual upstream PR URL
"https://github.com/jeromefroe/lru-rs/pull/224 (merged January 7, 2026)" so the
tracked line accurately references the upstream fix for the lru advisory in the
ignore comment near the ignore = ["RUSTSEC-2026-0002"] entry.

@pityjllk pityjllk force-pushed the feat/mt-resolve-deposit-tests branch from 63dfb0d to 851ca7f Compare January 23, 2026 20:00
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