Skip to content

feat: arbitrary fn call#228

Open
hlgltvnnk wants to merge 6 commits intomainfrom
feat/arbitrary-fn-call
Open

feat: arbitrary fn call#228
hlgltvnnk wants to merge 6 commits intomainfrom
feat/arbitrary-fn-call

Conversation

@hlgltvnnk
Copy link
Copy Markdown
Collaborator

@hlgltvnnk hlgltvnnk commented Mar 9, 2026

Summary by CodeRabbit

  • New Features

    • Introduced a new defuse-actions package with action abstractions for transfers and function calls.
    • Added arbitrary_call contract method to execute actions with DAO role-based access control and deposit validation.
    • Added helper utilities for generating arbitrary action test values.
  • Tests

    • Added comprehensive tests for arbitrary action execution with permission and balance validation.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 9, 2026

📝 Walkthrough

Walkthrough

This PR introduces a new defuse-actions workspace crate providing core Action trait abstractions (TransferAction, FunctionCallAction) with builder-style constructors and serialization support. These actions are integrated into the defuse contract via an ArbitraryManager trait with DAO role-based access control, enabling arbitrary NEAR action execution. Includes sandbox extensions, helper utilities, and comprehensive test coverage.

Changes

Cohort / File(s) Summary
Workspace Setup
Cargo.toml, actions/Cargo.toml
Added new actions workspace member with manifest defining workspace dependencies, features (abi, arbitrary), and near-sdk/arbitrary dependencies.
Core Action Abstractions
actions/src/lib.rs, actions/src/arbitrary_helpers.rs
Introduced Action trait (append, get_deposit) with TransferAction and FunctionCallAction implementations; builder methods for args, deposit, gas configuration; conditional arbitrary derives; helper functions for near_token and gas value generation from Unstructured input.
Defuse Crate Integration
defuse/Cargo.toml, defuse/src/lib.rs, defuse/src/arbitrary.rs
Added defuse-actions dependency and public arbitrary module; introduced ArbitraryAction enum wrapping FunctionCallAction and TransferAction; defined ArbitraryManager trait for contract interaction.
Contract Implementation
defuse/src/contract/mod.rs, defuse/src/contract/arbitrary.rs
Added private arbitrary module to contract; implemented ArbitraryManager for Contract with #[payable] #[access_control_any(roles(Role::DAO))] arbitrary_call method validating attached deposit.
Testing Infrastructure
tests/Cargo.toml, tests/src/tests/defuse/mod.rs, tests/src/tests/defuse/arbitrary.rs
Added defuse-actions dependency; introduced test module with transfer_ft_with_arbitrary_call and transfer_near_with_arbitrary_call validating DAO role enforcement and balance changes.
Sandbox Extensions
sandbox/src/extensions/defuse/mod.rs, sandbox/src/extensions/defuse/arbitrary.rs
Added arbitrary module with ArbitraryManagerExt trait providing async arbitrary_call method for SigningAccount integration with function call serialization.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Suggested reviewers

  • mitinarseny
  • pityjllk
  • fusede

Poem

🐰 New actions hop into place,
Traits and transfers set the pace,
DAO guardians check each call,
Arbitrary power to all!
With tests to prove it works just right,
The defuse protocol shines bright!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 62.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'feat: arbitrary fn call' is vague and does not clearly convey the main change. While it references an aspect of the PR (arbitrary calls), it lacks specificity about what was actually added and does not adequately summarize the substantial new functionality. Consider a more descriptive title such as 'feat: add Action trait with TransferAction and FunctionCallAction support' or 'feat: introduce arbitrary actions abstraction for near-sdk' to better reflect the core additions.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/arbitrary-fn-call

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
Copy Markdown

@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

🧹 Nitpick comments (2)
tests/src/tests/defuse/arbitrary.rs (1)

29-59: Cover the new deposit guard explicitly.

This suite never exercises the env::attached_deposit() >= action.get_deposit() check in defuse/src/contract/arbitrary.rs, Lines 15-18. Add a case where the outer deposit is smaller than FunctionCallAction.deposit so a regression there does not hide behind the permission checks.

I can draft that test if helpful.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/src/tests/defuse/arbitrary.rs` around lines 29 - 59, The test never
exercises the deposit guard (env::attached_deposit() >= action.get_deposit()) in
arbitrary.rs; add a case before granting DAO role that calls
admin.arbitrary_call(...) with an outer deposit smaller than the
FunctionCallAction.deposit (set FunctionCallAction.deposit > deposit passed to
arbitrary_call) and assert it returns an error; reference the FunctionCallAction
struct's deposit field and the arbitrary_call method to locate where to insert
this failing call and optionally assert the contract balance remains unchanged.
defuse/src/arbitrary.rs (1)

19-31: Clarify the public API’s target, auth, and funding semantics.

arbitrary_call dispatches Promise::new(account_id), so the action runs against account_id, not “on the contract”. It would also help to mention that the contract implementation is DAO-gated and that Transfer spends contract balance because this variant reports NearToken::ZERO as the required attached deposit.

✏️ Suggested doc update
 #[ext_contract(ext_arbitrary_manager)]
 pub trait ArbitraryManager {
-    /// Allows the caller to execute an arbitrary function call
-    /// or transfer on the contract.
+    /// DAO-only entrypoint that dispatches `action` from this contract to `account_id`.
+    /// `FunctionCall` forwards its attached deposit; `Transfer` spends this contract's NEAR balance.
     fn arbitrary_call(&mut self, account_id: AccountId, action: ArbitraryAction) -> Promise;
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@defuse/src/arbitrary.rs` around lines 19 - 31, The public API docs for
ArbitraryManager::arbitrary_call and the ArbitraryAction enum are ambiguous
about where the action executes, who can authorize it, and who funds a Transfer;
update the trait and related comments to state that arbitrary_call dispatches
Promise::new(account_id) so the action executes against the provided account_id
(not "on the contract"), that execution is gated by the contract's DAO
authorization (mention the DAO check in the implementation), and clarify
funding: ArbitraryAction::Transfer spends contract balance (get_deposit returns
NearToken::ZERO for Transfer, meaning callers need not attach deposit but the
contract funds the transfer) while FunctionCall requires the caller to attach
the required deposit (reference get_deposit and the FunctionCall/Transfer
variants to locate the code).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@actions/src/lib.rs`:
- Around line 39-72: The struct FunctionCallAction uses field-level
#[serde_as(...)] but lacks the container-level #[serde_as] attribute and the
serde_with import; add the container attribute #[serde_as] above the
FunctionCallAction definition and import serde_with::serde_as (and any
serde_with helpers used like DisplayFromStr/Base64 if not already imported) so
the field-level serde_with attributes (on args and gas_weight) are wired
correctly and the code compiles.
- Around line 93-107: The methods args_json and args_borsh currently call
unwrap() on fallible serializers causing panics; replace these with
non-panicking alternatives by adding fallible variants (e.g., try_args_json and
try_args_borsh) that perform serde_json::to_vec and borsh::to_vec and return
Result<Self, E> (propagate or map serialization errors into a suitable error
type), and keep or deprecate the existing convenience methods to call the try_
variants internally if desired; update the builder signatures (args_json,
args_borsh, or their try_ counterparts) to return Result so callers can handle
serialization failures instead of panicking, and reference these functions
(args_json, args_borsh, try_args_json, try_args_borsh) when making the change.

In `@defuse/Cargo.toml`:
- Around line 75-76: The `arbitrary` feature in Cargo.toml needs to propagate
into defuse-actions so the Arbitrary derives for FunctionCallAction and
TransferAction are enabled; update the `arbitrary` feature list to include
"defuse-actions/arbitrary" (so consumers enabling `defuse/arbitrary` also enable
the gated derives/attributes used in actions/src/lib.rs and
actions/src/arbitrary.rs), ensuring defuse/src/arbitrary.rs re-exports have
complete Arbitrary implementations.

---

Nitpick comments:
In `@defuse/src/arbitrary.rs`:
- Around line 19-31: The public API docs for ArbitraryManager::arbitrary_call
and the ArbitraryAction enum are ambiguous about where the action executes, who
can authorize it, and who funds a Transfer; update the trait and related
comments to state that arbitrary_call dispatches Promise::new(account_id) so the
action executes against the provided account_id (not "on the contract"), that
execution is gated by the contract's DAO authorization (mention the DAO check in
the implementation), and clarify funding: ArbitraryAction::Transfer spends
contract balance (get_deposit returns NearToken::ZERO for Transfer, meaning
callers need not attach deposit but the contract funds the transfer) while
FunctionCall requires the caller to attach the required deposit (reference
get_deposit and the FunctionCall/Transfer variants to locate the code).

In `@tests/src/tests/defuse/arbitrary.rs`:
- Around line 29-59: The test never exercises the deposit guard
(env::attached_deposit() >= action.get_deposit()) in arbitrary.rs; add a case
before granting DAO role that calls admin.arbitrary_call(...) with an outer
deposit smaller than the FunctionCallAction.deposit (set
FunctionCallAction.deposit > deposit passed to arbitrary_call) and assert it
returns an error; reference the FunctionCallAction struct's deposit field and
the arbitrary_call method to locate where to insert this failing call and
optionally assert the contract balance remains unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 1d1d2043-2ff8-4b7c-9e73-25c945f098a0

📥 Commits

Reviewing files that changed from the base of the PR and between ea2975d and 6c72da1.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (14)
  • Cargo.toml
  • actions/Cargo.toml
  • actions/src/arbitrary.rs
  • actions/src/lib.rs
  • defuse/Cargo.toml
  • defuse/src/arbitrary.rs
  • defuse/src/contract/arbitrary.rs
  • defuse/src/contract/mod.rs
  • defuse/src/lib.rs
  • sandbox/src/extensions/defuse/arbitrary.rs
  • sandbox/src/extensions/defuse/mod.rs
  • tests/Cargo.toml
  • tests/src/tests/defuse/arbitrary.rs
  • tests/src/tests/defuse/mod.rs

Comment thread actions/src/lib.rs
Comment on lines +39 to +72
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
#[near(serializers = [borsh, json])]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct FunctionCallAction {
pub function_name: String,

#[cfg_attr(
all(feature = "abi", not(target_arch = "wasm32")),
schemars(with = "String")
)]
#[serde_as(as = "Base64")]
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub args: Vec<u8>,

#[cfg_attr(
feature = "arbitrary",
arbitrary(with = crate::arbitrary::near_token),
)]
#[serde(default, skip_serializing_if = "NearToken::is_zero")]
pub deposit: NearToken,

#[cfg_attr(
feature = "arbitrary",
arbitrary(with = crate::arbitrary::gas),
)]
#[serde(default, skip_serializing_if = "Gas::is_zero")]
pub min_gas: Gas,

#[serde_as(as = "DisplayFromStr")]
#[serde(
default = "default_gas_weight",
skip_serializing_if = "is_default_gas_weight"
)]
pub gas_weight: u64,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Inspect existing serde_with macro usage in this crate.
rg -n -C2 'serde_as|extern crate serde_with|macro_use' actions/src

Repository: near/intents

Length of output: 549


🏁 Script executed:

head -40 actions/src/lib.rs

Repository: near/intents

Length of output: 1005


🏁 Script executed:

sed -n '39,80p' actions/src/lib.rs

Repository: near/intents

Length of output: 1262


🏁 Script executed:

cat actions/Cargo.toml | head -50

Repository: near/intents

Length of output: 450


🏁 Script executed:

# Check if there are any build artifacts or CI logs indicating compile status
git log --oneline -n 5 actions/src/lib.rs

Repository: near/intents

Length of output: 70


Wire up serde_with::serde_as correctly.

Lines 49 and 67 use field-level #[serde_as(...)] attributes, but FunctionCallAction is missing the container-level #[serde_as] macro, and serde_as is not imported. As written, the serde-with configuration is incomplete and will not compile.

Suggested fix
 use near_sdk::{
     Gas, GasWeight, NearToken, Promise,
     borsh::{self, BorshSerialize},
     near,
     serde::Serialize,
     serde_json,
-    serde_with::{DisplayFromStr, base64::Base64},
+    serde_with::{serde_as, DisplayFromStr, base64::Base64},
 };
 ...
 #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
+#[serde_as]
 #[near(serializers = [borsh, json])]
 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
 pub struct FunctionCallAction {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@actions/src/lib.rs` around lines 39 - 72, The struct FunctionCallAction uses
field-level #[serde_as(...)] but lacks the container-level #[serde_as] attribute
and the serde_with import; add the container attribute #[serde_as] above the
FunctionCallAction definition and import serde_with::serde_as (and any
serde_with helpers used like DisplayFromStr/Base64 if not already imported) so
the field-level serde_with attributes (on args and gas_weight) are wired
correctly and the code compiles.

Comment thread actions/src/lib.rs
Comment on lines +93 to +107
#[must_use]
pub fn args_json<T>(self, args: T) -> Self
where
T: Serialize,
{
self.args(serde_json::to_vec(&args).unwrap())
}

#[must_use]
pub fn args_borsh<T>(self, args: T) -> Self
where
T: BorshSerialize,
{
self.args(borsh::to_vec(&args).unwrap())
}
Copy link
Copy Markdown

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
# Find current call sites before making these builders fallible.
rg -nP -C2 '\.(args_json|args_borsh)\s*\('

Repository: near/intents

Length of output: 38


🏁 Script executed:

cat -n actions/src/lib.rs | sed -n '85,115p'

Repository: near/intents

Length of output: 893


🏁 Script executed:

# Check if try_args_json or try_args_borsh variants already exist
rg -nP '(try_args_json|try_args_borsh)'

Repository: near/intents

Length of output: 38


🏁 Script executed:

# Search more broadly for args_json usage across the repo
rg -nP 'args_json' --type rust

Repository: near/intents

Length of output: 124


🏁 Script executed:

# Check if ActionBuilder is exported from lib.rs and understand the public API
head -50 actions/src/lib.rs

Repository: near/intents

Length of output: 1322


🏁 Script executed:

# Check Cargo.toml to understand if this is a public library
cat actions/Cargo.toml

Repository: near/intents

Length of output: 450


🏁 Script executed:

# Find the ActionBuilder struct definition to understand the full context
rg -nP 'struct ActionBuilder' -A 15

Repository: near/intents

Length of output: 38


🏁 Script executed:

# Search for ActionBuilder more broadly
rg -nP 'ActionBuilder' -A 5

Repository: near/intents

Length of output: 38


🏁 Script executed:

# Look for the struct definition that contains args_json method
rg -nP 'struct\s+\w+' actions/src/lib.rs

Repository: near/intents

Length of output: 121


🏁 Script executed:

# Find what struct the args_json method belongs to by looking around lines 93-107
sed -n '60,95p' actions/src/lib.rs

Repository: near/intents

Length of output: 958


🏁 Script executed:

# Check if there are impl blocks
rg -nP 'impl\s+' actions/src/lib.rs -A 2

Repository: near/intents

Length of output: 606


🏁 Script executed:

# Check what's exported from the crate's lib.rs module
tail -50 actions/src/lib.rs

Repository: near/intents

Length of output: 1165


🏁 Script executed:

# Check if FunctionCallAction is re-exported in the workspace root
find . -name "lib.rs" -type f | head -5 | xargs grep -l "FunctionCallAction" 2>/dev/null

Repository: near/intents

Length of output: 76


🏁 Script executed:

# Check the root lib.rs or mod declarations to confirm public API exposure
rg -nP 'pub.*mod\s+actions|pub.*use.*FunctionCallAction' . --type rust

Repository: near/intents

Length of output: 38


🏁 Script executed:

# Check if there are any comments or TODOs about error handling in this file
rg -nP '(TODO|FIXME|XXX|HACK).*(?:error|unwrap|panic)' actions/src/lib.rs

Repository: near/intents

Length of output: 38


🏁 Script executed:

# Check Cargo.toml at the workspace root to understand package structure
cat Cargo.toml

Repository: near/intents

Length of output: 3892


🏁 Script executed:

# Check actions/Cargo.toml to see if it's published/public
cat actions/Cargo.toml

Repository: near/intents

Length of output: 450


🏁 Script executed:

# Look for README or documentation that describes the library purpose
find actions -name "README*" -o -name "*.md" | head -5

Repository: near/intents

Length of output: 38


🏁 Script executed:

# Search for whether borsh::to_vec or serde_json::to_vec can fail
# by checking their documentation in the nearcore/SDK or common patterns
rg -nP 'Result.*to_vec' . --type rust | head -10

Repository: near/intents

Length of output: 38


🏁 Script executed:

# Check if serde_json::to_vec can actually fail - look at its usage in tests or examples
rg -nP 'to_vec' actions/src/lib.rs -B 2 -A 2

Repository: near/intents

Length of output: 272


🏁 Script executed:

# Verify what serialization errors these libraries can produce
# by checking if there are any error handling patterns elsewhere
rg -nP 'serde_json::.*Result|borsh::.*Result' . --type rust | head -5

Repository: near/intents

Length of output: 178


🏁 Script executed:

# Verify the signature of serde_json::to_vec and borsh::to_vec
# by checking what near-sdk exports
rg -nP '(pub fn to_vec|type.*Result).*serde_json|borsh' /dev/stdin 2>/dev/null || echo "Checking near-sdk docs..."

# Instead, search for any documentation or patterns in codebase
rg -nP 'serde_json::to_vec.*\?' . --type rust

Repository: near/intents

Length of output: 81


🏁 Script executed:

# Check if there are any serialization error patterns in the codebase
rg -nP 'Error|serde_json.*error|borsh.*error' actions/src/lib.rs

Repository: near/intents

Length of output: 38


Avoid panicking on serialization errors in public builder methods.

The args_json and args_borsh methods call unwrap() on fallible serializers (lines 98 and 106). Both serde_json::to_vec and borsh::to_vec return Result types and can fail for valid serializable objects. In a public library API, callers cannot handle these panics. Consider providing fallible variants (try_args_json / try_args_borsh) that return Result, or add a non-panicking error handling path.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@actions/src/lib.rs` around lines 93 - 107, The methods args_json and
args_borsh currently call unwrap() on fallible serializers causing panics;
replace these with non-panicking alternatives by adding fallible variants (e.g.,
try_args_json and try_args_borsh) that perform serde_json::to_vec and
borsh::to_vec and return Result<Self, E> (propagate or map serialization errors
into a suitable error type), and keep or deprecate the existing convenience
methods to call the try_ variants internally if desired; update the builder
signatures (args_json, args_borsh, or their try_ counterparts) to return Result
so callers can handle serialization failures instead of panicking, and reference
these functions (args_json, args_borsh, try_args_json, try_args_borsh) when
making the change.

Comment thread defuse/Cargo.toml Outdated
Copy link
Copy Markdown

@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)
actions/src/lib.rs (2)

39-72: ⚠️ Potential issue | 🔴 Critical

Add the missing serde_with::serde_as wiring.

Line 49 and Line 67 use field-level #[serde_as(...)], but FunctionCallAction still lacks the container-level #[serde_as], and serde_as is not imported. That leaves the serde-with configuration incomplete and should fail to compile.

Suggested fix
 use near_sdk::{
     Gas, GasWeight, NearToken, Promise,
     borsh::{self, BorshSerialize},
     near,
     serde::Serialize,
     serde_json,
-    serde_with::{DisplayFromStr, base64::Base64},
+    serde_with::{serde_as, DisplayFromStr, base64::Base64},
 };
 ...
 #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
+#[serde_as]
 #[near(serializers = [borsh, json])]
 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
 pub struct FunctionCallAction {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@actions/src/lib.rs` around lines 39 - 72, The struct FunctionCallAction uses
field-level #[serde_as(...)] attributes (on args and gas_weight) but lacks the
container-level #[serde_as] attribute and the serde_as import; fix by adding a
container attribute #[serde_as] above the FunctionCallAction definition and add
use serde_with::serde_as; (or equivalent) to imports so the field-level
serde_with annotations (e.g., #[serde_as(as = "Base64")] on args and
#[serde_as(as = "DisplayFromStr")] on gas_weight) compile correctly.

93-107: ⚠️ Potential issue | 🟠 Major

Don’t panic on serialization errors in public builders.

serde_json::to_vec and borsh::to_vec are fallible, so the unwrap() calls on Line 98 and Line 106 make args_json() / args_borsh() panic on valid caller input. Please expose fallible variants (try_args_json / try_args_borsh) or return Result<Self, _> here so callers can handle serialization failures.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@actions/src/lib.rs` around lines 93 - 107, The current args_json and
args_borsh builders call serde_json::to_vec(...).unwrap() and
borsh::to_vec(...).unwrap(), which can panic; change these to fallible APIs:
either add try_args_json and try_args_borsh that return Result<Self, E>
(propagating the serialization error) or change args_json/args_borsh to return
Result<Self, E> instead of Self; replace the unwrap() calls with proper error
propagation (using ? or map_err) and update callers/tests accordingly so
serialization failures are surfaced rather than panicking (refer to the
args_json and args_borsh methods for the exact symbols to modify).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@actions/src/arbitrary_helpers.rs`:
- Around line 9-11: The gas() helper currently generates min_gas up to 300 TGas
independently, which can conflict with gas_weight applied later in
FunctionCallAction::append() via Promise::function_call_weight(); change the
approach so min_gas and gas_weight are generated together and validated:
implement a custom Arbitrary for FunctionCallAction (or a combined generator
used when constructing FunctionCallAction) that produces a consistent pair
(min_gas, gas_weight) by either capping min_gas to account for the extra gas
added by function_call_weight() or by sampling gas_weight first and then
restricting min_gas to a safe maximum (e.g., total_prepaid_limit -
function_call_weight(gas_weight)); update uses of gas() or replace it in
FunctionCallAction construction to use this coordinated generator so tests no
longer produce "Exceeded the prepaid gas." failures.

---

Duplicate comments:
In `@actions/src/lib.rs`:
- Around line 39-72: The struct FunctionCallAction uses field-level
#[serde_as(...)] attributes (on args and gas_weight) but lacks the
container-level #[serde_as] attribute and the serde_as import; fix by adding a
container attribute #[serde_as] above the FunctionCallAction definition and add
use serde_with::serde_as; (or equivalent) to imports so the field-level
serde_with annotations (e.g., #[serde_as(as = "Base64")] on args and
#[serde_as(as = "DisplayFromStr")] on gas_weight) compile correctly.
- Around line 93-107: The current args_json and args_borsh builders call
serde_json::to_vec(...).unwrap() and borsh::to_vec(...).unwrap(), which can
panic; change these to fallible APIs: either add try_args_json and
try_args_borsh that return Result<Self, E> (propagating the serialization error)
or change args_json/args_borsh to return Result<Self, E> instead of Self;
replace the unwrap() calls with proper error propagation (using ? or map_err)
and update callers/tests accordingly so serialization failures are surfaced
rather than panicking (refer to the args_json and args_borsh methods for the
exact symbols to modify).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d7e058a4-64ca-439a-9e23-dfec6dd8e404

📥 Commits

Reviewing files that changed from the base of the PR and between 6c72da1 and f89c415.

📒 Files selected for processing (3)
  • actions/src/arbitrary_helpers.rs
  • actions/src/lib.rs
  • defuse/Cargo.toml
🚧 Files skipped from review as they are similar to previous changes (1)
  • defuse/Cargo.toml

Comment on lines +9 to +11
pub fn gas(u: &mut Unstructured<'_>) -> Result<Gas> {
u.int_in_range(0..=Gas::from_tgas(300).as_gas())
.map(Gas::from_gas)
Copy link
Copy Markdown

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:

find . -type f -name "*.rs" | grep -E "(arbitrary_helpers|lib)" | head -20

Repository: near/intents

Length of output: 510


🏁 Script executed:

ls -la actions/src/ 2>/dev/null || find . -type d -name "actions" | head -5

Repository: near/intents

Length of output: 292


🏁 Script executed:

git ls-files | grep -E "actions.*\.rs$" | head -20

Repository: near/intents

Length of output: 107


🏁 Script executed:

cat -n actions/src/arbitrary_helpers.rs

Repository: near/intents

Length of output: 541


🏁 Script executed:

cat -n actions/src/lib.rs

Repository: near/intents

Length of output: 5058


🏁 Script executed:

rg -i "function_call_weight|GasWeight" --type rs | head -40

Repository: near/intents

Length of output: 82


🏁 Script executed:

# Search for any validation or constraint logic involving min_gas and gas_weight
rg "min_gas.*gas_weight|gas_weight.*min_gas" --type rs -A 3 -B 3

Repository: near/intents

Length of output: 82


🏁 Script executed:

# Check if there are any tests or examples showing the expected constraints
fd -e rs -x grep -l "FunctionCallAction\|function_call_weight" {} \; | xargs grep -l "test\|example" 2>/dev/null | head -5

Repository: near/intents

Length of output: 148


🏁 Script executed:

rg -i "function_call_weight" | head -20

Repository: near/intents

Length of output: 270


🏁 Script executed:

rg "GasWeight" | head -20

Repository: near/intents

Length of output: 607


🏁 Script executed:

# Check the test file that was found
cat -n ./tests/src/tests/defuse/arbitrary.rs | head -100

Repository: near/intents

Length of output: 4062


🏁 Script executed:

# Check if there are any comments or documentation about gas constraints
rg "min_gas|gas_weight|function_call_weight" -A 5 -B 5 | grep -E "//|/\*|gas.*limit|overflow" | head -30

Repository: near/intents

Length of output: 2720


🏁 Script executed:

# Look for Cargo.toml to see near_sdk version and dependencies
cat -n Cargo.toml 2>/dev/null || find . -maxdepth 2 -name "Cargo.toml" -exec cat -n {} \;

Repository: near/intents

Length of output: 4956


🏁 Script executed:

# Check if there are any validation tests or constraints
rg "test.*gas|gas.*test" --type rust -A 10 | head -50

Repository: near/intents

Length of output: 4166


🏁 Script executed:

# Look for any Arbitrary impl for FunctionCallAction to see if there's custom logic
rg "impl.*Arbitrary.*FunctionCallAction" -A 20

Repository: near/intents

Length of output: 38


🏁 Script executed:

# Check if there's documentation or comments about gas_weight behavior
rg "gas_weight" -B 5 -A 5 | head -80

Repository: near/intents

Length of output: 5419


🏁 Script executed:

# Search for any constraint validation between min_gas and gas_weight
rg "gas_weight.*0|if.*gas_weight" | head -20

Repository: near/intents

Length of output: 1784


🏁 Script executed:

# Look at default_gas_weight to understand what values gas_weight takes
rg "default_gas_weight\|GasWeight::default" -B 2 -A 2

Repository: near/intents

Length of output: 38


🏁 Script executed:

# Check what the default gas_weight value is
rg "GasWeight::default" -A 3

Repository: near/intents

Length of output: 206


🏁 Script executed:

# Look for any validation or constraints on gas_weight in near_sdk or Promise calls
rg "with_static_gas|with_unused_gas_weight|function_call_weight" -B 2 -A 2 | head -60

Repository: near/intents

Length of output: 4262


🏁 Script executed:

# Check if FunctionCallAction has any custom validation logic
rg "impl.*FunctionCallAction" -A 30

Repository: near/intents

Length of output: 4884


🏁 Script executed:

# Search for any constraints or validation between min_gas and gas_weight
rg "min_gas.*gas_weight|gas_weight.*min_gas|GasWeight.*0|unused_gas_weight" -B 3 -A 3 | head -100

Repository: near/intents

Length of output: 7841


🏁 Script executed:

# Look for any documentation or constraints about min_gas + gas_weight interaction
rg "GasWeight\|function_call_weight" -B 10 -A 5 | head -120

Repository: near/intents

Length of output: 38


🏁 Script executed:

# Check if there's validation logic in tests that shows constraints
rg "min_gas.*300|300.*min_gas|gas_weight.*0|validate" | head -30

Repository: near/intents

Length of output: 2694


🏁 Script executed:

# Look at the exact_gas method which sets both min_gas and gas_weight
rg "exact_gas" -B 2 -A 2

Repository: near/intents

Length of output: 273


🏁 Script executed:

# Check if there are any comments about the relationship between these fields
rg "min_gas|gas_weight" -B 3 -A 3 | grep -E "//|/\*" | head -40

Repository: near/intents

Length of output: 3109


🏁 Script executed:

# Search for the test file that mentions min_gas: Some(Gas::from_tgas(300))
cat -n tests/src/tests/defuse/intents/ft_withdraw.rs | grep -A 20 -B 5 "300"

Repository: near/intents

Length of output: 2019


🏁 Script executed:

# Check what GasWeight::default() actually returns
rg "impl.*Default.*GasWeight|const.*GasWeight" | head -10

Repository: near/intents

Length of output: 38


🏁 Script executed:

# Look at the Arbitrary derive on FunctionCallAction more carefully
cat -n actions/src/lib.rs | sed -n '39,73p'

Repository: near/intents

Length of output: 1331


🏁 Script executed:

# Check the actual test error message and context around the 300 TGas failure
cat -n tests/src/tests/defuse/intents/ft_withdraw.rs | sed -n '130,160p'

Repository: near/intents

Length of output: 1204


🏁 Script executed:

# Search for any comments or documentation about how min_gas and gas_weight should interact
rg "Arbitrary\|arbitrary\|fuzz\|property" --type rust -i | grep -i "gas\|weight" | head -20

Repository: near/intents

Length of output: 38


🌐 Web query:

near_sdk Promise function_call_weight gas_weight semantics min_gas

💡 Result:

Promise::function_call_weight (near-sdk-rs) — gas, GasWeight, and “min_gas” semantics

  • Signature: function_call_weight(function_name, args, amount, gas: Gas, weight: GasWeight) schedules a cross-contract call like function_call, but also assigns a weight used to distribute unused (leftover) gas from the current execution to outbound calls. [1]

  • gas is the “minimum / base” gas attached to that call. You still provide an explicit Gas value, and the protocol may later add extra gas on top from whatever remains unused when the current method finishes. So, conceptually:

    • attached_gas_for_call = gas + (share_of_leftover_gas_by_weight) [1][2][3]
  • GasWeight (weight) semantics: leftover gas is split proportionally across all function calls scheduled with weights (in the same scheduling execution). Default weight is 1. Example from docs: if 40 gas is leftover and weights are 1, 5, 2, the leftover is split 5, 25, 10. [2]
    Nearcore describes this as specifying a ratio of unspent gas computed after the current call has finished (NEP-264 / promise_batch_action_function_call_weight). [3]

  • What “min_gas” usually refers to (common confusion):

    • On NEAR, min_gas_price is a protocol configuration parameter (a floor on gas price), returned by the RPC /protocol_config response—it’s not the same thing as the gas you attach to a promise action. [4]
    • In many contracts/examples you may also see a variable named min_gas meaning “minimum gas required for this specific pattern” (e.g., X + Y for call+callback), but that’s an app-level convention, not a near-sdk builtin.

Sources: Promise docs [1], GasWeight docs [2], nearcore gas/NEP-264 explanation [3], NEAR RPC protocol config (min_gas_price) [4].


Coordinate arbitrary min_gas with gas_weight generation.

The gas() helper independently generates min_gas up to 300 TGas, but FunctionCallAction combines this with gas_weight (which defaults to nonzero) via Promise::function_call_weight() in the append() method. Since function_call_weight allocates additional gas based on the weight, generating maximum min_gas with the default nonzero gas_weight creates internally inconsistent arbitrary values that fail execution for generator reasons rather than exercising meaningful application states. This is evidenced by tests showing 300 TGas failing with "Exceeded the prepaid gas." errors.

Either implement a custom Arbitrary impl for FunctionCallAction that validates min_gas and gas_weight as a coordinated pair, or generate these fields together with constraints that prevent invalid combinations.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@actions/src/arbitrary_helpers.rs` around lines 9 - 11, The gas() helper
currently generates min_gas up to 300 TGas independently, which can conflict
with gas_weight applied later in FunctionCallAction::append() via
Promise::function_call_weight(); change the approach so min_gas and gas_weight
are generated together and validated: implement a custom Arbitrary for
FunctionCallAction (or a combined generator used when constructing
FunctionCallAction) that produces a consistent pair (min_gas, gas_weight) by
either capping min_gas to account for the extra gas added by
function_call_weight() or by sampling gas_weight first and then restricting
min_gas to a safe maximum (e.g., total_prepaid_limit -
function_call_weight(gas_weight)); update uses of gas() or replace it in
FunctionCallAction construction to use this coordinated generator so tests no
longer produce "Exceeded the prepaid gas." failures.

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.

1 participant