From 35534ab4e73f013023bdd765448e14ef4122e10c Mon Sep 17 00:00:00 2001 From: Brice Dobry Date: Mon, 15 Sep 2025 15:40:27 -0400 Subject: [PATCH 01/35] feat: initial implementation for `restric-assets?` This just adds the basic support for the syntax and some setup for the full implementation. --- clarity-types/src/errors/analysis.rs | 4 + .../src/vm/analysis/arithmetic_checker/mod.rs | 4 +- .../src/vm/analysis/read_only_checker/mod.rs | 17 +++ .../type_checker/v2_05/natives/mod.rs | 11 +- .../src/vm/analysis/type_checker/v2_1/mod.rs | 6 +- .../analysis/type_checker/v2_1/natives/mod.rs | 55 +++++++-- clarity/src/vm/costs/cost_functions.rs | 3 + clarity/src/vm/costs/costs_1.rs | 8 +- clarity/src/vm/costs/costs_2.rs | 8 +- clarity/src/vm/costs/costs_2_testnet.rs | 8 +- clarity/src/vm/costs/costs_3.rs | 8 +- clarity/src/vm/costs/costs_4.rs | 10 +- clarity/src/vm/docs/mod.rs | 92 ++++++++++----- clarity/src/vm/functions/mod.rs | 18 ++- clarity/src/vm/functions/post_conditions.rs | 110 ++++++++++++++++++ .../src/chainstate/stacks/boot/costs-4.clar | 3 + stackslib/src/clarity_vm/tests/costs.rs | 1 + 17 files changed, 302 insertions(+), 64 deletions(-) create mode 100644 clarity/src/vm/functions/post_conditions.rs diff --git a/clarity-types/src/errors/analysis.rs b/clarity-types/src/errors/analysis.rs index 09e4e5f056..f4c1a3d7bf 100644 --- a/clarity-types/src/errors/analysis.rs +++ b/clarity-types/src/errors/analysis.rs @@ -307,6 +307,9 @@ pub enum CheckErrors { // time checker errors ExecutionTimeExpired, + + // contract post-conditions + RestrictAssetsExpectedListOfAllowances, } #[derive(Debug, PartialEq)] @@ -605,6 +608,7 @@ impl DiagnosableError for CheckErrors { CheckErrors::CostComputationFailed(s) => format!("contract cost computation failed: {s}"), CheckErrors::CouldNotDetermineSerializationType => "could not determine the input type for the serialization function".into(), CheckErrors::ExecutionTimeExpired => "execution time expired".into(), + CheckErrors::RestrictAssetsExpectedListOfAllowances => "restrict-assets? expects a list of asset allowances as its second argument".into(), } } diff --git a/clarity/src/vm/analysis/arithmetic_checker/mod.rs b/clarity/src/vm/analysis/arithmetic_checker/mod.rs index 83be68ab23..95b459c227 100644 --- a/clarity/src/vm/analysis/arithmetic_checker/mod.rs +++ b/clarity/src/vm/analysis/arithmetic_checker/mod.rs @@ -175,7 +175,9 @@ impl ArithmeticOnlyChecker<'_> { | StxGetAccount => Err(Error::FunctionNotPermitted(function)), Append | Concat | AsMaxLen | ContractOf | PrincipalOf | ListCons | Print | AsContract | ElementAt | ElementAtAlias | IndexOf | IndexOfAlias | Map | Filter - | Fold | Slice | ReplaceAt | ContractHash => Err(Error::FunctionNotPermitted(function)), + | Fold | Slice | ReplaceAt | ContractHash | RestrictAssets => { + Err(Error::FunctionNotPermitted(function)) + } BuffToIntLe | BuffToUIntLe | BuffToIntBe | BuffToUIntBe => { Err(Error::FunctionNotPermitted(function)) } diff --git a/clarity/src/vm/analysis/read_only_checker/mod.rs b/clarity/src/vm/analysis/read_only_checker/mod.rs index 21b2bbd8f7..5eeb1b014f 100644 --- a/clarity/src/vm/analysis/read_only_checker/mod.rs +++ b/clarity/src/vm/analysis/read_only_checker/mod.rs @@ -427,6 +427,23 @@ impl<'a, 'b> ReadOnlyChecker<'a, 'b> { self.check_each_expression_is_read_only(&args[2..]) .map(|args_read_only| args_read_only && is_function_read_only) } + RestrictAssets => { + check_arguments_at_least(3, args)?; + + // Check the asset owner argument. + let asset_owner_read_only = self.check_read_only(&args[0])?; + + // Check the allowances argument. + let allowances = args[1] + .match_list() + .ok_or(CheckErrors::RestrictAssetsExpectedListOfAllowances)?; + let allowances_read_only = self.check_each_expression_is_read_only(allowances)?; + + // Check the body expressions. + let body_read_only = self.check_each_expression_is_read_only(&args[2..])?; + + Ok(asset_owner_read_only && allowances_read_only && body_read_only) + } } } diff --git a/clarity/src/vm/analysis/type_checker/v2_05/natives/mod.rs b/clarity/src/vm/analysis/type_checker/v2_05/natives/mod.rs index 08297f7440..99bb0583c8 100644 --- a/clarity/src/vm/analysis/type_checker/v2_05/natives/mod.rs +++ b/clarity/src/vm/analysis/type_checker/v2_05/natives/mod.rs @@ -16,15 +16,15 @@ use stacks_common::types::StacksEpochId; -use super::{check_argument_count, check_arguments_at_least, no_type, TypeChecker, TypingContext}; +use super::{TypeChecker, TypingContext, check_argument_count, check_arguments_at_least, no_type}; use crate::vm::analysis::errors::{CheckError, CheckErrors, SyntaxBindingErrorType}; use crate::vm::costs::cost_functions::ClarityCostFunction; use crate::vm::costs::{analysis_typecheck_cost, runtime_cost}; use crate::vm::diagnostic::DiagnosableError; -use crate::vm::functions::{handle_binding_list, NativeFunctions}; +use crate::vm::functions::{NativeFunctions, handle_binding_list}; use crate::vm::types::{ - BlockInfoProperty, FixedFunction, FunctionArg, FunctionSignature, FunctionType, PrincipalData, - TupleTypeSignature, TypeSignature, Value, BUFF_20, BUFF_32, BUFF_33, BUFF_64, BUFF_65, + BUFF_20, BUFF_32, BUFF_33, BUFF_64, BUFF_65, BlockInfoProperty, FixedFunction, FunctionArg, + FunctionSignature, FunctionType, PrincipalData, TupleTypeSignature, TypeSignature, Value, }; use crate::vm::{ClarityName, ClarityVersion, SymbolicExpression, SymbolicExpressionType}; @@ -782,7 +782,8 @@ impl TypedNativeFunction { | StringToUInt | IntToAscii | IntToUtf8 | GetBurnBlockInfo | StxTransferMemo | StxGetAccount | BitwiseAnd | BitwiseOr | BitwiseNot | BitwiseLShift | BitwiseRShift | BitwiseXor2 | Slice | ToConsensusBuff | FromConsensusBuff - | ReplaceAt | GetStacksBlockInfo | GetTenureInfo | ContractHash | ToAscii => { + | ReplaceAt | GetStacksBlockInfo | GetTenureInfo | ContractHash | ToAscii + | RestrictAssets => { return Err(CheckErrors::Expects( "Clarity 2+ keywords should not show up in 2.05".into(), )); diff --git a/clarity/src/vm/analysis/type_checker/v2_1/mod.rs b/clarity/src/vm/analysis/type_checker/v2_1/mod.rs index b3f1386a72..8bed512c34 100644 --- a/clarity/src/vm/analysis/type_checker/v2_1/mod.rs +++ b/clarity/src/vm/analysis/type_checker/v2_1/mod.rs @@ -1135,7 +1135,7 @@ impl<'a, 'b> TypeChecker<'a, 'b> { Ok(()) } - // Type check an expression, with an expected_type that should _admit_ the expression. + /// Type check an expression, with an expected_type that should _admit_ the expression. pub fn type_check_expects( &mut self, expr: &SymbolicExpression, @@ -1157,7 +1157,7 @@ impl<'a, 'b> TypeChecker<'a, 'b> { } } - // Type checks an expression, recursively type checking its subexpressions + /// Type checks an expression, recursively type checking its subexpressions pub fn type_check( &mut self, expr: &SymbolicExpression, @@ -1176,6 +1176,8 @@ impl<'a, 'b> TypeChecker<'a, 'b> { result } + /// Type checks a list of statements, ensuring that each statement is valid + /// and any responses before the last statement are handled. fn type_check_consecutive_statements( &mut self, args: &[SymbolicExpression], diff --git a/clarity/src/vm/analysis/type_checker/v2_1/natives/mod.rs b/clarity/src/vm/analysis/type_checker/v2_1/natives/mod.rs index 161e6a5689..b7ed52dde1 100644 --- a/clarity/src/vm/analysis/type_checker/v2_1/natives/mod.rs +++ b/clarity/src/vm/analysis/type_checker/v2_1/natives/mod.rs @@ -17,23 +17,23 @@ use stacks_common::types::StacksEpochId; use super::{ - check_argument_count, check_arguments_at_least, check_arguments_at_most, - compute_typecheck_cost, no_type, TypeChecker, TypingContext, + TypeChecker, TypingContext, check_argument_count, check_arguments_at_least, + check_arguments_at_most, compute_typecheck_cost, no_type, }; use crate::vm::analysis::errors::{CheckError, CheckErrors, SyntaxBindingErrorType}; use crate::vm::costs::cost_functions::ClarityCostFunction; -use crate::vm::costs::{analysis_typecheck_cost, runtime_cost, CostErrors, CostTracker}; +use crate::vm::costs::{CostErrors, CostTracker, analysis_typecheck_cost, runtime_cost}; use crate::vm::diagnostic::DiagnosableError; -use crate::vm::functions::{handle_binding_list, NativeFunctions}; +use crate::vm::functions::{NativeFunctions, handle_binding_list}; use crate::vm::types::signatures::{ - CallableSubtype, FunctionArgSignature, FunctionReturnsSignature, SequenceSubtype, ASCII_40, + ASCII_40, CallableSubtype, FunctionArgSignature, FunctionReturnsSignature, SequenceSubtype, TO_ASCII_MAX_BUFF, TO_ASCII_RESPONSE_STRING, UTF8_40, }; use crate::vm::types::{ - BlockInfoProperty, BufferLength, BurnBlockInfoProperty, FixedFunction, FunctionArg, - FunctionSignature, FunctionType, PrincipalData, StacksBlockInfoProperty, TenureInfoProperty, - TupleTypeSignature, TypeSignature, Value, BUFF_1, BUFF_20, BUFF_32, BUFF_33, BUFF_64, BUFF_65, - MAX_VALUE_SIZE, + BUFF_1, BUFF_20, BUFF_32, BUFF_33, BUFF_64, BUFF_65, BlockInfoProperty, BufferLength, + BurnBlockInfoProperty, FixedFunction, FunctionArg, FunctionSignature, FunctionType, + MAX_VALUE_SIZE, PrincipalData, StacksBlockInfoProperty, TenureInfoProperty, TupleTypeSignature, + TypeSignature, Value, }; use crate::vm::{ClarityName, ClarityVersion, SymbolicExpression, SymbolicExpressionType}; @@ -819,6 +819,42 @@ fn check_get_tenure_info( Ok(TypeSignature::new_option(block_info_prop.type_result())?) } +fn check_restrict_assets( + checker: &mut TypeChecker, + args: &[SymbolicExpression], + context: &TypingContext, +) -> Result { + check_arguments_at_least(3, args)?; + + let asset_owner = &args[0]; + let allowance_list = args[1].match_list().ok_or(CheckError::new( + CheckErrors::RestrictAssetsExpectedListOfAllowances, + ))?; + let body_exprs = &args[2..]; + + runtime_cost( + ClarityCostFunction::AnalysisListItemsCheck, + checker, + allowance_list.len() + body_exprs.len(), + )?; + + checker.type_check_expects(asset_owner, context, &TypeSignature::PrincipalType)?; + + // TODO: type-check the allowances + + // Check the body expressions, ensuring any intermediate responses are handled + let mut last_return = None; + for expr in body_exprs { + let type_return = checker.type_check(expr, context)?; + if type_return.is_response_type() { + return Err(CheckErrors::UncheckedIntermediaryResponses.into()); + } + last_return = Some(type_return); + } + + last_return.ok_or_else(|| CheckError::new(CheckErrors::CheckerImplementationFailure)) +} + impl TypedNativeFunction { pub fn type_check_application( &self, @@ -1209,6 +1245,7 @@ impl TypedNativeFunction { CheckErrors::Expects("FATAL: Legal Clarity response type marked invalid".into()) })?, ))), + RestrictAssets => Special(SpecialNativeFunction(&check_restrict_assets)), }; Ok(out) diff --git a/clarity/src/vm/costs/cost_functions.rs b/clarity/src/vm/costs/cost_functions.rs index 6abbaec555..ac9aa39c2b 100644 --- a/clarity/src/vm/costs/cost_functions.rs +++ b/clarity/src/vm/costs/cost_functions.rs @@ -159,6 +159,7 @@ define_named_enum!(ClarityCostFunction { BitwiseRShift("cost_bitwise_right_shift"), ContractHash("cost_contract_hash"), ToAscii("cost_to_ascii"), + RestrictAssets("cost_restrict_assets"), Unimplemented("cost_unimplemented"), }); @@ -330,6 +331,7 @@ pub trait CostValues { fn cost_bitwise_right_shift(n: u64) -> InterpreterResult; fn cost_contract_hash(n: u64) -> InterpreterResult; fn cost_to_ascii(n: u64) -> InterpreterResult; + fn cost_restrict_assets(n: u64) -> InterpreterResult; } impl ClarityCostFunction { @@ -484,6 +486,7 @@ impl ClarityCostFunction { ClarityCostFunction::BitwiseRShift => C::cost_bitwise_right_shift(n), ClarityCostFunction::ContractHash => C::cost_contract_hash(n), ClarityCostFunction::ToAscii => C::cost_to_ascii(n), + ClarityCostFunction::RestrictAssets => C::cost_restrict_assets(n), ClarityCostFunction::Unimplemented => Err(RuntimeErrorType::NotImplemented.into()), } } diff --git a/clarity/src/vm/costs/costs_1.rs b/clarity/src/vm/costs/costs_1.rs index 1e400f56bd..00f3d53ce1 100644 --- a/clarity/src/vm/costs/costs_1.rs +++ b/clarity/src/vm/costs/costs_1.rs @@ -13,9 +13,9 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -/// This file implements the cost functions from costs.clar in Rust. -use super::cost_functions::{linear, logn, nlogn, CostValues}; use super::ExecutionCost; +/// This file implements the cost functions from costs.clar in Rust. +use super::cost_functions::{CostValues, linear, logn, nlogn}; use crate::vm::errors::{InterpreterResult, RuntimeErrorType}; pub struct Costs1; @@ -753,4 +753,8 @@ impl CostValues for Costs1 { fn cost_to_ascii(n: u64) -> InterpreterResult { Err(RuntimeErrorType::NotImplemented.into()) } + + fn cost_restrict_assets(n: u64) -> InterpreterResult { + Err(RuntimeErrorType::NotImplemented.into()) + } } diff --git a/clarity/src/vm/costs/costs_2.rs b/clarity/src/vm/costs/costs_2.rs index 451008bd1b..56d1921acf 100644 --- a/clarity/src/vm/costs/costs_2.rs +++ b/clarity/src/vm/costs/costs_2.rs @@ -13,9 +13,9 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -/// This file implements the cost functions from costs-2.clar in Rust. -use super::cost_functions::{linear, logn, nlogn, CostValues}; use super::ExecutionCost; +/// This file implements the cost functions from costs-2.clar in Rust. +use super::cost_functions::{CostValues, linear, logn, nlogn}; use crate::vm::errors::{InterpreterResult, RuntimeErrorType}; pub struct Costs2; @@ -753,4 +753,8 @@ impl CostValues for Costs2 { fn cost_to_ascii(n: u64) -> InterpreterResult { Err(RuntimeErrorType::NotImplemented.into()) } + + fn cost_restrict_assets(n: u64) -> InterpreterResult { + Err(RuntimeErrorType::NotImplemented.into()) + } } diff --git a/clarity/src/vm/costs/costs_2_testnet.rs b/clarity/src/vm/costs/costs_2_testnet.rs index 647bafedb9..919a0c71c2 100644 --- a/clarity/src/vm/costs/costs_2_testnet.rs +++ b/clarity/src/vm/costs/costs_2_testnet.rs @@ -13,9 +13,9 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -/// This file implements the cost functions from costs-2-testnet.clar in Rust. -use super::cost_functions::{linear, logn, nlogn, CostValues}; use super::ExecutionCost; +/// This file implements the cost functions from costs-2-testnet.clar in Rust. +use super::cost_functions::{CostValues, linear, logn, nlogn}; use crate::vm::errors::{InterpreterResult, RuntimeErrorType}; pub struct Costs2Testnet; @@ -753,4 +753,8 @@ impl CostValues for Costs2Testnet { fn cost_to_ascii(n: u64) -> InterpreterResult { Err(RuntimeErrorType::NotImplemented.into()) } + + fn cost_restrict_assets(n: u64) -> InterpreterResult { + Err(RuntimeErrorType::NotImplemented.into()) + } } diff --git a/clarity/src/vm/costs/costs_3.rs b/clarity/src/vm/costs/costs_3.rs index b195303510..d9dfa0482d 100644 --- a/clarity/src/vm/costs/costs_3.rs +++ b/clarity/src/vm/costs/costs_3.rs @@ -13,9 +13,9 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -/// This file implements the cost functions from costs-3.clar in Rust. -use super::cost_functions::{linear, logn, nlogn, CostValues}; use super::ExecutionCost; +/// This file implements the cost functions from costs-3.clar in Rust. +use super::cost_functions::{CostValues, linear, logn, nlogn}; use crate::vm::errors::{InterpreterResult, RuntimeErrorType}; pub struct Costs3; @@ -771,4 +771,8 @@ impl CostValues for Costs3 { fn cost_to_ascii(n: u64) -> InterpreterResult { Err(RuntimeErrorType::NotImplemented.into()) } + + fn cost_restrict_assets(n: u64) -> InterpreterResult { + Err(RuntimeErrorType::NotImplemented.into()) + } } diff --git a/clarity/src/vm/costs/costs_4.rs b/clarity/src/vm/costs/costs_4.rs index d1c92732d9..52304f8a41 100644 --- a/clarity/src/vm/costs/costs_4.rs +++ b/clarity/src/vm/costs/costs_4.rs @@ -13,6 +13,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +use super::ExecutionCost; /// This file implements the cost functions from costs-4.clar in Rust. /// For Clarity 4, all cost functions are the same as in costs-3, except /// for the new `cost_contract_hash` function. To avoid duplication, this @@ -20,7 +21,6 @@ /// overrides only `cost_contract_hash`. use super::cost_functions::CostValues; use super::costs_3::Costs3; -use super::ExecutionCost; use crate::vm::costs::cost_functions::linear; use crate::vm::errors::InterpreterResult; @@ -446,7 +446,8 @@ impl CostValues for Costs4 { Costs3::cost_bitwise_right_shift(n) } - // New in costs-4 + // --- New in costs-4 --- + fn cost_contract_hash(_n: u64) -> InterpreterResult { Ok(ExecutionCost { runtime: 100, // TODO: needs criterion benchmark @@ -461,4 +462,9 @@ impl CostValues for Costs4 { // TODO: needs criterion benchmark Ok(ExecutionCost::runtime(linear(n, 1, 100))) } + + fn cost_restrict_assets(n: u64) -> InterpreterResult { + // TODO: needs criterion benchmark + Ok(ExecutionCost::runtime(linear(n, 1, 100))) + } } diff --git a/clarity/src/vm/docs/mod.rs b/clarity/src/vm/docs/mod.rs index df84474a39..88f551c215 100644 --- a/clarity/src/vm/docs/mod.rs +++ b/clarity/src/vm/docs/mod.rs @@ -15,13 +15,13 @@ // along with this program. If not, see . use super::types::signatures::{FunctionArgSignature, FunctionReturnsSignature}; -use crate::vm::analysis::type_checker::v2_1::natives::SimpleNativeFunction; +use crate::vm::ClarityVersion; use crate::vm::analysis::type_checker::v2_1::TypedNativeFunction; -use crate::vm::functions::define::DefineFunctions; +use crate::vm::analysis::type_checker::v2_1::natives::SimpleNativeFunction; use crate::vm::functions::NativeFunctions; +use crate::vm::functions::define::DefineFunctions; use crate::vm::types::{FixedFunction, FunctionType}; use crate::vm::variables::NativeVariables; -use crate::vm::ClarityVersion; #[cfg(feature = "rusqlite")] pub mod contracts; @@ -102,8 +102,7 @@ const BLOCK_HEIGHT: SimpleKeywordAPI = SimpleKeywordAPI { description: "Returns the current block height of the Stacks blockchain in Clarity 1 and 2. Upon activation of epoch 3.0, `block-height` will return the same value as `tenure-height`. In Clarity 3, `block-height` is removed and has been replaced with `stacks-block-height`.", - example: - "(> block-height u1000) ;; returns true if the current block-height has passed 1000 blocks.", + example: "(> block-height u1000) ;; returns true if the current block-height has passed 1000 blocks.", }; const BURN_BLOCK_HEIGHT: SimpleKeywordAPI = SimpleKeywordAPI { @@ -139,8 +138,7 @@ const STACKS_BLOCK_HEIGHT_KEYWORD: SimpleKeywordAPI = SimpleKeywordAPI { snippet: "stacks-block-height", output_type: "uint", description: "Returns the current block height of the Stacks blockchain.", - example: - "(<= stacks-block-height u500000) ;; returns true if the current block-height has not passed 500,000 blocks.", + example: "(<= stacks-block-height u500000) ;; returns true if the current block-height has not passed 500,000 blocks.", }; const TENURE_HEIGHT_KEYWORD: SimpleKeywordAPI = SimpleKeywordAPI { @@ -193,8 +191,7 @@ const REGTEST_KEYWORD: SimpleKeywordAPI = SimpleKeywordAPI { snippet: "is-in-regtest", output_type: "bool", description: "Returns whether or not the code is running in a regression test", - example: - "(print is-in-regtest) ;; Will print 'true' if the code is running in a regression test", + example: "(print is-in-regtest) ;; Will print 'true' if the code is running in a regression test", }; const MAINNET_KEYWORD: SimpleKeywordAPI = SimpleKeywordAPI { @@ -255,7 +252,7 @@ const TO_UINT_API: SimpleFunctionAPI = SimpleFunctionAPI { snippet: "to-uint ${1:int}", signature: "(to-uint i)", description: "Tries to convert the `int` argument to a `uint`. Will cause a runtime error and abort if the supplied argument is negative.", - example: "(to-uint 238) ;; Returns u238" + example: "(to-uint 238) ;; Returns u238", }; const TO_INT_API: SimpleFunctionAPI = SimpleFunctionAPI { @@ -263,7 +260,7 @@ const TO_INT_API: SimpleFunctionAPI = SimpleFunctionAPI { snippet: "to-int ${1:uint}", signature: "(to-int u)", description: "Tries to convert the `uint` argument to an `int`. Will cause a runtime error and abort if the supplied argument is >= `pow(2, 127)`", - example: "(to-int u238) ;; Returns 238" + example: "(to-int u238) ;; Returns 238", }; const BUFF_TO_INT_LE_API: SimpleFunctionAPI = SimpleFunctionAPI { @@ -464,7 +461,7 @@ const ADD_API: SimpleFunctionAPI = SimpleFunctionAPI { snippet: "+ ${1:expr-1} ${2:expr-2}", signature: "(+ i1 i2...)", description: "Adds a variable number of integer inputs and returns the result. In the event of an _overflow_, throws a runtime error.", - example: "(+ 1 2 3) ;; Returns 6" + example: "(+ 1 2 3) ;; Returns 6", }; const SUB_API: SimpleFunctionAPI = SimpleFunctionAPI { @@ -474,7 +471,7 @@ const SUB_API: SimpleFunctionAPI = SimpleFunctionAPI { description: "Subtracts a variable number of integer inputs and returns the result. In the event of an _underflow_, throws a runtime error.", example: "(- 2 1 1) ;; Returns 0 (- 0 3) ;; Returns -3 -" +", }; const DIV_API: SimpleFunctionAPI = SimpleFunctionAPI { @@ -485,7 +482,7 @@ const DIV_API: SimpleFunctionAPI = SimpleFunctionAPI { example: "(/ 2 3) ;; Returns 0 (/ 5 2) ;; Returns 2 (/ 4 2 2) ;; Returns 1 -" +", }; const MUL_API: SimpleFunctionAPI = SimpleFunctionAPI { @@ -496,7 +493,7 @@ const MUL_API: SimpleFunctionAPI = SimpleFunctionAPI { example: "(* 2 3) ;; Returns 6 (* 5 2) ;; Returns 10 (* 2 2 2) ;; Returns 8 -" +", }; const MOD_API: SimpleFunctionAPI = SimpleFunctionAPI { @@ -507,7 +504,7 @@ const MOD_API: SimpleFunctionAPI = SimpleFunctionAPI { example: "(mod 2 3) ;; Returns 2 (mod 5 2) ;; Returns 1 (mod 7 1) ;; Returns 0 -" +", }; const POW_API: SimpleFunctionAPI = SimpleFunctionAPI { @@ -569,8 +566,7 @@ const BITWISE_XOR_API: SimpleFunctionAPI = SimpleFunctionAPI { name: None, snippet: "bit-xor ${1:expr-1} ${2:expr-2}", signature: "(bit-xor i1 i2...)", - description: - "Returns the result of bitwise exclusive or'ing a variable number of integer inputs.", + description: "Returns the result of bitwise exclusive or'ing a variable number of integer inputs.", example: "(bit-xor 1 2) ;; Returns 3 (bit-xor 120 280) ;; Returns 352 (bit-xor -128 64) ;; Returns -64 @@ -596,8 +592,7 @@ const BITWISE_OR_API: SimpleFunctionAPI = SimpleFunctionAPI { name: None, snippet: "bit-or ${1:expr-1} ${2:expr-2}", signature: "(bit-or i1 i2...)", - description: - "Returns the result of bitwise inclusive or'ing a variable number of integer inputs.", + description: "Returns the result of bitwise inclusive or'ing a variable number of integer inputs.", example: "(bit-or 4 8) ;; Returns 12 (bit-or 1 2 4) ;; Returns 7 (bit-or 64 -32 -16) ;; Returns -16 @@ -833,7 +828,9 @@ pub fn get_output_type_string(function_type: &FunctionType) -> String { let arg_sig = match pos { 0 => left, 1 => right, - _ => panic!("Index out of range: TypeOfArgAtPosition for FunctionType::Binary can only handle two arguments, zero-indexed (0 or 1).") + _ => panic!( + "Index out of range: TypeOfArgAtPosition for FunctionType::Binary can only handle two arguments, zero-indexed (0 or 1)." + ), }; match arg_sig { @@ -1360,7 +1357,7 @@ const KECCAK256_API: SpecialAPI = SpecialAPI { Note: this differs from the `NIST SHA-3` (that is, FIPS 202) standard. If an integer (128 bit) is supplied the hash is computed over the little-endian representation of the integer.", - example: "(keccak256 0) ;; Returns 0xf490de2920c8a35fabeb13208852aa28c76f9be9b03a4dd2b3c075f7a26923b4" + example: "(keccak256 0) ;; Returns 0xf490de2920c8a35fabeb13208852aa28c76f9be9b03a4dd2b3c075f7a26923b4", }; const SECP256K1RECOVER_API: SpecialAPI = SpecialAPI { @@ -1433,7 +1430,8 @@ const PRINCIPAL_OF_API: SpecialAPI = SpecialAPI { snippet: "principal-of? ${1:public-key}", output_type: "(response principal uint)", signature: "(principal-of? public-key)", - description: "The `principal-of?` function returns the principal derived from the provided public key. + description: + "The `principal-of?` function returns the principal derived from the provided public key. This function may fail with the error code: * `(err u1)` -- `public-key` is invalid @@ -1444,7 +1442,7 @@ with Stacks 2.1, this bug is fixed, so that this function will return a principa the network it is called on. In particular, if this is called on the mainnet, it will return a single-signature mainnet principal. ", - example: "(principal-of? 0x03adb8de4bfb65db2cfd6120d55c6526ae9c52e675db7e47308636534ba7786110) ;; Returns (ok ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP)" + example: "(principal-of? 0x03adb8de4bfb65db2cfd6120d55c6526ae9c52e675db7e47308636534ba7786110) ;; Returns (ok ST1AW6EKPGT61SQ9FNVDS17RKNWT8ZP582VF9HSCP)", }; const AT_BLOCK: SpecialAPI = SpecialAPI { @@ -1580,8 +1578,7 @@ If the supplied argument is an `(ok ...)` value, }; const MATCH_API: SpecialAPI = SpecialAPI { - input_type: - "(optional A) name expression expression | (response A B) name expression name expression", + input_type: "(optional A) name expression expression | (response A B) name expression name expression", snippet: "match ${1:algebraic-expr} ${2:some-binding-name} ${3:some-branch} ${4:none-branch}", output_type: "C", signature: "(match opt-input some-binding-name some-branch none-branch) | @@ -2566,6 +2563,35 @@ characters.", "#, }; +const RESTRICT_ASSETS: SpecialAPI = SpecialAPI { + input_type: "principal, ((Allowance)*), AnyType, ... A", + snippet: "restrict-assets? ${1:asset-owner} (${2:allowance-1} ${3:allowance-2}) ${4:expr-1}", + output_type: "(response A int)", + signature: "(restrict-assets? asset-owner ((with-stx|with-ft|with-nft|with-stacking)*) expr-body1 expr-body2 ... expr-body-last)", + description: "Executes the body expressions, then checks the asset +outflows against the granted allowances, in declaration order. If any +allowance is violated, the body expressions are reverted, an error is +returned, and an event is emitted with the full details of the violation to +help with debugging. Note that the `asset-owner` and allowance setup +expressions are evaluated before executing the body expressions. The final +body expression cannot return a `response` value in order to avoid returning +a nested `response` value from `restrict-assets?` (nested responses are +error-prone). Returns: +* `(ok x)` if the outflows are within the allowances, where `x` is the + result of the final body expression and has type `A`. +* `(err index)` if an allowance was violated, where `index` is the 0-based + index of the first violated allowance in the list of granted allowances, + or -1 if an asset with no allowance caused the violation.", + example: r#" +(restrict-assets? tx-sender () + (try! (stx-transfer? u1000000 tx-sender 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM)) +) ;; Returns (err -1) +(restrict-assets? tx-sender () + (+ u1 u2) +) ;; Returns (ok u3) +"#, +}; + pub fn make_api_reference(function: &NativeFunctions) -> FunctionAPI { use crate::vm::functions::NativeFunctions::*; let name = function.get_name(); @@ -2680,6 +2706,7 @@ pub fn make_api_reference(function: &NativeFunctions) -> FunctionAPI { BitwiseRShift => make_for_simple_native(&BITWISE_RIGHT_SHIFT_API, function, name), ContractHash => make_for_simple_native(&CONTRACT_HASH, function, name), ToAscii => make_for_special(&TO_ASCII, function), + RestrictAssets => make_for_special(&RESTRICT_ASSETS, function), } } @@ -2791,11 +2818,11 @@ pub fn make_json_api_reference() -> String { #[cfg(test)] mod test { use stacks_common::consts::{CHAIN_ID_TESTNET, PEER_VERSION_EPOCH_2_1}; + use stacks_common::types::StacksEpochId; use stacks_common::types::chainstate::{ BlockHeaderHash, BurnchainHeaderHash, ConsensusHash, SortitionId, StacksAddress, StacksBlockId, VRFSeed, }; - use stacks_common::types::StacksEpochId; use stacks_common::util::hash::hex_bytes; use super::{get_input_type_string, make_all_api_reference, make_json_api_reference}; @@ -2806,13 +2833,13 @@ mod test { BurnStateDB, ClarityDatabase, HeadersDB, MemoryBackingStore, STXBalance, }; use crate::vm::docs::get_output_type_string; - use crate::vm::types::signatures::{FunctionArgSignature, FunctionReturnsSignature, ASCII_40}; + use crate::vm::types::signatures::{ASCII_40, FunctionArgSignature, FunctionReturnsSignature}; use crate::vm::types::{ FunctionType, PrincipalData, QualifiedContractIdentifier, TupleData, TypeSignature, }; use crate::vm::{ - ast, eval_all, execute, ClarityVersion, ContractContext, GlobalContext, LimitedCostTracker, - StacksEpoch, Value, + ClarityVersion, ContractContext, GlobalContext, LimitedCostTracker, StacksEpoch, Value, + ast, eval_all, execute, }; struct DocHeadersDB {} @@ -3362,7 +3389,10 @@ mod test { ret, ); result = get_input_type_string(&function_type); - assert_eq!(result, "uint, uint | uint, int | uint, principal | principal, uint | principal, int | principal, principal | int, uint | int, int | int, principal"); + assert_eq!( + result, + "uint, uint | uint, int | uint, principal | principal, uint | principal, int | principal, principal | int, uint | int, int | int, principal" + ); } #[test] diff --git a/clarity/src/vm/functions/mod.rs b/clarity/src/vm/functions/mod.rs index f458c282e6..b587ffbb73 100644 --- a/clarity/src/vm/functions/mod.rs +++ b/clarity/src/vm/functions/mod.rs @@ -16,18 +16,18 @@ use stacks_common::types::StacksEpochId; -use crate::vm::callables::{cost_input_sized_vararg, CallableType, NativeHandle}; +use crate::vm::Value::CallableContract; +use crate::vm::callables::{CallableType, NativeHandle, cost_input_sized_vararg}; use crate::vm::costs::cost_functions::ClarityCostFunction; -use crate::vm::costs::{constants as cost_constants, runtime_cost, CostTracker, MemoryConsumer}; +use crate::vm::costs::{CostTracker, MemoryConsumer, constants as cost_constants, runtime_cost}; use crate::vm::errors::{ - check_argument_count, check_arguments_at_least, CheckErrors, Error, - InterpreterResult as Result, ShortReturnType, SyntaxBindingError, SyntaxBindingErrorType, + CheckErrors, Error, InterpreterResult as Result, ShortReturnType, SyntaxBindingError, + SyntaxBindingErrorType, check_argument_count, check_arguments_at_least, }; pub use crate::vm::functions::assets::stx_transfer_consolidated; use crate::vm::representations::{ClarityName, SymbolicExpression, SymbolicExpressionType}; use crate::vm::types::{PrincipalData, TypeSignature, Value}; -use crate::vm::Value::CallableContract; -use crate::vm::{eval, is_reserved, Environment, LocalContext}; +use crate::vm::{Environment, LocalContext, eval, is_reserved}; macro_rules! switch_on_global_epoch { ($Name:ident ($Epoch2Version:ident, $Epoch205Version:ident)) => { @@ -76,6 +76,7 @@ mod crypto; mod database; pub mod define; mod options; +mod post_conditions; pub mod principals; mod sequences; pub mod tuples; @@ -193,6 +194,7 @@ define_versioned_named_enum_with_max!(NativeFunctions(ClarityVersion) { GetTenureInfo("get-tenure-info?", ClarityVersion::Clarity3, None), ContractHash("contract-hash?", ClarityVersion::Clarity4, None), ToAscii("to-ascii?", ClarityVersion::Clarity4, None), + RestrictAssets("restrict-assets?", ClarityVersion::Clarity4, None) }); /// @@ -565,6 +567,10 @@ pub fn lookup_reserved_functions(name: &str, version: &ClarityVersion) -> Option SpecialFunction("special_contract_hash", &database::special_contract_hash) } ToAscii => SpecialFunction("special_to_ascii", &conversions::special_to_ascii), + RestrictAssets => SpecialFunction( + "special_restrict_assets", + &post_conditions::special_restrict_assets, + ), }; Some(callable) } else { diff --git a/clarity/src/vm/functions/post_conditions.rs b/clarity/src/vm/functions/post_conditions.rs new file mode 100644 index 0000000000..3c52a6a307 --- /dev/null +++ b/clarity/src/vm/functions/post_conditions.rs @@ -0,0 +1,110 @@ +// Copyright (C) 2025 Stacks Open Internet Foundation +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use crate::vm::costs::cost_functions::ClarityCostFunction; +use crate::vm::costs::runtime_cost; +use crate::vm::errors::{ + check_arguments_at_least, CheckErrors, InterpreterError, InterpreterResult, +}; +use crate::vm::representations::SymbolicExpression; +use crate::vm::types::{QualifiedContractIdentifier, Value}; +use crate::vm::{eval, Environment, LocalContext}; + +struct StxAllowance { + amount: u128, +} + +struct FtAllowance { + contract: QualifiedContractIdentifier, + token: String, + amount: u128, +} + +struct NftAllowance { + contract: QualifiedContractIdentifier, + token: String, + asset_id: Value, +} + +struct StackingAllowance { + amount: u128, +} + +enum Allowance { + Stx(StxAllowance), + Ft(FtAllowance), + Nft(NftAllowance), + Stacking(StackingAllowance), + All, +} + +fn eval_allowance( + _allowance_expr: &SymbolicExpression, + _env: &mut Environment, + _context: &LocalContext, +) -> InterpreterResult { + // FIXME: Placeholder + Ok(Allowance::All) +} + +/// Handles the function `restrict-assets?` +pub fn special_restrict_assets( + args: &[SymbolicExpression], + env: &mut Environment, + context: &LocalContext, +) -> InterpreterResult { + // (restrict-assets? asset-owner ((with-stx|with-ft|with-nft|with-stacking)*) expr-body1 expr-body2 ... expr-body-last) + // arg1 => asset owner to protect + // arg2 => list of asset allowances + // arg3..n => body + check_arguments_at_least(3, args)?; + + let asset_owner_expr = &args[0]; + let allowance_list = args[1] + .match_list() + .ok_or(CheckErrors::RestrictAssetsExpectedListOfAllowances)?; + let body_exprs = &args[2..]; + + let _asset_owner = eval(asset_owner_expr, env, context)?; + + runtime_cost( + ClarityCostFunction::RestrictAssets, + env, + allowance_list.len(), + )?; + + let mut allowances = Vec::with_capacity(allowance_list.len()); + for allowance in allowance_list { + allowances.push(eval_allowance(allowance, env, context)?); + } + + // Create a new evaluation context, so that we can rollback if the + // post-conditions are violated + env.global_context.begin(); + + // evaluate the body expressions + let mut last_result = None; + for expr in body_exprs { + let result = eval(expr, env, context)?; + last_result.replace(result); + } + + // TODO: Check the post-conditions and rollback if they are violated + + env.global_context.commit()?; + + // last_result should always be Some(...), because of the arg len check above. + last_result.ok_or_else(|| InterpreterError::Expect("Failed to get let result".into()).into()) +} diff --git a/stackslib/src/chainstate/stacks/boot/costs-4.clar b/stackslib/src/chainstate/stacks/boot/costs-4.clar index 715bee4966..a1654273f7 100644 --- a/stackslib/src/chainstate/stacks/boot/costs-4.clar +++ b/stackslib/src/chainstate/stacks/boot/costs-4.clar @@ -663,3 +663,6 @@ (define-read-only (cost_to_ascii (n uint)) (runtime (linear n u1 u100))) ;; TODO: needs criterion benchmark + +(define-read-only (cost_restrict_assets (n uint)) + (runtime (linear n u1 u100))) ;; TODO: needs criterion benchmark diff --git a/stackslib/src/clarity_vm/tests/costs.rs b/stackslib/src/clarity_vm/tests/costs.rs index 75be448599..c4bced3342 100644 --- a/stackslib/src/clarity_vm/tests/costs.rs +++ b/stackslib/src/clarity_vm/tests/costs.rs @@ -165,6 +165,7 @@ pub fn get_simple_test(function: &NativeFunctions) -> &'static str { GetTenureInfo => "(get-tenure-info? time u1)", ContractHash => "(contract-hash? .contract-other)", ToAscii => "(to-ascii? 65)", + RestrictAssets => "(restrict-assets? tx-sender () (+ u1 u2))", } } From 9672363fa641d17e71205b5317b1de6d3dc09985 Mon Sep 17 00:00:00 2001 From: Brice Dobry Date: Thu, 18 Sep 2025 15:50:14 -0400 Subject: [PATCH 02/35] feat: add syntax and type-checking for `as-contract?` and allowances --- clarity-types/src/errors/analysis.rs | 10 +- clarity-types/src/types/signatures.rs | 3 + .../src/vm/analysis/arithmetic_checker/mod.rs | 30 +- .../src/vm/analysis/read_only_checker/mod.rs | 137 ++- .../type_checker/v2_05/natives/mod.rs | 52 +- .../analysis/type_checker/v2_1/natives/mod.rs | 65 +- .../v2_1/natives/post_conditions.rs | 254 ++++++ .../analysis/type_checker/v2_1/tests/mod.rs | 1 + .../v2_1/tests/post_conditions.rs | 817 ++++++++++++++++++ clarity/src/vm/costs/cost_functions.rs | 3 + clarity/src/vm/costs/costs_1.rs | 8 +- clarity/src/vm/costs/costs_2.rs | 8 +- clarity/src/vm/costs/costs_2_testnet.rs | 8 +- clarity/src/vm/costs/costs_3.rs | 8 +- clarity/src/vm/costs/costs_4.rs | 7 +- clarity/src/vm/docs/mod.rs | 193 ++++- clarity/src/vm/functions/mod.rs | 32 +- clarity/src/vm/functions/post_conditions.rs | 66 +- .../chainstate/stacks/boot/contract_tests.rs | 2 +- .../src/chainstate/stacks/boot/costs-4.clar | 3 + .../src/clarity_vm/tests/analysis_costs.rs | 27 +- stackslib/src/clarity_vm/tests/costs.rs | 55 +- 22 files changed, 1644 insertions(+), 145 deletions(-) create mode 100644 clarity/src/vm/analysis/type_checker/v2_1/natives/post_conditions.rs create mode 100644 clarity/src/vm/analysis/type_checker/v2_1/tests/post_conditions.rs diff --git a/clarity-types/src/errors/analysis.rs b/clarity-types/src/errors/analysis.rs index f4c1a3d7bf..698181a65d 100644 --- a/clarity-types/src/errors/analysis.rs +++ b/clarity-types/src/errors/analysis.rs @@ -309,7 +309,10 @@ pub enum CheckErrors { ExecutionTimeExpired, // contract post-conditions - RestrictAssetsExpectedListOfAllowances, + ExpectedListOfAllowances(String, i32), + AllowanceExprNotAllowed, + ExpectedAllowanceExpr(String), + WithAllAllowanceNotAllowed, } #[derive(Debug, PartialEq)] @@ -608,7 +611,10 @@ impl DiagnosableError for CheckErrors { CheckErrors::CostComputationFailed(s) => format!("contract cost computation failed: {s}"), CheckErrors::CouldNotDetermineSerializationType => "could not determine the input type for the serialization function".into(), CheckErrors::ExecutionTimeExpired => "execution time expired".into(), - CheckErrors::RestrictAssetsExpectedListOfAllowances => "restrict-assets? expects a list of asset allowances as its second argument".into(), + CheckErrors::ExpectedListOfAllowances(fn_name, arg_num) => format!("{fn_name} expects a list of asset allowances as argument {arg_num}"), + CheckErrors::AllowanceExprNotAllowed => "allowance expressions are only allowed in the context of a `restrict-assets?` or `as-contract?`".into(), + CheckErrors::ExpectedAllowanceExpr(got_name) => format!("expected an allowance expression, got: {got_name}"), + CheckErrors::WithAllAllowanceNotAllowed => "with-all-assets-unsafe is not allowed here, only in the allowance list for `as-contract?`".into(), } } diff --git a/clarity-types/src/types/signatures.rs b/clarity-types/src/types/signatures.rs index a8e49b92f9..d07492f5cd 100644 --- a/clarity-types/src/types/signatures.rs +++ b/clarity-types/src/types/signatures.rs @@ -261,6 +261,9 @@ lazy_static! { pub const ASCII_40: TypeSignature = SequenceType(SequenceSubtype::StringType( StringSubtype::ASCII(BufferLength(40)), )); +pub const ASCII_128: TypeSignature = SequenceType(SequenceSubtype::StringType( + StringSubtype::ASCII(BufferLength(128)), +)); pub const UTF8_40: TypeSignature = SequenceType(SequenceSubtype::StringType(StringSubtype::UTF8( StringUTF8Length(40), ))); diff --git a/clarity/src/vm/analysis/arithmetic_checker/mod.rs b/clarity/src/vm/analysis/arithmetic_checker/mod.rs index 95b459c227..65815a7a37 100644 --- a/clarity/src/vm/analysis/arithmetic_checker/mod.rs +++ b/clarity/src/vm/analysis/arithmetic_checker/mod.rs @@ -173,11 +173,31 @@ impl ArithmeticOnlyChecker<'_> { | ContractCall | StxTransfer | StxTransferMemo | StxBurn | AtBlock | GetStxBalance | GetTokenSupply | BurnToken | FromConsensusBuff | ToConsensusBuff | BurnAsset | StxGetAccount => Err(Error::FunctionNotPermitted(function)), - Append | Concat | AsMaxLen | ContractOf | PrincipalOf | ListCons | Print - | AsContract | ElementAt | ElementAtAlias | IndexOf | IndexOfAlias | Map | Filter - | Fold | Slice | ReplaceAt | ContractHash | RestrictAssets => { - Err(Error::FunctionNotPermitted(function)) - } + Append + | Concat + | AsMaxLen + | ContractOf + | PrincipalOf + | ListCons + | Print + | AsContract + | ElementAt + | ElementAtAlias + | IndexOf + | IndexOfAlias + | Map + | Filter + | Fold + | Slice + | ReplaceAt + | ContractHash + | RestrictAssets + | AsContractSafe + | AllowanceWithStx + | AllowanceWithFt + | AllowanceWithNft + | AllowanceWithStacking + | AllowanceAll => Err(Error::FunctionNotPermitted(function)), BuffToIntLe | BuffToUIntLe | BuffToIntBe | BuffToUIntBe => { Err(Error::FunctionNotPermitted(function)) } diff --git a/clarity/src/vm/analysis/read_only_checker/mod.rs b/clarity/src/vm/analysis/read_only_checker/mod.rs index 5eeb1b014f..80700cad66 100644 --- a/clarity/src/vm/analysis/read_only_checker/mod.rs +++ b/clarity/src/vm/analysis/read_only_checker/mod.rs @@ -282,20 +282,101 @@ impl<'a, 'b> ReadOnlyChecker<'a, 'b> { use crate::vm::functions::NativeFunctions::*; match function { - Add | Subtract | Divide | Multiply | CmpGeq | CmpLeq | CmpLess | CmpGreater - | Modulo | Power | Sqrti | Log2 | BitwiseXor | And | Or | Not | Hash160 | Sha256 - | Keccak256 | Equals | If | Sha512 | Sha512Trunc256 | Secp256k1Recover - | Secp256k1Verify | ConsSome | ConsOkay | ConsError | DefaultTo | UnwrapRet - | UnwrapErrRet | IsOkay | IsNone | Asserts | Unwrap | UnwrapErr | Match | IsErr - | IsSome | TryRet | ToUInt | ToInt | BuffToIntLe | BuffToUIntLe | BuffToIntBe - | BuffToUIntBe | IntToAscii | IntToUtf8 | StringToInt | StringToUInt | IsStandard - | ToConsensusBuff | PrincipalDestruct | PrincipalConstruct | Append | Concat - | AsMaxLen | ContractOf | PrincipalOf | ListCons | GetBlockInfo | GetBurnBlockInfo - | GetStacksBlockInfo | GetTenureInfo | TupleGet | TupleMerge | Len | Print - | AsContract | Begin | FetchVar | GetStxBalance | StxGetAccount | GetTokenBalance - | GetAssetOwner | GetTokenSupply | ElementAt | IndexOf | Slice | ReplaceAt - | BitwiseAnd | BitwiseOr | BitwiseNot | BitwiseLShift | BitwiseRShift | BitwiseXor2 - | ElementAtAlias | IndexOfAlias | ContractHash | ToAscii => { + Add + | Subtract + | Divide + | Multiply + | CmpGeq + | CmpLeq + | CmpLess + | CmpGreater + | Modulo + | Power + | Sqrti + | Log2 + | BitwiseXor + | And + | Or + | Not + | Hash160 + | Sha256 + | Keccak256 + | Equals + | If + | Sha512 + | Sha512Trunc256 + | Secp256k1Recover + | Secp256k1Verify + | ConsSome + | ConsOkay + | ConsError + | DefaultTo + | UnwrapRet + | UnwrapErrRet + | IsOkay + | IsNone + | Asserts + | Unwrap + | UnwrapErr + | Match + | IsErr + | IsSome + | TryRet + | ToUInt + | ToInt + | BuffToIntLe + | BuffToUIntLe + | BuffToIntBe + | BuffToUIntBe + | IntToAscii + | IntToUtf8 + | StringToInt + | StringToUInt + | IsStandard + | ToConsensusBuff + | PrincipalDestruct + | PrincipalConstruct + | Append + | Concat + | AsMaxLen + | ContractOf + | PrincipalOf + | ListCons + | GetBlockInfo + | GetBurnBlockInfo + | GetStacksBlockInfo + | GetTenureInfo + | TupleGet + | TupleMerge + | Len + | Print + | AsContract + | Begin + | FetchVar + | GetStxBalance + | StxGetAccount + | GetTokenBalance + | GetAssetOwner + | GetTokenSupply + | ElementAt + | IndexOf + | Slice + | ReplaceAt + | BitwiseAnd + | BitwiseOr + | BitwiseNot + | BitwiseLShift + | BitwiseRShift + | BitwiseXor2 + | ElementAtAlias + | IndexOfAlias + | ContractHash + | ToAscii + | AllowanceWithStx + | AllowanceWithFt + | AllowanceWithNft + | AllowanceWithStacking + | AllowanceAll => { // Check all arguments. self.check_each_expression_is_read_only(args) } @@ -434,9 +515,13 @@ impl<'a, 'b> ReadOnlyChecker<'a, 'b> { let asset_owner_read_only = self.check_read_only(&args[0])?; // Check the allowances argument. - let allowances = args[1] - .match_list() - .ok_or(CheckErrors::RestrictAssetsExpectedListOfAllowances)?; + let allowances = + args[1] + .match_list() + .ok_or(CheckErrors::ExpectedListOfAllowances( + "restrict-assets?".into(), + 2, + ))?; let allowances_read_only = self.check_each_expression_is_read_only(allowances)?; // Check the body expressions. @@ -444,6 +529,24 @@ impl<'a, 'b> ReadOnlyChecker<'a, 'b> { Ok(asset_owner_read_only && allowances_read_only && body_read_only) } + AsContractSafe => { + check_arguments_at_least(2, args)?; + + // Check the allowances argument. + let allowances = + args[0] + .match_list() + .ok_or(CheckErrors::ExpectedListOfAllowances( + "as-contract?".into(), + 1, + ))?; + let allowances_read_only = self.check_each_expression_is_read_only(allowances)?; + + // Check the body expressions. + let body_read_only = self.check_each_expression_is_read_only(&args[1..])?; + + Ok(allowances_read_only && body_read_only) + } } } diff --git a/clarity/src/vm/analysis/type_checker/v2_05/natives/mod.rs b/clarity/src/vm/analysis/type_checker/v2_05/natives/mod.rs index 99bb0583c8..459fb8a5bc 100644 --- a/clarity/src/vm/analysis/type_checker/v2_05/natives/mod.rs +++ b/clarity/src/vm/analysis/type_checker/v2_05/natives/mod.rs @@ -16,15 +16,15 @@ use stacks_common::types::StacksEpochId; -use super::{TypeChecker, TypingContext, check_argument_count, check_arguments_at_least, no_type}; +use super::{check_argument_count, check_arguments_at_least, no_type, TypeChecker, TypingContext}; use crate::vm::analysis::errors::{CheckError, CheckErrors, SyntaxBindingErrorType}; use crate::vm::costs::cost_functions::ClarityCostFunction; use crate::vm::costs::{analysis_typecheck_cost, runtime_cost}; use crate::vm::diagnostic::DiagnosableError; -use crate::vm::functions::{NativeFunctions, handle_binding_list}; +use crate::vm::functions::{handle_binding_list, NativeFunctions}; use crate::vm::types::{ - BUFF_20, BUFF_32, BUFF_33, BUFF_64, BUFF_65, BlockInfoProperty, FixedFunction, FunctionArg, - FunctionSignature, FunctionType, PrincipalData, TupleTypeSignature, TypeSignature, Value, + BlockInfoProperty, FixedFunction, FunctionArg, FunctionSignature, FunctionType, PrincipalData, + TupleTypeSignature, TypeSignature, Value, BUFF_20, BUFF_32, BUFF_33, BUFF_64, BUFF_65, }; use crate::vm::{ClarityName, ClarityVersion, SymbolicExpression, SymbolicExpressionType}; @@ -777,13 +777,43 @@ impl TypedNativeFunction { IsNone => Special(SpecialNativeFunction(&options::check_special_is_optional)), IsSome => Special(SpecialNativeFunction(&options::check_special_is_optional)), AtBlock => Special(SpecialNativeFunction(&check_special_at_block)), - ElementAtAlias | IndexOfAlias | BuffToIntLe | BuffToUIntLe | BuffToIntBe - | BuffToUIntBe | IsStandard | PrincipalDestruct | PrincipalConstruct | StringToInt - | StringToUInt | IntToAscii | IntToUtf8 | GetBurnBlockInfo | StxTransferMemo - | StxGetAccount | BitwiseAnd | BitwiseOr | BitwiseNot | BitwiseLShift - | BitwiseRShift | BitwiseXor2 | Slice | ToConsensusBuff | FromConsensusBuff - | ReplaceAt | GetStacksBlockInfo | GetTenureInfo | ContractHash | ToAscii - | RestrictAssets => { + ElementAtAlias + | IndexOfAlias + | BuffToIntLe + | BuffToUIntLe + | BuffToIntBe + | BuffToUIntBe + | IsStandard + | PrincipalDestruct + | PrincipalConstruct + | StringToInt + | StringToUInt + | IntToAscii + | IntToUtf8 + | GetBurnBlockInfo + | StxTransferMemo + | StxGetAccount + | BitwiseAnd + | BitwiseOr + | BitwiseNot + | BitwiseLShift + | BitwiseRShift + | BitwiseXor2 + | Slice + | ToConsensusBuff + | FromConsensusBuff + | ReplaceAt + | GetStacksBlockInfo + | GetTenureInfo + | ContractHash + | ToAscii + | RestrictAssets + | AsContractSafe + | AllowanceWithStx + | AllowanceWithFt + | AllowanceWithNft + | AllowanceWithStacking + | AllowanceAll => { return Err(CheckErrors::Expects( "Clarity 2+ keywords should not show up in 2.05".into(), )); diff --git a/clarity/src/vm/analysis/type_checker/v2_1/natives/mod.rs b/clarity/src/vm/analysis/type_checker/v2_1/natives/mod.rs index b7ed52dde1..ee016f2dbf 100644 --- a/clarity/src/vm/analysis/type_checker/v2_1/natives/mod.rs +++ b/clarity/src/vm/analysis/type_checker/v2_1/natives/mod.rs @@ -17,23 +17,23 @@ use stacks_common::types::StacksEpochId; use super::{ - TypeChecker, TypingContext, check_argument_count, check_arguments_at_least, - check_arguments_at_most, compute_typecheck_cost, no_type, + check_argument_count, check_arguments_at_least, check_arguments_at_most, + compute_typecheck_cost, no_type, TypeChecker, TypingContext, }; use crate::vm::analysis::errors::{CheckError, CheckErrors, SyntaxBindingErrorType}; use crate::vm::costs::cost_functions::ClarityCostFunction; -use crate::vm::costs::{CostErrors, CostTracker, analysis_typecheck_cost, runtime_cost}; +use crate::vm::costs::{analysis_typecheck_cost, runtime_cost, CostErrors, CostTracker}; use crate::vm::diagnostic::DiagnosableError; -use crate::vm::functions::{NativeFunctions, handle_binding_list}; +use crate::vm::functions::{handle_binding_list, NativeFunctions}; use crate::vm::types::signatures::{ - ASCII_40, CallableSubtype, FunctionArgSignature, FunctionReturnsSignature, SequenceSubtype, + CallableSubtype, FunctionArgSignature, FunctionReturnsSignature, SequenceSubtype, ASCII_40, TO_ASCII_MAX_BUFF, TO_ASCII_RESPONSE_STRING, UTF8_40, }; use crate::vm::types::{ - BUFF_1, BUFF_20, BUFF_32, BUFF_33, BUFF_64, BUFF_65, BlockInfoProperty, BufferLength, - BurnBlockInfoProperty, FixedFunction, FunctionArg, FunctionSignature, FunctionType, - MAX_VALUE_SIZE, PrincipalData, StacksBlockInfoProperty, TenureInfoProperty, TupleTypeSignature, - TypeSignature, Value, + BlockInfoProperty, BufferLength, BurnBlockInfoProperty, FixedFunction, FunctionArg, + FunctionSignature, FunctionType, PrincipalData, StacksBlockInfoProperty, TenureInfoProperty, + TupleTypeSignature, TypeSignature, Value, BUFF_1, BUFF_20, BUFF_32, BUFF_33, BUFF_64, BUFF_65, + MAX_VALUE_SIZE, }; use crate::vm::{ClarityName, ClarityVersion, SymbolicExpression, SymbolicExpressionType}; @@ -41,6 +41,7 @@ mod assets; mod conversions; mod maps; mod options; +mod post_conditions; mod sequences; #[allow(clippy::large_enum_variant)] @@ -819,42 +820,6 @@ fn check_get_tenure_info( Ok(TypeSignature::new_option(block_info_prop.type_result())?) } -fn check_restrict_assets( - checker: &mut TypeChecker, - args: &[SymbolicExpression], - context: &TypingContext, -) -> Result { - check_arguments_at_least(3, args)?; - - let asset_owner = &args[0]; - let allowance_list = args[1].match_list().ok_or(CheckError::new( - CheckErrors::RestrictAssetsExpectedListOfAllowances, - ))?; - let body_exprs = &args[2..]; - - runtime_cost( - ClarityCostFunction::AnalysisListItemsCheck, - checker, - allowance_list.len() + body_exprs.len(), - )?; - - checker.type_check_expects(asset_owner, context, &TypeSignature::PrincipalType)?; - - // TODO: type-check the allowances - - // Check the body expressions, ensuring any intermediate responses are handled - let mut last_return = None; - for expr in body_exprs { - let type_return = checker.type_check(expr, context)?; - if type_return.is_response_type() { - return Err(CheckErrors::UncheckedIntermediaryResponses.into()); - } - last_return = Some(type_return); - } - - last_return.ok_or_else(|| CheckError::new(CheckErrors::CheckerImplementationFailure)) -} - impl TypedNativeFunction { pub fn type_check_application( &self, @@ -1245,7 +1210,15 @@ impl TypedNativeFunction { CheckErrors::Expects("FATAL: Legal Clarity response type marked invalid".into()) })?, ))), - RestrictAssets => Special(SpecialNativeFunction(&check_restrict_assets)), + RestrictAssets => Special(SpecialNativeFunction( + &post_conditions::check_restrict_assets, + )), + AsContractSafe => Special(SpecialNativeFunction(&post_conditions::check_as_contract)), + AllowanceWithStx + | AllowanceWithFt + | AllowanceWithNft + | AllowanceWithStacking + | AllowanceAll => Special(SpecialNativeFunction(&post_conditions::check_allowance_err)), }; Ok(out) diff --git a/clarity/src/vm/analysis/type_checker/v2_1/natives/post_conditions.rs b/clarity/src/vm/analysis/type_checker/v2_1/natives/post_conditions.rs new file mode 100644 index 0000000000..8caa6279a4 --- /dev/null +++ b/clarity/src/vm/analysis/type_checker/v2_1/natives/post_conditions.rs @@ -0,0 +1,254 @@ +// Copyright (C) 2025 Stacks Open Internet Foundation +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use clarity_types::errors::analysis::{check_argument_count, check_arguments_at_least}; +use clarity_types::errors::{CheckError, CheckErrors}; +use clarity_types::representations::SymbolicExpression; +use clarity_types::types::signatures::ASCII_128; +use clarity_types::types::TypeSignature; + +use crate::vm::analysis::type_checker::contexts::TypingContext; +use crate::vm::analysis::type_checker::v2_1::TypeChecker; +use crate::vm::costs::cost_functions::ClarityCostFunction; +use crate::vm::costs::runtime_cost; +use crate::vm::functions::NativeFunctions; + +pub fn check_restrict_assets( + checker: &mut TypeChecker, + args: &[SymbolicExpression], + context: &TypingContext, +) -> Result { + check_arguments_at_least(3, args)?; + + let asset_owner = &args[0]; + let allowance_list = args[1] + .match_list() + .ok_or(CheckErrors::ExpectedListOfAllowances( + "restrict-assets?".into(), + 2, + ))?; + let body_exprs = &args[2..]; + + runtime_cost( + ClarityCostFunction::AnalysisListItemsCheck, + checker, + allowance_list.len() + body_exprs.len(), + )?; + + checker.type_check_expects(asset_owner, context, &TypeSignature::PrincipalType)?; + + for allowance in allowance_list { + check_allowance( + checker, + allowance, + context, + &NativeFunctions::RestrictAssets, + )?; + } + + // Check the body expressions, ensuring any intermediate responses are handled + let mut last_return = None; + for expr in body_exprs { + let type_return = checker.type_check(expr, context)?; + if type_return.is_response_type() { + return Err(CheckErrors::UncheckedIntermediaryResponses.into()); + } + last_return = Some(type_return); + } + + let ok_type = last_return.ok_or_else(|| CheckErrors::CheckerImplementationFailure)?; + Ok(TypeSignature::new_response( + ok_type, + TypeSignature::IntType, + )?) +} + +pub fn check_as_contract( + checker: &mut TypeChecker, + args: &[SymbolicExpression], + context: &TypingContext, +) -> Result { + check_arguments_at_least(2, args)?; + + let allowance_list = args[0] + .match_list() + .ok_or(CheckErrors::ExpectedListOfAllowances( + "as-contract?".into(), + 1, + ))?; + let body_exprs = &args[1..]; + + runtime_cost( + ClarityCostFunction::AnalysisListItemsCheck, + checker, + allowance_list.len() + body_exprs.len(), + )?; + + for allowance in allowance_list { + check_allowance( + checker, + allowance, + context, + &NativeFunctions::AsContractSafe, + )?; + } + + // Check the body expressions, ensuring any intermediate responses are handled + let mut last_return = None; + for expr in body_exprs { + let type_return = checker.type_check(expr, context)?; + if type_return.is_response_type() { + return Err(CheckErrors::UncheckedIntermediaryResponses.into()); + } + last_return = Some(type_return); + } + + let ok_type = last_return.ok_or_else(|| CheckErrors::CheckerImplementationFailure)?; + Ok(TypeSignature::new_response( + ok_type, + TypeSignature::IntType, + )?) +} + +/// Type-checking for allowance expressions. These are only allowed within the +/// context of an `restrict-assets?` or `as-contract?` expression. All other +/// uses will reach this function and return an error. +pub fn check_allowance_err( + _checker: &mut TypeChecker, + _args: &[SymbolicExpression], + _context: &TypingContext, +) -> Result { + Err(CheckErrors::AllowanceExprNotAllowed.into()) +} + +pub fn check_allowance( + checker: &mut TypeChecker, + allowance: &SymbolicExpression, + context: &TypingContext, + parent_expr: &NativeFunctions, +) -> Result<(), CheckError> { + let list = allowance + .match_list() + .ok_or(CheckErrors::ExpectedListApplication)?; + let (allowance_fn, args) = list + .split_first() + .ok_or(CheckErrors::ExpectedListApplication)?; + let function_name = allowance_fn + .match_atom() + .ok_or(CheckErrors::NonFunctionApplication)?; + let Some(ref native_function) = + NativeFunctions::lookup_by_name_at_version(function_name, &checker.clarity_version) + else { + return Err(CheckErrors::ExpectedAllowanceExpr(function_name.to_string()).into()); + }; + + match native_function { + NativeFunctions::AllowanceWithStx => { + check_allowance_with_stx(checker, args, context, parent_expr) + } + NativeFunctions::AllowanceWithFt => { + check_allowance_with_ft(checker, args, context, parent_expr) + } + NativeFunctions::AllowanceWithNft => { + check_allowance_with_nft(checker, args, context, parent_expr) + } + NativeFunctions::AllowanceWithStacking => { + check_allowance_with_stacking(checker, args, context, parent_expr) + } + NativeFunctions::AllowanceAll => check_allowance_all(checker, args, context, parent_expr), + _ => Err(CheckErrors::ExpectedAllowanceExpr(function_name.to_string()).into()), + } +} + +/// Type check a `with-stx` allowance expression. +/// `(with-stx amount:uint)` +fn check_allowance_with_stx( + checker: &mut TypeChecker, + args: &[SymbolicExpression], + context: &TypingContext, + _parent_expr: &NativeFunctions, +) -> Result<(), CheckError> { + check_argument_count(1, args)?; + + checker.type_check_expects(&args[0], context, &TypeSignature::UIntType)?; + + Ok(()) +} + +/// Type check a `with-ft` allowance expression. +/// `(with-ft contract-id:principal token-name:(string-ascii 128) amount:uint)` +fn check_allowance_with_ft( + checker: &mut TypeChecker, + args: &[SymbolicExpression], + context: &TypingContext, + _parent_expr: &NativeFunctions, +) -> Result<(), CheckError> { + check_argument_count(3, args)?; + + checker.type_check_expects(&args[0], context, &TypeSignature::PrincipalType)?; + checker.type_check_expects(&args[1], context, &ASCII_128)?; + checker.type_check_expects(&args[2], context, &TypeSignature::UIntType)?; + + Ok(()) +} + +/// Type check a `with-nft` allowance expression. +/// `(with-nft contract-id:principal token-name:(string-ascii 128) asset-id:any)` +fn check_allowance_with_nft( + checker: &mut TypeChecker, + args: &[SymbolicExpression], + context: &TypingContext, + _parent_expr: &NativeFunctions, +) -> Result<(), CheckError> { + check_argument_count(3, args)?; + + checker.type_check_expects(&args[0], context, &TypeSignature::PrincipalType)?; + checker.type_check_expects(&args[1], context, &ASCII_128)?; + // Asset ID can be any type + + Ok(()) +} + +/// Type check a `with-stacking` allowance expression. +/// `(with-stacking amount:uint)` +fn check_allowance_with_stacking( + checker: &mut TypeChecker, + args: &[SymbolicExpression], + context: &TypingContext, + _parent_expr: &NativeFunctions, +) -> Result<(), CheckError> { + check_argument_count(1, args)?; + + checker.type_check_expects(&args[0], context, &TypeSignature::UIntType)?; + + Ok(()) +} + +/// Type check an `with-all-assets-unsafe` allowance expression. +/// `(with-all-assets-unsafe)` +fn check_allowance_all( + _checker: &mut TypeChecker, + args: &[SymbolicExpression], + _context: &TypingContext, + parent_expr: &NativeFunctions, +) -> Result<(), CheckError> { + check_argument_count(0, args)?; + + if parent_expr != &NativeFunctions::AsContractSafe { + return Err(CheckErrors::WithAllAllowanceNotAllowed.into()); + } + + Ok(()) +} diff --git a/clarity/src/vm/analysis/type_checker/v2_1/tests/mod.rs b/clarity/src/vm/analysis/type_checker/v2_1/tests/mod.rs index ac978b277b..0aaac2174a 100644 --- a/clarity/src/vm/analysis/type_checker/v2_1/tests/mod.rs +++ b/clarity/src/vm/analysis/type_checker/v2_1/tests/mod.rs @@ -39,6 +39,7 @@ use crate::vm::{execute_v2, ClarityName, ClarityVersion}; mod assets; pub mod contracts; +mod post_conditions; /// Backwards-compatibility shim for type_checker tests. Runs at latest Clarity version. pub fn mem_type_check(exp: &str) -> Result<(Option, ContractAnalysis), CheckError> { diff --git a/clarity/src/vm/analysis/type_checker/v2_1/tests/post_conditions.rs b/clarity/src/vm/analysis/type_checker/v2_1/tests/post_conditions.rs new file mode 100644 index 0000000000..45dd4bd03f --- /dev/null +++ b/clarity/src/vm/analysis/type_checker/v2_1/tests/post_conditions.rs @@ -0,0 +1,817 @@ +// Copyright (C) 2025 Stacks Open Internet Foundation +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use clarity_types::errors::CheckErrors; +use clarity_types::types::TypeSignature; +use stacks_common::types::StacksEpochId; + +use crate::vm::analysis::type_checker::v2_1::tests::type_check_helper_version; +use crate::vm::tests::test_clarity_versions; +use crate::vm::ClarityVersion; + +/// Test type-checking for `restrict-assets?` expressions +#[apply(test_clarity_versions)] +fn test_restrict_assets(#[case] version: ClarityVersion, #[case] _epoch: StacksEpochId) { + let good = [ + // simple + ( + "(restrict-assets? tx-sender ((with-stx u1000)) true)", + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap() + ), + // literal asset owner + ( + "(restrict-assets? 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4 ((with-stx u1000)) true)", + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap() + ), + // literal asset owner with contract id + ( + "(restrict-assets? 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token ((with-stx u1000)) true)", + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap() + ), + // variable asset owner + ( + "(let ((p tx-sender)) + (restrict-assets? p ((with-stx u1000)) true))", + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap() + ), + // no allowances + ( + "(restrict-assets? tx-sender () true)", + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap() + ), + // multiple allowances + ( + "(restrict-assets? tx-sender ((with-stx u1000) (with-ft .token \"foo\" u5000) (with-nft .token \"foo\" 0x01) (with-stacking u1000)) true)", + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap() + ), + // multiple body expressions + ( + "(restrict-assets? tx-sender ((with-stx u1000)) (+ u1 u2) true)", + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap() + ), + ]; + let bad = [ + // with-all-assets-unsafe + ( + "(restrict-assets? tx-sender ((with-all-assets-unsafe)) true)", + CheckErrors::WithAllAllowanceNotAllowed, + ), + // no asset-owner + ( + "(restrict-assets? ((with-stx u5000)) true)", + CheckErrors::RequiresAtLeastArguments(3, 2), + ), + // no asset-owner, 3 args + ( + "(restrict-assets? ((with-stx u5000)) true true)", + CheckErrors::NonFunctionApplication, + ), + // bad asset-owner type + ( + "(restrict-assets? u100 ((with-stx u5000)) true)", + CheckErrors::TypeError( + TypeSignature::PrincipalType.into(), + TypeSignature::UIntType.into(), + ), + ), + // no allowances + ( + "(restrict-assets? tx-sender true)", + CheckErrors::RequiresAtLeastArguments(3, 2), + ), + // allowance not in list + ( + "(restrict-assets? tx-sender (with-stx u1) true)", + CheckErrors::ExpectedListApplication, + ), + // other value in place of allowance list + ( + "(restrict-assets? tx-sender u1 true)", + CheckErrors::ExpectedListOfAllowances("restrict-assets?".into(), 2), + ), + // non-allowance in allowance list + ( + "(restrict-assets? tx-sender (u1) true)", + CheckErrors::ExpectedListApplication, + ), + // empty list in allowance list + ( + "(restrict-assets? tx-sender (()) true)", + CheckErrors::NonFunctionApplication, + ), + // list with literal in allowance list + ( + "(restrict-assets? tx-sender ((123)) true)", + CheckErrors::NonFunctionApplication, + ), + // non-allowance function in allowance list + ( + "(restrict-assets? tx-sender ((foo)) true)", + CheckErrors::UnknownFunction("foo".into()), + ), + // no body expressions + ( + "(restrict-assets? tx-sender ((with-stx u5000)))", + CheckErrors::RequiresAtLeastArguments(3, 2), + ), + // unhandled response in only body expression + ( + "(restrict-assets? tx-sender ((with-stx u1000)) (err u1))", + CheckErrors::UncheckedIntermediaryResponses, + ), + // unhandled response in last body expression + ( + "(restrict-assets? tx-sender ((with-stx u1000)) true (err u1))", + CheckErrors::UncheckedIntermediaryResponses, + ), + // unhandled response in other body expression + ( + "(restrict-assets? tx-sender ((with-stx u1000)) (err u1) true)", + CheckErrors::UncheckedIntermediaryResponses, + ), + ]; + + for (good_code, expected_type) in &good { + info!("test good code: '{}'", good_code); + if version < ClarityVersion::Clarity4 { + // restrict-assets? is only available in Clarity 4+ + assert_eq!( + CheckErrors::UnknownFunction("restrict-assets?".to_string()), + *type_check_helper_version(good_code, version) + .unwrap_err() + .err + ); + } else { + assert_eq!( + expected_type, + &type_check_helper_version(good_code, version).unwrap() + ); + } + } + + for (bad_code, expected_err) in &bad { + info!("test bad code: '{}'", bad_code); + if version < ClarityVersion::Clarity4 { + // restrict-assets? is only available in Clarity 4+ + assert_eq!( + CheckErrors::UnknownFunction("restrict-assets?".to_string()), + *type_check_helper_version(bad_code, version) + .unwrap_err() + .err + ); + } else { + assert_eq!( + expected_err, + type_check_helper_version(bad_code, version) + .unwrap_err() + .err + .as_ref() + ); + } + } +} + +/// Test type-checking for `as-contract?` expressions +#[apply(test_clarity_versions)] +fn test_as_contract(#[case] version: ClarityVersion, #[case] _epoch: StacksEpochId) { + let good = [ + // simple + ( + "(as-contract? ((with-stx u1000)) true)", + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap() + ), + // no allowances + ( + "(as-contract? () true)", + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap() + ), + // multiple allowances + ( + "(as-contract? ((with-stx u1000) (with-ft .token \"foo\" u5000) (with-nft .token \"foo\" 0x01) (with-stacking u1000)) true)", + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap() + ), + // multiple body expressions + ( + "(as-contract? ((with-stx u1000)) (+ u1 u2) true)", + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap() + ), + // with-all-assets-unsafe + ( + "(as-contract? ((with-all-assets-unsafe)) true)", + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap() + ), + + ]; + let bad = [ + // no allowances + ( + "(as-contract? true)", + CheckErrors::RequiresAtLeastArguments(2, 1), + ), + // allowance not in list + ( + "(as-contract? (with-stx u1) true)", + CheckErrors::ExpectedListApplication, + ), + // other value in place of allowance list + ( + "(as-contract? u1 true)", + CheckErrors::ExpectedListOfAllowances("as-contract?".into(), 1), + ), + // non-allowance in allowance list + ( + "(as-contract? (u1) true)", + CheckErrors::ExpectedListApplication, + ), + // empty list in allowance list + ( + "(as-contract? (()) true)", + CheckErrors::NonFunctionApplication, + ), + // list with literal in allowance list + ( + "(as-contract? ((123)) true)", + CheckErrors::NonFunctionApplication, + ), + // non-allowance function in allowance list + ( + "(as-contract? ((foo)) true)", + CheckErrors::UnknownFunction("foo".into()), + ), + // no body expressions + ( + "(as-contract? ((with-stx u5000)))", + CheckErrors::RequiresAtLeastArguments(2, 1), + ), + // unhandled response in only body expression + ( + "(as-contract? ((with-stx u1000)) (err u1))", + CheckErrors::UncheckedIntermediaryResponses, + ), + // unhandled response in last body expression + ( + "(as-contract? ((with-stx u1000)) true (err u1))", + CheckErrors::UncheckedIntermediaryResponses, + ), + // unhandled response in other body expression + ( + "(as-contract? ((with-stx u1000)) (err u1) true)", + CheckErrors::UncheckedIntermediaryResponses, + ), + ]; + + for (code, expected_type) in &good { + info!("test code: '{}'", code); + if version < ClarityVersion::Clarity4 { + // as-contract? is only available in Clarity 4+ + assert_eq!( + CheckErrors::UnknownFunction("as-contract?".to_string()), + *type_check_helper_version(code, version) + .unwrap_err() + .err + ); + } else { + assert_eq!( + expected_type, + &type_check_helper_version(code, version).unwrap() + ); + } + } + + for (code, expected_err) in &bad { + info!("test code: '{}'", code); + if version < ClarityVersion::Clarity4 { + // as-contract? is only available in Clarity 4+ + assert_eq!( + CheckErrors::UnknownFunction("as-contract?".to_string()), + *type_check_helper_version(code, version) + .unwrap_err() + .err + ); + } else { + assert_eq!( + expected_err, + type_check_helper_version(code, version) + .unwrap_err() + .err + .as_ref() + ); + } + } +} + +/// Test type-checking for `with-stx` allowance expressions +#[apply(test_clarity_versions)] +fn test_with_stx_allowance(#[case] version: ClarityVersion, #[case] _epoch: StacksEpochId) { + let good = [ + // basic usage + ( + "(restrict-assets? tx-sender ((with-stx u1000)) true)", + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap() + ), + // zero amount + ( + "(restrict-assets? tx-sender ((with-stx u0)) true)", + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap() + ), + // large amount + ( + "(restrict-assets? tx-sender ((with-stx u340282366920938463463374607431768211455)) true)", + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap() + ), + // variable amount + ( + "(let ((amount u1000)) (restrict-assets? tx-sender ((with-stx amount)) true))", + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap() + ), + ]; + + let bad = [ + // no arguments + ( + "(restrict-assets? tx-sender ((with-stx)) true)", + CheckErrors::IncorrectArgumentCount(1, 0), + ), + // too many arguments + ( + "(restrict-assets? tx-sender ((with-stx u1000 u2000)) true)", + CheckErrors::IncorrectArgumentCount(1, 2), + ), + // wrong type - string instead of uint + ( + r#"(restrict-assets? tx-sender ((with-stx "1000")) true)"#, + CheckErrors::TypeError( + TypeSignature::UIntType.into(), + TypeSignature::new_string_ascii(4).unwrap().into(), + ), + ), + // wrong type - int instead of uint + ( + "(restrict-assets? tx-sender ((with-stx 1000)) true)", + CheckErrors::TypeError( + TypeSignature::UIntType.into(), + TypeSignature::IntType.into(), + ), + ), + ]; + + for (code, expected_type) in &good { + info!("test code: '{}'", code); + if version < ClarityVersion::Clarity4 { + assert_eq!( + CheckErrors::UnknownFunction("restrict-assets?".to_string()), + *type_check_helper_version(code, version).unwrap_err().err + ); + } else { + assert_eq!( + expected_type, + &type_check_helper_version(code, version).unwrap() + ); + } + } + + for (code, expected_err) in &bad { + info!("test code: '{}'", code); + if version < ClarityVersion::Clarity4 { + assert_eq!( + CheckErrors::UnknownFunction("restrict-assets?".to_string()), + *type_check_helper_version(code, version).unwrap_err().err + ); + } else { + assert_eq!( + expected_err, + type_check_helper_version(code, version) + .unwrap_err() + .err + .as_ref() + ); + } + } +} + +/// Test type-checking for `with-ft` allowance expressions +#[apply(test_clarity_versions)] +fn test_with_ft_allowance(#[case] version: ClarityVersion, #[case] _epoch: StacksEpochId) { + let good = [ + // basic usage with shortcut contract principal + ( + r#"(restrict-assets? tx-sender ((with-ft .token "token-name" u1000)) true)"#, + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(), + ), + // full literal principal + ( + r#"(restrict-assets? tx-sender ((with-ft 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.token "token-name" u1000)) true)"#, + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(), + ), + // variable principal + ( + r#"(let ((contract .token)) (restrict-assets? tx-sender ((with-ft contract "token-name" u1000)) true))"#, + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(), + ), + // variable token name + ( + r#"(let ((name "token-name")) (restrict-assets? tx-sender ((with-ft .token name u1000)) true))"#, + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(), + ), + // variable amount + ( + r#"(let ((amount u1000)) (restrict-assets? tx-sender ((with-ft .token "token-name" amount)) true))"#, + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(), + ), + // "*" token name + ( + r#"(restrict-assets? tx-sender ((with-ft .token "*" u1000)) true)"#, + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(), + ), + // empty token name + ( + r#"(restrict-assets? tx-sender ((with-ft .token "" u1000)) true)"#, + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(), + ), + ]; + + let bad = [ + // no arguments + ( + "(restrict-assets? tx-sender ((with-ft)) true)", + CheckErrors::IncorrectArgumentCount(3, 0), + ), + // one argument + ( + "(restrict-assets? tx-sender ((with-ft .token)) true)", + CheckErrors::IncorrectArgumentCount(3, 1), + ), + // two arguments + ( + r#"(restrict-assets? tx-sender ((with-ft .token "token-name")) true)"#, + CheckErrors::IncorrectArgumentCount(3, 2), + ), + // too many arguments + ( + r#"(restrict-assets? tx-sender ((with-ft .token "token-name" u1000 u2000)) true)"#, + CheckErrors::IncorrectArgumentCount(3, 4), + ), + // wrong type for contract-id - uint instead of principal + ( + r#"(restrict-assets? tx-sender ((with-ft u123 "token-name" u1000)) true)"#, + CheckErrors::TypeError( + TypeSignature::PrincipalType.into(), + TypeSignature::UIntType.into(), + ), + ), + // wrong type for token-name - uint instead of string + ( + "(restrict-assets? tx-sender ((with-ft .token u123 u1000)) true)", + CheckErrors::TypeError( + TypeSignature::new_string_ascii(128).unwrap().into(), + TypeSignature::UIntType.into(), + ), + ), + // wrong type for amount - string instead of uint + ( + r#"(restrict-assets? tx-sender ((with-ft .token "token-name" "1000")) true)"#, + CheckErrors::TypeError( + TypeSignature::UIntType.into(), + TypeSignature::new_string_ascii(4).unwrap().into(), + ), + ), + // wrong type for amount - int instead of uint + ( + r#"(restrict-assets? tx-sender ((with-ft .token "token-name" 1000)) true)"#, + CheckErrors::TypeError( + TypeSignature::UIntType.into(), + TypeSignature::IntType.into(), + ), + ), + // too long token name (longer than 128 chars) + ( + "(restrict-assets? tx-sender ((with-ft .token \"this-token-name-is-way-too-long-to-be-valid-because-it-has-more-than-one-hundred-and-twenty-eight-characters-in-it-so-it-is-not-a-valid-token-name\" u1000)) true)", + CheckErrors::TypeError( + TypeSignature::new_string_ascii(128).unwrap().into(), + TypeSignature::new_string_ascii(146).unwrap().into(), + ), + ), + ]; + + for (code, expected_type) in &good { + info!("test code: '{}'", code); + if version < ClarityVersion::Clarity4 { + assert_eq!( + CheckErrors::UnknownFunction("restrict-assets?".to_string()), + *type_check_helper_version(code, version).unwrap_err().err + ); + } else { + assert_eq!( + expected_type, + &type_check_helper_version(code, version).unwrap() + ); + } + } + + for (code, expected_err) in &bad { + info!("test code: '{}'", code); + if version < ClarityVersion::Clarity4 { + assert_eq!( + CheckErrors::UnknownFunction("restrict-assets?".to_string()), + *type_check_helper_version(code, version).unwrap_err().err + ); + } else { + assert_eq!( + expected_err, + type_check_helper_version(code, version) + .unwrap_err() + .err + .as_ref() + ); + } + } +} + +/// Test type-checking for `with-nft` allowance expressions +#[apply(test_clarity_versions)] +fn test_with_nft_allowance(#[case] version: ClarityVersion, #[case] _epoch: StacksEpochId) { + let good = [ + // basic usage with shortcut contract principal + ( + r#"(restrict-assets? tx-sender ((with-nft .token "token-name" u1000)) true)"#, + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(), + ), + // full literal principal + ( + r#"(restrict-assets? tx-sender ((with-ft 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.token "token-name" u1000)) true)"#, + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(), + ), + // variable principal + ( + r#"(let ((contract .token)) (restrict-assets? tx-sender ((with-nft contract "token-name" u1000)) true))"#, + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(), + ), + // variable token name + ( + r#"(let ((name "token-name")) (restrict-assets? tx-sender ((with-nft .token name u1000)) true))"#, + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(), + ), + // "*" token name + ( + r#"(restrict-assets? tx-sender ((with-nft .token "*" u1000)) true)"#, + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(), + ), + // empty token name + ( + r#"(restrict-assets? tx-sender ((with-nft .token "" u1000)) true)"#, + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(), + ), + // string asset-id + ( + r#"(restrict-assets? tx-sender ((with-nft .token "token-name" "asset-123")) true)"#, + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(), + ), + // buffer asset-id + ( + r#"(restrict-assets? tx-sender ((with-nft .token "token-name" 0x0123456789)) true)"#, + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(), + ), + // variable asset-id + ( + r#"(let ((asset-id u123)) (restrict-assets? tx-sender ((with-nft .token "token-name" asset-id)) true))"#, + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(), + ), + ]; + + let bad = [ + // no arguments + ( + "(restrict-assets? tx-sender ((with-nft)) true)", + CheckErrors::IncorrectArgumentCount(3, 0), + ), + // one argument + ( + "(restrict-assets? tx-sender ((with-nft .token)) true)", + CheckErrors::IncorrectArgumentCount(3, 1), + ), + // two arguments + ( + r#"(restrict-assets? tx-sender ((with-nft .token "token-name")) true)"#, + CheckErrors::IncorrectArgumentCount(3, 2), + ), + // too many arguments + ( + r#"(restrict-assets? tx-sender ((with-nft .token "token-name" u123 u456)) true)"#, + CheckErrors::IncorrectArgumentCount(3, 4), + ), + // wrong type for contract-id - uint instead of principal + ( + r#"(restrict-assets? tx-sender ((with-nft u123 "token-name" u456)) true)"#, + CheckErrors::TypeError( + TypeSignature::PrincipalType.into(), + TypeSignature::UIntType.into(), + ), + ), + // wrong type for token-name - uint instead of string + ( + "(restrict-assets? tx-sender ((with-nft .token u123 u456)) true)", + CheckErrors::TypeError( + TypeSignature::new_string_ascii(128).unwrap().into(), + TypeSignature::UIntType.into(), + ), + ), + // too long token name (longer than 128 chars) + ( + "(restrict-assets? tx-sender ((with-ft .token \"this-token-name-is-way-too-long-to-be-valid-because-it-has-more-than-one-hundred-and-twenty-eight-characters-in-it-so-it-is-not-a-valid-token-name\" u1000)) true)", + CheckErrors::TypeError( + TypeSignature::new_string_ascii(128).unwrap().into(), + TypeSignature::new_string_ascii(146).unwrap().into(), + ), + ), + ]; + + for (code, expected_type) in &good { + info!("test code: '{}'", code); + if version < ClarityVersion::Clarity4 { + assert_eq!( + CheckErrors::UnknownFunction("restrict-assets?".to_string()), + *type_check_helper_version(code, version).unwrap_err().err + ); + } else { + assert_eq!( + expected_type, + &type_check_helper_version(code, version).unwrap() + ); + } + } + + for (code, expected_err) in &bad { + info!("test code: '{}'", code); + if version < ClarityVersion::Clarity4 { + assert_eq!( + CheckErrors::UnknownFunction("restrict-assets?".to_string()), + *type_check_helper_version(code, version).unwrap_err().err + ); + } else { + assert_eq!( + expected_err, + type_check_helper_version(code, version) + .unwrap_err() + .err + .as_ref() + ); + } + } +} + +/// Test type-checking for `with-stacking` allowance expressions +#[apply(test_clarity_versions)] +fn test_with_stacking_allowance(#[case] version: ClarityVersion, #[case] _epoch: StacksEpochId) { + let good = [ + // basic usage + ( + "(restrict-assets? tx-sender ((with-stacking u1000)) true)", + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(), + ), + // zero amount + ( + "(restrict-assets? tx-sender ((with-stacking u0)) true)", + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(), + ), + // variable amount + ( + "(let ((amount u1000)) (restrict-assets? tx-sender ((with-stacking amount)) true))", + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(), + ), + ]; + + let bad = [ + // no arguments + ( + "(restrict-assets? tx-sender ((with-stacking)) true)", + CheckErrors::IncorrectArgumentCount(1, 0), + ), + // too many arguments + ( + "(restrict-assets? tx-sender ((with-stacking u1000 u2000)) true)", + CheckErrors::IncorrectArgumentCount(1, 2), + ), + // wrong type - string instead of uint + ( + r#"(restrict-assets? tx-sender ((with-stacking "1000")) true)"#, + CheckErrors::TypeError( + TypeSignature::UIntType.into(), + TypeSignature::new_string_ascii(4).unwrap().into(), + ), + ), + // wrong type - int instead of uint + ( + "(restrict-assets? tx-sender ((with-stacking 1000)) true)", + CheckErrors::TypeError( + TypeSignature::UIntType.into(), + TypeSignature::IntType.into(), + ), + ), + ]; + + for (code, expected_type) in &good { + info!("test code: '{}'", code); + if version < ClarityVersion::Clarity4 { + assert_eq!( + CheckErrors::UnknownFunction("restrict-assets?".to_string()), + *type_check_helper_version(code, version).unwrap_err().err + ); + } else { + assert_eq!( + expected_type, + &type_check_helper_version(code, version).unwrap() + ); + } + } + + for (code, expected_err) in &bad { + info!("test code: '{}'", code); + if version < ClarityVersion::Clarity4 { + assert_eq!( + CheckErrors::UnknownFunction("restrict-assets?".to_string()), + *type_check_helper_version(code, version).unwrap_err().err + ); + } else { + assert_eq!( + expected_err, + type_check_helper_version(code, version) + .unwrap_err() + .err + .as_ref() + ); + } + } +} + +/// Test type-checking for `with-all-assets-unsafe` allowance expressions +#[apply(test_clarity_versions)] +fn test_with_all_assets_unsafe_allowance( + #[case] version: ClarityVersion, + #[case] _epoch: StacksEpochId, +) { + let good = [ + // basic usage + ( + "(as-contract? ((with-all-assets-unsafe)) true)", + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(), + ), + ]; + + let bad = [ + // with-all-assets-unsafe in restrict-assets? (not allowed) + ( + "(restrict-assets? tx-sender ((with-all-assets-unsafe)) true)", + CheckErrors::WithAllAllowanceNotAllowed, + ), + // with-all-assets-unsafe with arguments (should take 0) + ( + "(restrict-assets? tx-sender ((with-all-assets-unsafe u123)) true)", + CheckErrors::IncorrectArgumentCount(0, 1), + ), + ]; + + for (code, expected_type) in &good { + info!("test code: '{}'", code); + if version < ClarityVersion::Clarity4 { + assert_eq!( + CheckErrors::UnknownFunction("as-contract?".to_string()), + *type_check_helper_version(code, version).unwrap_err().err + ); + } else { + assert_eq!( + expected_type, + &type_check_helper_version(code, version).unwrap() + ); + } + } + + for (code, expected_err) in &bad { + info!("test code: '{}'", code); + if version < ClarityVersion::Clarity4 { + assert_eq!( + CheckErrors::UnknownFunction("restrict-assets?".to_string()), + *type_check_helper_version(code, version).unwrap_err().err + ); + } else { + assert_eq!( + expected_err, + type_check_helper_version(code, version) + .unwrap_err() + .err + .as_ref() + ); + } + } +} diff --git a/clarity/src/vm/costs/cost_functions.rs b/clarity/src/vm/costs/cost_functions.rs index ac9aa39c2b..055232ff4e 100644 --- a/clarity/src/vm/costs/cost_functions.rs +++ b/clarity/src/vm/costs/cost_functions.rs @@ -160,6 +160,7 @@ define_named_enum!(ClarityCostFunction { ContractHash("cost_contract_hash"), ToAscii("cost_to_ascii"), RestrictAssets("cost_restrict_assets"), + AsContractSafe("cost_as_contract_safe"), Unimplemented("cost_unimplemented"), }); @@ -332,6 +333,7 @@ pub trait CostValues { fn cost_contract_hash(n: u64) -> InterpreterResult; fn cost_to_ascii(n: u64) -> InterpreterResult; fn cost_restrict_assets(n: u64) -> InterpreterResult; + fn cost_as_contract_safe(n: u64) -> InterpreterResult; } impl ClarityCostFunction { @@ -487,6 +489,7 @@ impl ClarityCostFunction { ClarityCostFunction::ContractHash => C::cost_contract_hash(n), ClarityCostFunction::ToAscii => C::cost_to_ascii(n), ClarityCostFunction::RestrictAssets => C::cost_restrict_assets(n), + ClarityCostFunction::AsContractSafe => C::cost_as_contract_safe(n), ClarityCostFunction::Unimplemented => Err(RuntimeErrorType::NotImplemented.into()), } } diff --git a/clarity/src/vm/costs/costs_1.rs b/clarity/src/vm/costs/costs_1.rs index 00f3d53ce1..7afbe365a9 100644 --- a/clarity/src/vm/costs/costs_1.rs +++ b/clarity/src/vm/costs/costs_1.rs @@ -13,9 +13,9 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use super::ExecutionCost; /// This file implements the cost functions from costs.clar in Rust. -use super::cost_functions::{CostValues, linear, logn, nlogn}; +use super::cost_functions::{linear, logn, nlogn, CostValues}; +use super::ExecutionCost; use crate::vm::errors::{InterpreterResult, RuntimeErrorType}; pub struct Costs1; @@ -757,4 +757,8 @@ impl CostValues for Costs1 { fn cost_restrict_assets(n: u64) -> InterpreterResult { Err(RuntimeErrorType::NotImplemented.into()) } + + fn cost_as_contract_safe(n: u64) -> InterpreterResult { + Err(RuntimeErrorType::NotImplemented.into()) + } } diff --git a/clarity/src/vm/costs/costs_2.rs b/clarity/src/vm/costs/costs_2.rs index 56d1921acf..bdb4fa3e81 100644 --- a/clarity/src/vm/costs/costs_2.rs +++ b/clarity/src/vm/costs/costs_2.rs @@ -13,9 +13,9 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use super::ExecutionCost; /// This file implements the cost functions from costs-2.clar in Rust. -use super::cost_functions::{CostValues, linear, logn, nlogn}; +use super::cost_functions::{linear, logn, nlogn, CostValues}; +use super::ExecutionCost; use crate::vm::errors::{InterpreterResult, RuntimeErrorType}; pub struct Costs2; @@ -757,4 +757,8 @@ impl CostValues for Costs2 { fn cost_restrict_assets(n: u64) -> InterpreterResult { Err(RuntimeErrorType::NotImplemented.into()) } + + fn cost_as_contract_safe(n: u64) -> InterpreterResult { + Err(RuntimeErrorType::NotImplemented.into()) + } } diff --git a/clarity/src/vm/costs/costs_2_testnet.rs b/clarity/src/vm/costs/costs_2_testnet.rs index 919a0c71c2..d942d83019 100644 --- a/clarity/src/vm/costs/costs_2_testnet.rs +++ b/clarity/src/vm/costs/costs_2_testnet.rs @@ -13,9 +13,9 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use super::ExecutionCost; /// This file implements the cost functions from costs-2-testnet.clar in Rust. -use super::cost_functions::{CostValues, linear, logn, nlogn}; +use super::cost_functions::{linear, logn, nlogn, CostValues}; +use super::ExecutionCost; use crate::vm::errors::{InterpreterResult, RuntimeErrorType}; pub struct Costs2Testnet; @@ -757,4 +757,8 @@ impl CostValues for Costs2Testnet { fn cost_restrict_assets(n: u64) -> InterpreterResult { Err(RuntimeErrorType::NotImplemented.into()) } + + fn cost_as_contract_safe(n: u64) -> InterpreterResult { + Err(RuntimeErrorType::NotImplemented.into()) + } } diff --git a/clarity/src/vm/costs/costs_3.rs b/clarity/src/vm/costs/costs_3.rs index d9dfa0482d..46ae6796ed 100644 --- a/clarity/src/vm/costs/costs_3.rs +++ b/clarity/src/vm/costs/costs_3.rs @@ -13,9 +13,9 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use super::ExecutionCost; /// This file implements the cost functions from costs-3.clar in Rust. -use super::cost_functions::{CostValues, linear, logn, nlogn}; +use super::cost_functions::{linear, logn, nlogn, CostValues}; +use super::ExecutionCost; use crate::vm::errors::{InterpreterResult, RuntimeErrorType}; pub struct Costs3; @@ -775,4 +775,8 @@ impl CostValues for Costs3 { fn cost_restrict_assets(n: u64) -> InterpreterResult { Err(RuntimeErrorType::NotImplemented.into()) } + + fn cost_as_contract_safe(n: u64) -> InterpreterResult { + Err(RuntimeErrorType::NotImplemented.into()) + } } diff --git a/clarity/src/vm/costs/costs_4.rs b/clarity/src/vm/costs/costs_4.rs index 52304f8a41..aca4731eb2 100644 --- a/clarity/src/vm/costs/costs_4.rs +++ b/clarity/src/vm/costs/costs_4.rs @@ -13,7 +13,6 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use super::ExecutionCost; /// This file implements the cost functions from costs-4.clar in Rust. /// For Clarity 4, all cost functions are the same as in costs-3, except /// for the new `cost_contract_hash` function. To avoid duplication, this @@ -21,6 +20,7 @@ use super::ExecutionCost; /// overrides only `cost_contract_hash`. use super::cost_functions::CostValues; use super::costs_3::Costs3; +use super::ExecutionCost; use crate::vm::costs::cost_functions::linear; use crate::vm::errors::InterpreterResult; @@ -467,4 +467,9 @@ impl CostValues for Costs4 { // TODO: needs criterion benchmark Ok(ExecutionCost::runtime(linear(n, 1, 100))) } + + fn cost_as_contract_safe(n: u64) -> InterpreterResult { + // TODO: needs criterion benchmark + Ok(ExecutionCost::runtime(linear(n, 1, 100))) + } } diff --git a/clarity/src/vm/docs/mod.rs b/clarity/src/vm/docs/mod.rs index 88f551c215..202d7c318a 100644 --- a/clarity/src/vm/docs/mod.rs +++ b/clarity/src/vm/docs/mod.rs @@ -15,13 +15,13 @@ // along with this program. If not, see . use super::types::signatures::{FunctionArgSignature, FunctionReturnsSignature}; -use crate::vm::ClarityVersion; -use crate::vm::analysis::type_checker::v2_1::TypedNativeFunction; use crate::vm::analysis::type_checker::v2_1::natives::SimpleNativeFunction; -use crate::vm::functions::NativeFunctions; +use crate::vm::analysis::type_checker::v2_1::TypedNativeFunction; use crate::vm::functions::define::DefineFunctions; +use crate::vm::functions::NativeFunctions; use crate::vm::types::{FixedFunction, FunctionType}; use crate::vm::variables::NativeVariables; +use crate::vm::ClarityVersion; #[cfg(feature = "rusqlite")] pub mod contracts; @@ -102,7 +102,8 @@ const BLOCK_HEIGHT: SimpleKeywordAPI = SimpleKeywordAPI { description: "Returns the current block height of the Stacks blockchain in Clarity 1 and 2. Upon activation of epoch 3.0, `block-height` will return the same value as `tenure-height`. In Clarity 3, `block-height` is removed and has been replaced with `stacks-block-height`.", - example: "(> block-height u1000) ;; returns true if the current block-height has passed 1000 blocks.", + example: + "(> block-height u1000) ;; returns true if the current block-height has passed 1000 blocks.", }; const BURN_BLOCK_HEIGHT: SimpleKeywordAPI = SimpleKeywordAPI { @@ -191,7 +192,8 @@ const REGTEST_KEYWORD: SimpleKeywordAPI = SimpleKeywordAPI { snippet: "is-in-regtest", output_type: "bool", description: "Returns whether or not the code is running in a regression test", - example: "(print is-in-regtest) ;; Will print 'true' if the code is running in a regression test", + example: + "(print is-in-regtest) ;; Will print 'true' if the code is running in a regression test", }; const MAINNET_KEYWORD: SimpleKeywordAPI = SimpleKeywordAPI { @@ -566,7 +568,8 @@ const BITWISE_XOR_API: SimpleFunctionAPI = SimpleFunctionAPI { name: None, snippet: "bit-xor ${1:expr-1} ${2:expr-2}", signature: "(bit-xor i1 i2...)", - description: "Returns the result of bitwise exclusive or'ing a variable number of integer inputs.", + description: + "Returns the result of bitwise exclusive or'ing a variable number of integer inputs.", example: "(bit-xor 1 2) ;; Returns 3 (bit-xor 120 280) ;; Returns 352 (bit-xor -128 64) ;; Returns -64 @@ -592,7 +595,8 @@ const BITWISE_OR_API: SimpleFunctionAPI = SimpleFunctionAPI { name: None, snippet: "bit-or ${1:expr-1} ${2:expr-2}", signature: "(bit-or i1 i2...)", - description: "Returns the result of bitwise inclusive or'ing a variable number of integer inputs.", + description: + "Returns the result of bitwise inclusive or'ing a variable number of integer inputs.", example: "(bit-or 4 8) ;; Returns 12 (bit-or 1 2 4) ;; Returns 7 (bit-or 64 -32 -16) ;; Returns -16 @@ -1578,7 +1582,8 @@ If the supplied argument is an `(ok ...)` value, }; const MATCH_API: SpecialAPI = SpecialAPI { - input_type: "(optional A) name expression expression | (response A B) name expression name expression", + input_type: + "(optional A) name expression expression | (response A B) name expression name expression", snippet: "match ${1:algebraic-expr} ${2:some-binding-name} ${3:some-branch} ${4:none-branch}", output_type: "C", signature: "(match opt-input some-binding-name some-branch none-branch) | @@ -2592,6 +2597,164 @@ error-prone). Returns: "#, }; +const AS_CONTRACT_SAFE: SpecialAPI = SpecialAPI { + input_type: "((Allowance)*), AnyType, ... A", + snippet: "as-contract? (${1:allowance-1} ${2:allowance-2}) ${3:expr-1}", + output_type: "(response A int)", + signature: "(as-contract? ((with-stx|with-ft|with-nft|with-stacking)*) expr-body1 expr-body2 ... expr-body-last)", + description: "Switches the current context's `tx-sender` and +`contract-caller` values to the contract's principal and executes the body +expressions within that context, then checks the asset outflows from the +contract against the granted allowances, in declaration order. If any +allowance is violated, the body expressions are reverted, an error is +returned, and an event is emitted with the full details of the violation to +help with debugging. Note that the allowance setup expressions are evaluated +before executing the body expressions. The final body expression cannot +return a `response` value in order to avoid returning a nested `response` +value from `as-contract?` (nested responses are error-prone). Returns: +* `(ok x)` if the outflows are within the allowances, where `x` is the + result of the final body expression and has type `A`. +* `(err index)` if an allowance was violated, where `index` is the 0-based + index of the first violated allowance in the list of granted allowances, + or -1 if an asset with no allowance caused the violation.", + example: r#" +(define-public (foo) + (as-contract? () + (try! (stx-transfer? u1000000 tx-sender recipient)) + ) +) ;; Returns (err -1) +(define-public (bar) + (as-contract? ((with-stx u1000000)) + (try! (stx-transfer? u1000000 tx-sender recipient)) + ) +) ;; Returns (ok true) +"#, +}; + +const ALLOWANCE_WITH_STX: SpecialAPI = SpecialAPI { + input_type: "uint", + snippet: "with-stx ${1:amount}", + output_type: "Allowance", + signature: "(with-stx amount)", + description: "Adds an outflow allowance for `amount` uSTX from the +`asset-owner` of the enclosing `restrict-assets?` or `as-contract?` +expression. `with-stx` is not allowed outside of `restrict-assets?` or +`as-contract?` contexts.", + example: r#" +(restrict-assets? tx-sender + ((with-stx u1000000)) + (try! (stx-transfer? u2000000 tx-sender 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM)) +) ;; Returns (err 0) +(restrict-assets? tx-sender + ((with-stx u1000000)) + (try! (stx-transfer? u1000000 tx-sender 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM)) +) ;; Returns (ok true) +"#, +}; + +const ALLOWANCE_WITH_FT: SpecialAPI = SpecialAPI { + input_type: "principal (string-ascii 128) uint", + snippet: "with-ft ${1:contract-id} ${2:token-name} ${3:amount}", + output_type: "Allowance", + signature: "(with-ft contract-id token-name amount)", + description: r#"Adds an outflow allowance for `amount` of the fungible +token defined in `contract-id` with name `token-name` from the `asset-owner` +of the enclosing `restrict-assets?` or `as-contract?` expression. `with-ft` is +not allowed outside of `restrict-assets?` or `as-contract?` contexts. Note that +`token-name` should match the name used in the `define-fungible-token` call in +the contract. When `"*"` is used for the token name, the allowance applies to +**all** FTs defined in `contract-id`."#, + example: r#" +(restrict-assets? tx-sender + ((with-ft (contract-of token-trait) "stackaroo" u50)) + (try! (contract-call? token-trait transfer u100 tx-sender 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM none)) +) ;; Returns (err 0) +(restrict-assets? tx-sender + ((with-ft (contract-of token-trait) "stackaroo" u50)) + (try! (contract-call? token-trait transfer u20 tx-sender 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM none)) +) ;; Returns (ok true) +"#, +}; + +const ALLOWANCE_WITH_NFT: SpecialAPI = SpecialAPI { + input_type: "principal (string-ascii 128) T", + snippet: "with-nft ${1:contract-id} ${2:asset-name} ${3:asset-identifier}", + output_type: "Allowance", + signature: "(with-nft contract-id asset-name identifier)", + description: r#"Adds an outflow allowance for the non-fungible token +identified by `identifier` defined in `contract-id` with name `token-name` +from the `asset-owner` of the enclosing `restrict-assets?` or `as-contract?` +expression. `with-nft` is not allowed outside of `restrict-assets?` or +`as-contract?` contexts. Note that `token-name` should match the name used in +the `define-non-fungible-token` call in the contract. When `"*"` is used for +the token name, the allowance applies to **all** NFTs defined in `contract-id`."#, + example: r#" +(restrict-assets? tx-sender + ((with-nft (contract-of nft-trait) "stackaroo" u123)) + (try! (contract-call? nft-trait transfer u4 tx-sender 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM)) +) ;; Returns (err 0) +(restrict-assets? tx-sender + ((with-nft (contract-of nft-trait) "stackaroo" u123)) + (try! (contract-call? nft-trait transfer u123 tx-sender 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM)) +) ;; Returns (ok true) +"#, +}; + +const ALLOWANCE_WITH_STACKING: SpecialAPI = SpecialAPI { + input_type: "uint", + snippet: "with-stacking ${1:amount}", + output_type: "Allowance", + signature: "(with-stacking amount)", + description: "Adds a stacking allowance for `amount` uSTX from the +`asset-owner` of the enclosing `restrict-assets?` or `as-contract?` +expression. `with-stacking` is not allowed outside of `restrict-assets?` or +`as-contract?` contexts. This restricts calls to `delegate-stx` and +`stack-stx` in the active PoX contract to lock up to the amount of uSTX +specified.", + example: r#" +(restrict-assets? tx-sender + ((with-stacking u1000000000000)) + (try! (contract-call? 'SP000000000000000000002Q6VF78.pox-4 delegate-stx + u1100000000000 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM none none + )) +) ;; Returns (err 0) +(restrict-assets? tx-sender + ((with-stacking u1000000000000)) + (try! (contract-call? 'SP000000000000000000002Q6VF78.pox-4 delegate-stx + u900000000000 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM none none + )) +) ;; Returns (ok true) +"#, +}; + +const ALLOWANCE_WITH_ALL: SpecialAPI = SpecialAPI { + input_type: "N/A", + snippet: "with-all-assets-unsafe", + output_type: "Allowance", + signature: "(with-all-assets-unsafe)", + description: "Grants unrestricted access to all assets of the contract to +the enclosing `as-contract?` expression. `with-stacking` is not allowed outside +of `as-contract?` contexts. Note that this is not allowed in `restrict-assets?` +and will trigger an analysis error, since usage there does not make sense (i.e. +just remove the `restrict-assets?` instead). +**_⚠️ Security Warning: This should be used with extreme caution, as it +effectively disables all asset protection for the contract. ⚠️_** This +dangerous allowance should only be used when the code executing within the +`as-contract?` body is verified to be trusted through other means (e.g. +checking traits against an allow list, passed in from a trusted caller), and +even then the more restrictive allowances should be preferred when possible.", + example: r#" +(define-public (execute-trait (trusted-trait )) + (begin + (asserts! (is-eq contract-caller TRUSTED_CALLER) ERR_UNTRUSTED_CALLER) + (as-contract? ((with-all-assets-unsafe)) + (contract-call? trusted-trait execute) + ) + ) +) +"#, +}; + pub fn make_api_reference(function: &NativeFunctions) -> FunctionAPI { use crate::vm::functions::NativeFunctions::*; let name = function.get_name(); @@ -2707,6 +2870,12 @@ pub fn make_api_reference(function: &NativeFunctions) -> FunctionAPI { ContractHash => make_for_simple_native(&CONTRACT_HASH, function, name), ToAscii => make_for_special(&TO_ASCII, function), RestrictAssets => make_for_special(&RESTRICT_ASSETS, function), + AsContractSafe => make_for_special(&AS_CONTRACT_SAFE, function), + AllowanceWithStx => make_for_special(&ALLOWANCE_WITH_STX, function), + AllowanceWithFt => make_for_special(&ALLOWANCE_WITH_FT, function), + AllowanceWithNft => make_for_special(&ALLOWANCE_WITH_NFT, function), + AllowanceWithStacking => make_for_special(&ALLOWANCE_WITH_STACKING, function), + AllowanceAll => make_for_special(&ALLOWANCE_WITH_ALL, function), } } @@ -2818,11 +2987,11 @@ pub fn make_json_api_reference() -> String { #[cfg(test)] mod test { use stacks_common::consts::{CHAIN_ID_TESTNET, PEER_VERSION_EPOCH_2_1}; - use stacks_common::types::StacksEpochId; use stacks_common::types::chainstate::{ BlockHeaderHash, BurnchainHeaderHash, ConsensusHash, SortitionId, StacksAddress, StacksBlockId, VRFSeed, }; + use stacks_common::types::StacksEpochId; use stacks_common::util::hash::hex_bytes; use super::{get_input_type_string, make_all_api_reference, make_json_api_reference}; @@ -2833,13 +3002,13 @@ mod test { BurnStateDB, ClarityDatabase, HeadersDB, MemoryBackingStore, STXBalance, }; use crate::vm::docs::get_output_type_string; - use crate::vm::types::signatures::{ASCII_40, FunctionArgSignature, FunctionReturnsSignature}; + use crate::vm::types::signatures::{FunctionArgSignature, FunctionReturnsSignature, ASCII_40}; use crate::vm::types::{ FunctionType, PrincipalData, QualifiedContractIdentifier, TupleData, TypeSignature, }; use crate::vm::{ - ClarityVersion, ContractContext, GlobalContext, LimitedCostTracker, StacksEpoch, Value, - ast, eval_all, execute, + ast, eval_all, execute, ClarityVersion, ContractContext, GlobalContext, LimitedCostTracker, + StacksEpoch, Value, }; struct DocHeadersDB {} diff --git a/clarity/src/vm/functions/mod.rs b/clarity/src/vm/functions/mod.rs index b587ffbb73..df58bb845c 100644 --- a/clarity/src/vm/functions/mod.rs +++ b/clarity/src/vm/functions/mod.rs @@ -16,18 +16,18 @@ use stacks_common::types::StacksEpochId; -use crate::vm::Value::CallableContract; -use crate::vm::callables::{CallableType, NativeHandle, cost_input_sized_vararg}; +use crate::vm::callables::{cost_input_sized_vararg, CallableType, NativeHandle}; use crate::vm::costs::cost_functions::ClarityCostFunction; -use crate::vm::costs::{CostTracker, MemoryConsumer, constants as cost_constants, runtime_cost}; +use crate::vm::costs::{constants as cost_constants, runtime_cost, CostTracker, MemoryConsumer}; use crate::vm::errors::{ - CheckErrors, Error, InterpreterResult as Result, ShortReturnType, SyntaxBindingError, - SyntaxBindingErrorType, check_argument_count, check_arguments_at_least, + check_argument_count, check_arguments_at_least, CheckErrors, Error, + InterpreterResult as Result, ShortReturnType, SyntaxBindingError, SyntaxBindingErrorType, }; pub use crate::vm::functions::assets::stx_transfer_consolidated; use crate::vm::representations::{ClarityName, SymbolicExpression, SymbolicExpressionType}; use crate::vm::types::{PrincipalData, TypeSignature, Value}; -use crate::vm::{Environment, LocalContext, eval, is_reserved}; +use crate::vm::Value::CallableContract; +use crate::vm::{eval, is_reserved, Environment, LocalContext}; macro_rules! switch_on_global_epoch { ($Name:ident ($Epoch2Version:ident, $Epoch205Version:ident)) => { @@ -144,7 +144,7 @@ define_versioned_named_enum_with_max!(NativeFunctions(ClarityVersion) { Secp256k1Verify("secp256k1-verify", ClarityVersion::Clarity1, None), Print("print", ClarityVersion::Clarity1, None), ContractCall("contract-call?", ClarityVersion::Clarity1, None), - AsContract("as-contract", ClarityVersion::Clarity1, None), + AsContract("as-contract", ClarityVersion::Clarity1, Some(ClarityVersion::Clarity3)), ContractOf("contract-of", ClarityVersion::Clarity1, None), PrincipalOf("principal-of?", ClarityVersion::Clarity1, None), AtBlock("at-block", ClarityVersion::Clarity1, None), @@ -194,7 +194,13 @@ define_versioned_named_enum_with_max!(NativeFunctions(ClarityVersion) { GetTenureInfo("get-tenure-info?", ClarityVersion::Clarity3, None), ContractHash("contract-hash?", ClarityVersion::Clarity4, None), ToAscii("to-ascii?", ClarityVersion::Clarity4, None), - RestrictAssets("restrict-assets?", ClarityVersion::Clarity4, None) + RestrictAssets("restrict-assets?", ClarityVersion::Clarity4, None), + AsContractSafe("as-contract?", ClarityVersion::Clarity4, None), + AllowanceWithStx("with-stx", ClarityVersion::Clarity4, None), + AllowanceWithFt("with-ft", ClarityVersion::Clarity4, None), + AllowanceWithNft("with-nft", ClarityVersion::Clarity4, None), + AllowanceWithStacking("with-stacking", ClarityVersion::Clarity4, None), + AllowanceAll("with-all-assets-unsafe", ClarityVersion::Clarity4, None), }); /// @@ -571,6 +577,16 @@ pub fn lookup_reserved_functions(name: &str, version: &ClarityVersion) -> Option "special_restrict_assets", &post_conditions::special_restrict_assets, ), + AsContractSafe => { + SpecialFunction("special_as_contract", &post_conditions::special_as_contract) + } + AllowanceWithStx + | AllowanceWithFt + | AllowanceWithNft + | AllowanceWithStacking + | AllowanceAll => { + SpecialFunction("special_allowance", &post_conditions::special_allowance) + } }; Some(callable) } else { diff --git a/clarity/src/vm/functions/post_conditions.rs b/clarity/src/vm/functions/post_conditions.rs index 3c52a6a307..a6cf691630 100644 --- a/clarity/src/vm/functions/post_conditions.rs +++ b/clarity/src/vm/functions/post_conditions.rs @@ -74,7 +74,10 @@ pub fn special_restrict_assets( let asset_owner_expr = &args[0]; let allowance_list = args[1] .match_list() - .ok_or(CheckErrors::RestrictAssetsExpectedListOfAllowances)?; + .ok_or(CheckErrors::ExpectedListOfAllowances( + "restrict-assets?".into(), + 2, + ))?; let body_exprs = &args[2..]; let _asset_owner = eval(asset_owner_expr, env, context)?; @@ -108,3 +111,64 @@ pub fn special_restrict_assets( // last_result should always be Some(...), because of the arg len check above. last_result.ok_or_else(|| InterpreterError::Expect("Failed to get let result".into()).into()) } + +/// Handles the function `as-contract?` +pub fn special_as_contract( + args: &[SymbolicExpression], + env: &mut Environment, + context: &LocalContext, +) -> InterpreterResult { + // (as-contract? ((with-stx|with-ft|with-nft|with-stacking)*) expr-body1 expr-body2 ... expr-body-last) + // arg1 => list of asset allowances + // arg2..n => body + check_arguments_at_least(2, args)?; + + let allowance_list = args[0] + .match_list() + .ok_or(CheckErrors::ExpectedListOfAllowances( + "as-contract?".into(), + 1, + ))?; + let body_exprs = &args[1..]; + + runtime_cost( + ClarityCostFunction::AsContractSafe, + env, + allowance_list.len(), + )?; + + let mut allowances = Vec::with_capacity(allowance_list.len()); + for allowance in allowance_list { + allowances.push(eval_allowance(allowance, env, context)?); + } + + // Create a new evaluation context, so that we can rollback if the + // post-conditions are violated + env.global_context.begin(); + + // evaluate the body expressions + let mut last_result = None; + for expr in body_exprs { + let result = eval(expr, env, context)?; + last_result.replace(result); + } + + // TODO: Check the post-conditions and rollback if they are violated + + env.global_context.commit()?; + + // last_result should always be Some(...), because of the arg len check above. + last_result.ok_or_else(|| InterpreterError::Expect("Failed to get let result".into()).into()) +} + +/// Handles all allowance functions, always returning an error, since these are +/// not allowed outside of specific contexts (in `restrict-assets?` and +/// `as-contract?`). When called in the appropriate context, they are handled +/// by the above `eval_allowance` function. +pub fn special_allowance( + _args: &[SymbolicExpression], + _env: &mut Environment, + _context: &LocalContext, +) -> InterpreterResult { + Err(CheckErrors::AllowanceExprNotAllowed.into()) +} diff --git a/stackslib/src/chainstate/stacks/boot/contract_tests.rs b/stackslib/src/chainstate/stacks/boot/contract_tests.rs index e75ed60445..9b042dd8a4 100644 --- a/stackslib/src/chainstate/stacks/boot/contract_tests.rs +++ b/stackslib/src/chainstate/stacks/boot/contract_tests.rs @@ -142,7 +142,7 @@ impl ClarityTestSim { /// Common setup logic for executing blocks in tests /// Returns (store, headers_db, burn_db, current_epoch) fn setup_block_environment( - &mut self, + &'_ mut self, new_tenure: bool, ) -> ( Box, diff --git a/stackslib/src/chainstate/stacks/boot/costs-4.clar b/stackslib/src/chainstate/stacks/boot/costs-4.clar index a1654273f7..f83afdcdf4 100644 --- a/stackslib/src/chainstate/stacks/boot/costs-4.clar +++ b/stackslib/src/chainstate/stacks/boot/costs-4.clar @@ -666,3 +666,6 @@ (define-read-only (cost_restrict_assets (n uint)) (runtime (linear n u1 u100))) ;; TODO: needs criterion benchmark + +(define-read-only (cost_as_contract_safe (n uint)) + (runtime (linear n u1 u100))) ;; TODO: needs criterion benchmark diff --git a/stackslib/src/clarity_vm/tests/analysis_costs.rs b/stackslib/src/clarity_vm/tests/analysis_costs.rs index 6f4244c74e..bee1055261 100644 --- a/stackslib/src/clarity_vm/tests/analysis_costs.rs +++ b/stackslib/src/clarity_vm/tests/analysis_costs.rs @@ -228,9 +228,11 @@ fn epoch_21_test_all(use_mainnet: bool, version: ClarityVersion) { continue; } - let test = get_simple_test(f); - let cost = test_tracked_costs(test, StacksEpochId::Epoch21, version, ix + 1, &mut instance); - assert!(cost.exceeds(&baseline)); + if let Some(test) = get_simple_test(f) { + let cost = + test_tracked_costs(test, StacksEpochId::Epoch21, version, ix + 1, &mut instance); + assert!(cost.exceeds(&baseline)); + } } } @@ -262,15 +264,16 @@ fn epoch_205_test_all(use_mainnet: bool) { for (ix, f) in NativeFunctions::ALL.iter().enumerate() { if f.get_min_version() == ClarityVersion::Clarity1 { - let test = get_simple_test(f); - let cost = test_tracked_costs( - test, - StacksEpochId::Epoch2_05, - ClarityVersion::Clarity1, - ix + 1, - &mut instance, - ); - assert!(cost.exceeds(&baseline)); + if let Some(test) = get_simple_test(f) { + let cost = test_tracked_costs( + test, + StacksEpochId::Epoch2_05, + ClarityVersion::Clarity1, + ix + 1, + &mut instance, + ); + assert!(cost.exceeds(&baseline)); + } } } } diff --git a/stackslib/src/clarity_vm/tests/costs.rs b/stackslib/src/clarity_vm/tests/costs.rs index c4bced3342..0e1f25646d 100644 --- a/stackslib/src/clarity_vm/tests/costs.rs +++ b/stackslib/src/clarity_vm/tests/costs.rs @@ -50,9 +50,9 @@ lazy_static! { boot_code_id("cost-voting", false); } -pub fn get_simple_test(function: &NativeFunctions) -> &'static str { +pub fn get_simple_test(function: &NativeFunctions) -> Option<&'static str> { use clarity::vm::functions::NativeFunctions::*; - match function { + let s = match function { Add => "(+ 1 1)", ToUInt => "(to-uint 1)", ToInt => "(to-int u1)", @@ -166,7 +166,12 @@ pub fn get_simple_test(function: &NativeFunctions) -> &'static str { ContractHash => "(contract-hash? .contract-other)", ToAscii => "(to-ascii? 65)", RestrictAssets => "(restrict-assets? tx-sender () (+ u1 u2))", - } + AsContractSafe => "(as-contract? () (+ u1 u2))", + // These expressions are not usable in this context, since they are + // only allowed within `restrict-assets?` or `as-contract?` + AllowanceWithStx | AllowanceWithFt | AllowanceWithNft | AllowanceWithStacking | AllowanceAll => return None, + }; + Some(s) } fn execute_transaction( @@ -1036,11 +1041,16 @@ fn epoch_20_205_test_all(use_mainnet: bool, epoch: StacksEpochId) { for (ix, f) in NativeFunctions::ALL.iter().enumerate() { // Note: The 2.0 and 2.05 test assumes Clarity1. - if f.get_min_version() == ClarityVersion::Clarity1 { - let test = get_simple_test(f); - let cost = - test_program_cost(test, ClarityVersion::Clarity1, &mut owned_env, ix + 1); - assert!(cost.exceeds(&baseline)); + if f.get_min_version() == ClarityVersion::Clarity1 + && f.get_max_version() + .map(|max| max >= ClarityVersion::Clarity1) + .unwrap_or(true) + { + if let Some(test) = get_simple_test(f) { + let cost = + test_program_cost(test, ClarityVersion::Clarity1, &mut owned_env, ix + 1); + assert!(cost.exceeds(&baseline)); + } } } }) @@ -1078,13 +1088,14 @@ fn epoch_21_test_all(use_mainnet: bool) { // Note: Include Clarity2 functions for Epoch21. if f.get_min_version() <= ClarityVersion::Clarity2 && f.get_max_version() - .map(|max| max < ClarityVersion::Clarity2) + .map(|max| max >= ClarityVersion::Clarity2) .unwrap_or(true) { - let test = get_simple_test(f); - let cost = - test_program_cost(test, ClarityVersion::Clarity2, &mut owned_env, ix + 1); - assert!(cost.exceeds(&baseline)); + if let Some(test) = get_simple_test(f) { + let cost = + test_program_cost(test, ClarityVersion::Clarity2, &mut owned_env, ix + 1); + assert!(cost.exceeds(&baseline)); + } } } }) @@ -1115,10 +1126,11 @@ fn epoch_30_test_all(use_mainnet: bool) { .map(|max| max >= ClarityVersion::Clarity3) .unwrap_or(true) { - let test = get_simple_test(f); - let cost = - test_program_cost(test, ClarityVersion::Clarity3, &mut owned_env, ix + 1); - assert!(cost.exceeds(&baseline)); + if let Some(test) = get_simple_test(f) { + let cost = + test_program_cost(test, ClarityVersion::Clarity3, &mut owned_env, ix + 1); + assert!(cost.exceeds(&baseline)); + } } } }) @@ -1149,10 +1161,11 @@ fn epoch_33_test_all(use_mainnet: bool) { .map(|max| max >= ClarityVersion::Clarity4) .unwrap_or(true) { - let test = get_simple_test(f); - let cost = - test_program_cost(test, ClarityVersion::Clarity4, &mut owned_env, ix + 1); - assert!(cost.exceeds(&baseline)); + if let Some(test) = get_simple_test(f) { + let cost = + test_program_cost(test, ClarityVersion::Clarity4, &mut owned_env, ix + 1); + assert!(cost.exceeds(&baseline)); + } } } }) From e4fc430c24f18855feef2a379a9bdd876c7b68fe Mon Sep 17 00:00:00 2001 From: Brice Dobry Date: Thu, 18 Sep 2025 20:22:51 -0400 Subject: [PATCH 03/35] chore: fix formatting --- .../analysis/type_checker/v2_1/tests/post_conditions.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/clarity/src/vm/analysis/type_checker/v2_1/tests/post_conditions.rs b/clarity/src/vm/analysis/type_checker/v2_1/tests/post_conditions.rs index 45dd4bd03f..8be23e0070 100644 --- a/clarity/src/vm/analysis/type_checker/v2_1/tests/post_conditions.rs +++ b/clarity/src/vm/analysis/type_checker/v2_1/tests/post_conditions.rs @@ -278,9 +278,7 @@ fn test_as_contract(#[case] version: ClarityVersion, #[case] _epoch: StacksEpoch // as-contract? is only available in Clarity 4+ assert_eq!( CheckErrors::UnknownFunction("as-contract?".to_string()), - *type_check_helper_version(code, version) - .unwrap_err() - .err + *type_check_helper_version(code, version).unwrap_err().err ); } else { assert_eq!( @@ -296,9 +294,7 @@ fn test_as_contract(#[case] version: ClarityVersion, #[case] _epoch: StacksEpoch // as-contract? is only available in Clarity 4+ assert_eq!( CheckErrors::UnknownFunction("as-contract?".to_string()), - *type_check_helper_version(code, version) - .unwrap_err() - .err + *type_check_helper_version(code, version).unwrap_err().err ); } else { assert_eq!( From e29ec1633108418cb2a5fca822a06656761a902a Mon Sep 17 00:00:00 2001 From: Brice Dobry Date: Fri, 19 Sep 2025 10:05:33 -0400 Subject: [PATCH 04/35] fix: add Clarity4 version of test contract --- .../src/vm/analysis/type_checker/v2_1/tests/contracts.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/clarity/src/vm/analysis/type_checker/v2_1/tests/contracts.rs b/clarity/src/vm/analysis/type_checker/v2_1/tests/contracts.rs index 82dd67ce0e..71398ca0c5 100644 --- a/clarity/src/vm/analysis/type_checker/v2_1/tests/contracts.rs +++ b/clarity/src/vm/analysis/type_checker/v2_1/tests/contracts.rs @@ -2647,13 +2647,18 @@ fn clarity_trait_experiments_downcast_trait_5( ) { let mut marf = MemoryBackingStore::new(); let mut db = marf.as_analysis_db(); + let downcast_trait_5 = if version >= ClarityVersion::Clarity4 { + "downcast-trait-5-c4" + } else { + "downcast-trait-5" + }; // Can we use a principal exp where a trait type is expected? // Principal can come from constant/var/map/function/keyword let err = db .execute(|db| { load_versioned(db, "math-trait", version, epoch)?; - load_versioned(db, "downcast-trait-5", version, epoch) + load_versioned(db, downcast_trait_5, version, epoch) }) .unwrap_err(); if epoch <= StacksEpochId::Epoch2_05 { From 196e71e1f0622a93e03e7be86b13ed7c751ee4fc Mon Sep 17 00:00:00 2001 From: Brice Dobry Date: Sat, 20 Sep 2025 18:12:43 -0400 Subject: [PATCH 05/35] feat: initial implementation of allowances --- CHANGELOG.md | 8 + clarity-types/src/errors/analysis.rs | 2 + clarity-types/src/types/mod.rs | 10 + .../v2_1/natives/post_conditions.rs | 68 ++- .../tests/contracts/downcast-trait-5-c4.clar | 13 + .../v2_1/tests/post_conditions.rs | 10 + clarity/src/vm/contexts.rs | 22 + clarity/src/vm/docs/mod.rs | 133 +++--- clarity/src/vm/functions/post_conditions.rs | 311 ++++++++++++-- clarity/src/vm/tests/mod.rs | 2 + clarity/src/vm/tests/post_conditions.rs | 406 ++++++++++++++++++ 11 files changed, 847 insertions(+), 138 deletions(-) create mode 100644 clarity/src/vm/analysis/type_checker/v2_1/tests/contracts/downcast-trait-5-c4.clar create mode 100644 clarity/src/vm/tests/post_conditions.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index da077c5051..4c7f8ddca5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,14 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE - `current-contract` - `block-time` - `to-ascii?` + - `restrict-assets?` + - `as-contract?` + - Special allowance expressions: + - `with-stx` + - `with-ft` + - `with-nft` + - `with-stacking` + - `with-all-assets-unsafe` - Added `contract_cost_limit_percentage` to the miner config file — sets the percentage of a block’s execution cost at which, if a large non-boot contract call would cause a BlockTooBigError, the miner will stop adding further non-boot contract calls and only include STX transfers and boot contract calls for the remainder of the block. ### Changed diff --git a/clarity-types/src/errors/analysis.rs b/clarity-types/src/errors/analysis.rs index 698181a65d..8573d91882 100644 --- a/clarity-types/src/errors/analysis.rs +++ b/clarity-types/src/errors/analysis.rs @@ -313,6 +313,7 @@ pub enum CheckErrors { AllowanceExprNotAllowed, ExpectedAllowanceExpr(String), WithAllAllowanceNotAllowed, + WithAllAllowanceNotAlone, } #[derive(Debug, PartialEq)] @@ -615,6 +616,7 @@ impl DiagnosableError for CheckErrors { CheckErrors::AllowanceExprNotAllowed => "allowance expressions are only allowed in the context of a `restrict-assets?` or `as-contract?`".into(), CheckErrors::ExpectedAllowanceExpr(got_name) => format!("expected an allowance expression, got: {got_name}"), CheckErrors::WithAllAllowanceNotAllowed => "with-all-assets-unsafe is not allowed here, only in the allowance list for `as-contract?`".into(), + CheckErrors::WithAllAllowanceNotAlone => "with-all-assets-unsafe must not be used along with other allowances".into(), } } diff --git a/clarity-types/src/types/mod.rs b/clarity-types/src/types/mod.rs index 5fb3e36a1c..ba15c0530e 100644 --- a/clarity-types/src/types/mod.rs +++ b/clarity-types/src/types/mod.rs @@ -1226,6 +1226,16 @@ impl Value { Err(InterpreterError::Expect("Expected response".into()).into()) } } + + pub fn expect_string_ascii(self) -> Result { + if let Value::Sequence(SequenceData::String(CharType::ASCII(ASCIIData { data }))) = self { + Ok(String::from_utf8(data) + .map_err(|_| InterpreterError::Expect("Non UTF-8 data in string".into()))?) + } else { + error!("Value '{self:?}' is not an ASCII string"); + Err(InterpreterError::Expect("Expected ASCII string".into()).into()) + } + } } impl BuffData { diff --git a/clarity/src/vm/analysis/type_checker/v2_1/natives/post_conditions.rs b/clarity/src/vm/analysis/type_checker/v2_1/natives/post_conditions.rs index 8caa6279a4..bf6c0f82d6 100644 --- a/clarity/src/vm/analysis/type_checker/v2_1/natives/post_conditions.rs +++ b/clarity/src/vm/analysis/type_checker/v2_1/natives/post_conditions.rs @@ -50,12 +50,9 @@ pub fn check_restrict_assets( checker.type_check_expects(asset_owner, context, &TypeSignature::PrincipalType)?; for allowance in allowance_list { - check_allowance( - checker, - allowance, - context, - &NativeFunctions::RestrictAssets, - )?; + if check_allowance(checker, allowance, context)? { + return Err(CheckErrors::WithAllAllowanceNotAllowed.into()); + } } // Check the body expressions, ensuring any intermediate responses are handled @@ -97,12 +94,9 @@ pub fn check_as_contract( )?; for allowance in allowance_list { - check_allowance( - checker, - allowance, - context, - &NativeFunctions::AsContractSafe, - )?; + if check_allowance(checker, allowance, context)? && allowance_list.len() > 1 { + return Err(CheckErrors::WithAllAllowanceNotAlone.into()); + } } // Check the body expressions, ensuring any intermediate responses are handled @@ -133,12 +127,13 @@ pub fn check_allowance_err( Err(CheckErrors::AllowanceExprNotAllowed.into()) } +/// Type check an allowance expression, returning whether it is a +/// `with-all-assets-unsafe` allowance (which has special rules). pub fn check_allowance( checker: &mut TypeChecker, allowance: &SymbolicExpression, context: &TypingContext, - parent_expr: &NativeFunctions, -) -> Result<(), CheckError> { +) -> Result { let list = allowance .match_list() .ok_or(CheckErrors::ExpectedListApplication)?; @@ -155,19 +150,13 @@ pub fn check_allowance( }; match native_function { - NativeFunctions::AllowanceWithStx => { - check_allowance_with_stx(checker, args, context, parent_expr) - } - NativeFunctions::AllowanceWithFt => { - check_allowance_with_ft(checker, args, context, parent_expr) - } - NativeFunctions::AllowanceWithNft => { - check_allowance_with_nft(checker, args, context, parent_expr) - } + NativeFunctions::AllowanceWithStx => check_allowance_with_stx(checker, args, context), + NativeFunctions::AllowanceWithFt => check_allowance_with_ft(checker, args, context), + NativeFunctions::AllowanceWithNft => check_allowance_with_nft(checker, args, context), NativeFunctions::AllowanceWithStacking => { - check_allowance_with_stacking(checker, args, context, parent_expr) + check_allowance_with_stacking(checker, args, context) } - NativeFunctions::AllowanceAll => check_allowance_all(checker, args, context, parent_expr), + NativeFunctions::AllowanceAll => check_allowance_all(checker, args, context), _ => Err(CheckErrors::ExpectedAllowanceExpr(function_name.to_string()).into()), } } @@ -178,13 +167,12 @@ fn check_allowance_with_stx( checker: &mut TypeChecker, args: &[SymbolicExpression], context: &TypingContext, - _parent_expr: &NativeFunctions, -) -> Result<(), CheckError> { +) -> Result { check_argument_count(1, args)?; checker.type_check_expects(&args[0], context, &TypeSignature::UIntType)?; - Ok(()) + Ok(false) } /// Type check a `with-ft` allowance expression. @@ -193,15 +181,14 @@ fn check_allowance_with_ft( checker: &mut TypeChecker, args: &[SymbolicExpression], context: &TypingContext, - _parent_expr: &NativeFunctions, -) -> Result<(), CheckError> { +) -> Result { check_argument_count(3, args)?; checker.type_check_expects(&args[0], context, &TypeSignature::PrincipalType)?; checker.type_check_expects(&args[1], context, &ASCII_128)?; checker.type_check_expects(&args[2], context, &TypeSignature::UIntType)?; - Ok(()) + Ok(false) } /// Type check a `with-nft` allowance expression. @@ -210,15 +197,14 @@ fn check_allowance_with_nft( checker: &mut TypeChecker, args: &[SymbolicExpression], context: &TypingContext, - _parent_expr: &NativeFunctions, -) -> Result<(), CheckError> { +) -> Result { check_argument_count(3, args)?; checker.type_check_expects(&args[0], context, &TypeSignature::PrincipalType)?; checker.type_check_expects(&args[1], context, &ASCII_128)?; // Asset ID can be any type - Ok(()) + Ok(false) } /// Type check a `with-stacking` allowance expression. @@ -227,13 +213,12 @@ fn check_allowance_with_stacking( checker: &mut TypeChecker, args: &[SymbolicExpression], context: &TypingContext, - _parent_expr: &NativeFunctions, -) -> Result<(), CheckError> { +) -> Result { check_argument_count(1, args)?; checker.type_check_expects(&args[0], context, &TypeSignature::UIntType)?; - Ok(()) + Ok(false) } /// Type check an `with-all-assets-unsafe` allowance expression. @@ -242,13 +227,8 @@ fn check_allowance_all( _checker: &mut TypeChecker, args: &[SymbolicExpression], _context: &TypingContext, - parent_expr: &NativeFunctions, -) -> Result<(), CheckError> { +) -> Result { check_argument_count(0, args)?; - if parent_expr != &NativeFunctions::AsContractSafe { - return Err(CheckErrors::WithAllAllowanceNotAllowed.into()); - } - - Ok(()) + Ok(true) } diff --git a/clarity/src/vm/analysis/type_checker/v2_1/tests/contracts/downcast-trait-5-c4.clar b/clarity/src/vm/analysis/type_checker/v2_1/tests/contracts/downcast-trait-5-c4.clar new file mode 100644 index 0000000000..1ecc4bae49 --- /dev/null +++ b/clarity/src/vm/analysis/type_checker/v2_1/tests/contracts/downcast-trait-5-c4.clar @@ -0,0 +1,13 @@ +(impl-trait .math-trait.math) +(define-read-only (add (x uint) (y uint)) (ok (+ x y)) ) +(define-read-only (sub (x uint) (y uint)) (ok (- x y)) ) + +(use-trait math .math-trait.math) + +(define-public (use (math-contract )) + (ok true) +) + +(define-public (downcast) + (as-contract? ((with-all-assets-unsafe)) (use tx-sender)) +) diff --git a/clarity/src/vm/analysis/type_checker/v2_1/tests/post_conditions.rs b/clarity/src/vm/analysis/type_checker/v2_1/tests/post_conditions.rs index 8be23e0070..488d341541 100644 --- a/clarity/src/vm/analysis/type_checker/v2_1/tests/post_conditions.rs +++ b/clarity/src/vm/analysis/type_checker/v2_1/tests/post_conditions.rs @@ -270,6 +270,16 @@ fn test_as_contract(#[case] version: ClarityVersion, #[case] _epoch: StacksEpoch "(as-contract? ((with-stx u1000)) (err u1) true)", CheckErrors::UncheckedIntermediaryResponses, ), + // other allowances together with with-all-assets-unsafe (first) + ( + "(as-contract? ((with-all-assets-unsafe) (with-stx u1000)) true)", + CheckErrors::WithAllAllowanceNotAlone, + ), + // other allowances together with with-all-assets-unsafe (second) + ( + "(as-contract? ((with-stx u1000) (with-all-assets-unsafe)) true)", + CheckErrors::WithAllAllowanceNotAlone, + ), ]; for (code, expected_type) in &good { diff --git a/clarity/src/vm/contexts.rs b/clarity/src/vm/contexts.rs index 116c631387..6dba496d4b 100644 --- a/clarity/src/vm/contexts.rs +++ b/clarity/src/vm/contexts.rs @@ -455,6 +455,14 @@ impl AssetMap { assets.get(asset_identifier).copied() } + pub fn get_all_fungible_tokens( + &self, + principal: &PrincipalData, + ) -> Option<&HashMap> { + let assets = self.token_map.get(principal)?; + Some(assets) + } + pub fn get_nonfungible_tokens( &self, principal: &PrincipalData, @@ -463,6 +471,14 @@ impl AssetMap { let assets = self.asset_map.get(principal)?; assets.get(asset_identifier) } + + pub fn get_all_nonfungible_tokens( + &self, + principal: &PrincipalData, + ) -> Option<&HashMap>> { + let assets = self.asset_map.get(principal)?; + Some(assets) + } } impl fmt::Display for AssetMap { @@ -1551,6 +1567,12 @@ impl<'a, 'hooks> GlobalContext<'a, 'hooks> { .ok_or_else(|| InterpreterError::Expect("Failed to obtain asset map".into()).into()) } + pub fn get_readonly_asset_map(&mut self) -> Result<&AssetMap> { + self.asset_maps + .last() + .ok_or_else(|| InterpreterError::Expect("Failed to obtain asset map".into()).into()) + } + pub fn log_asset_transfer( &mut self, sender: &PrincipalData, diff --git a/clarity/src/vm/docs/mod.rs b/clarity/src/vm/docs/mod.rs index 202d7c318a..a221064081 100644 --- a/clarity/src/vm/docs/mod.rs +++ b/clarity/src/vm/docs/mod.rs @@ -1412,7 +1412,7 @@ function returns _err_, any database changes resulting from calling `contract-ca If the function returns _ok_, database changes occurred.", example: " ;; instantiate the sample/contracts/tokens.clar contract first! -(as-contract (contract-call? .tokens mint! u19)) ;; Returns (ok u19)" +(as-contract? () (try! (contract-call? .tokens mint! u19))) ;; Returns (ok u19)" }; const CONTRACT_OF_API: SpecialAPI = SpecialAPI { @@ -2374,7 +2374,7 @@ In the event that the `owner` principal isn't materialized, it returns 0. ", example: " (stx-get-balance 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR) ;; Returns u0 -(stx-get-balance (as-contract tx-sender)) ;; Returns u1000 +(stx-get-balance tx-sender) ;; Returns u1000 ", }; @@ -2390,7 +2390,7 @@ unlock height for any locked STX, all denominated in microstacks. ", example: r#" (stx-account 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR) ;; Returns (tuple (locked u0) (unlock-height u0) (unlocked u0)) -(stx-account (as-contract tx-sender)) ;; Returns (tuple (locked u0) (unlock-height u0) (unlocked u1000)) +(stx-account tx-sender) ;; Returns (tuple (locked u0) (unlock-height u0) (unlocked u1000)) "#, }; @@ -2412,12 +2412,9 @@ one of the following error codes: * `(err u4)` -- the `sender` principal is not the current `tx-sender` ", example: r#" -(as-contract - (stx-transfer? u60 tx-sender 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR)) ;; Returns (ok true) -(as-contract - (stx-transfer? u60 tx-sender 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR)) ;; Returns (ok true) -(as-contract - (stx-transfer? u50 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR tx-sender)) ;; Returns (err u4) +(stx-transfer? u60 tx-sender 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR) ;; Returns (ok true) +(stx-transfer? u60 tx-sender 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR) ;; Returns (ok true) +(stx-transfer? u50 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR tx-sender) ;; Returns (err u4) "# }; @@ -2431,8 +2428,7 @@ const STX_TRANSFER_MEMO: SpecialAPI = SpecialAPI { This function returns (ok true) if the transfer is successful, or, on an error, returns the same codes as `stx-transfer?`. ", example: r#" -(as-contract - (stx-transfer-memo? u60 tx-sender 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR 0x010203)) ;; Returns (ok true) +(stx-transfer-memo? u60 tx-sender 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR 0x010203) ;; Returns (ok true) "# }; @@ -2452,10 +2448,8 @@ one of the following error codes: * `(err u4)` -- the `sender` principal is not the current `tx-sender` ", example: " -(as-contract - (stx-burn? u60 tx-sender)) ;; Returns (ok true) -(as-contract - (stx-burn? u50 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR)) ;; Returns (err u4) +(stx-burn? u60 tx-sender) ;; Returns (ok true) +(stx-burn? u50 'SZ2J6ZY48GV1EZ5V2V5RB9MP66SW86PYKKQ9H6DPR) ;; Returns (err u4) " }; @@ -2588,12 +2582,12 @@ error-prone). Returns: index of the first violated allowance in the list of granted allowances, or -1 if an asset with no allowance caused the violation.", example: r#" -(restrict-assets? tx-sender () - (try! (stx-transfer? u1000000 tx-sender 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM)) -) ;; Returns (err -1) (restrict-assets? tx-sender () (+ u1 u2) ) ;; Returns (ok u3) +(restrict-assets? tx-sender () + (try! (stx-transfer? u50 tx-sender 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM)) +) ;; Returns (err -1) "#, }; @@ -2618,16 +2612,16 @@ value from `as-contract?` (nested responses are error-prone). Returns: index of the first violated allowance in the list of granted allowances, or -1 if an asset with no allowance caused the violation.", example: r#" -(define-public (foo) +(let ((recipient tx-sender)) + (as-contract? ((with-stx u100)) + (try! (stx-transfer? u50 tx-sender recipient)) + ) +) ;; Returns (ok true) +(let ((recipient tx-sender)) (as-contract? () - (try! (stx-transfer? u1000000 tx-sender recipient)) + (try! (stx-transfer? u50 tx-sender recipient)) ) ) ;; Returns (err -1) -(define-public (bar) - (as-contract? ((with-stx u1000000)) - (try! (stx-transfer? u1000000 tx-sender recipient)) - ) -) ;; Returns (ok true) "#, }; @@ -2642,13 +2636,13 @@ expression. `with-stx` is not allowed outside of `restrict-assets?` or `as-contract?` contexts.", example: r#" (restrict-assets? tx-sender - ((with-stx u1000000)) - (try! (stx-transfer? u2000000 tx-sender 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM)) -) ;; Returns (err 0) -(restrict-assets? tx-sender - ((with-stx u1000000)) - (try! (stx-transfer? u1000000 tx-sender 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM)) + ((with-stx u100)) + (try! (stx-transfer? u100 tx-sender 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM)) ) ;; Returns (ok true) +(restrict-assets? tx-sender + ((with-stx u50)) + (try! (stx-transfer? u100 tx-sender 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM)) +) ;; Returns (err 0) "#, }; @@ -2665,14 +2659,16 @@ not allowed outside of `restrict-assets?` or `as-contract?` contexts. Note that the contract. When `"*"` is used for the token name, the allowance applies to **all** FTs defined in `contract-id`."#, example: r#" +(define-fungible-token stackaroo) +(ft-mint? stackaroo u200 tx-sender) (restrict-assets? tx-sender - ((with-ft (contract-of token-trait) "stackaroo" u50)) - (try! (contract-call? token-trait transfer u100 tx-sender 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM none)) -) ;; Returns (err 0) -(restrict-assets? tx-sender - ((with-ft (contract-of token-trait) "stackaroo" u50)) - (try! (contract-call? token-trait transfer u20 tx-sender 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM none)) + ((with-ft current-contract "stackaroo" u50)) + (try! (ft-transfer? stackaroo u100 tx-sender 'SPAXYA5XS51713FDTQ8H94EJ4V579CXMTRNBZKSF)) ) ;; Returns (ok true) +(restrict-assets? tx-sender + ((with-ft current-contract "stackaroo" u50)) + (try! (ft-transfer? stackaroo u100 tx-sender 'SPAXYA5XS51713FDTQ8H94EJ4V579CXMTRNBZKSF)) +) ;; Returns (err 0) "#, }; @@ -2689,14 +2685,18 @@ expression. `with-nft` is not allowed outside of `restrict-assets?` or the `define-non-fungible-token` call in the contract. When `"*"` is used for the token name, the allowance applies to **all** NFTs defined in `contract-id`."#, example: r#" +(define-non-fungible-token stackaroo uint) +(nft-mint? stackaroo u123 tx-sender) +(nft-mint? stackaroo u124 tx-sender) +(nft-mint? stackaroo u125 tx-sender) (restrict-assets? tx-sender - ((with-nft (contract-of nft-trait) "stackaroo" u123)) - (try! (contract-call? nft-trait transfer u4 tx-sender 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM)) -) ;; Returns (err 0) -(restrict-assets? tx-sender - ((with-nft (contract-of nft-trait) "stackaroo" u123)) - (try! (contract-call? nft-trait transfer u123 tx-sender 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM)) + ((with-nft current-contract "stackaroo" u123)) + (try! (nft-transfer? stackaroo u123 tx-sender 'SPAXYA5XS51713FDTQ8H94EJ4V579CXMTRNBZKSF)) ) ;; Returns (ok true) +(restrict-assets? tx-sender + ((with-nft current-contract "stackaroo" u125)) + (try! (nft-transfer? stackaroo u124 tx-sender 'SPAXYA5XS51713FDTQ8H94EJ4V579CXMTRNBZKSF)) +) ;; Returns (err 0) "#, }; @@ -2708,9 +2708,9 @@ const ALLOWANCE_WITH_STACKING: SpecialAPI = SpecialAPI { description: "Adds a stacking allowance for `amount` uSTX from the `asset-owner` of the enclosing `restrict-assets?` or `as-contract?` expression. `with-stacking` is not allowed outside of `restrict-assets?` or -`as-contract?` contexts. This restricts calls to `delegate-stx` and -`stack-stx` in the active PoX contract to lock up to the amount of uSTX -specified.", +`as-contract?` contexts. This restricts calls to the active PoX contract +that either delegate funds for stacking or stack directly, ensuring that the +locked amount is limited by the amount of uSTX specified.", example: r#" (restrict-assets? tx-sender ((with-stacking u1000000000000)) @@ -2744,14 +2744,11 @@ dangerous allowance should only be used when the code executing within the checking traits against an allow list, passed in from a trusted caller), and even then the more restrictive allowances should be preferred when possible.", example: r#" -(define-public (execute-trait (trusted-trait )) - (begin - (asserts! (is-eq contract-caller TRUSTED_CALLER) ERR_UNTRUSTED_CALLER) - (as-contract? ((with-all-assets-unsafe)) - (contract-call? trusted-trait execute) - ) +(let ((recipient tx-sender)) + (as-contract? ((with-all-assets-unsafe)) + (try! (stx-transfer? u100 tx-sender recipient)) ) -) +) ;; Returns (ok true) "#, }; @@ -2986,6 +2983,7 @@ pub fn make_json_api_reference() -> String { #[cfg(test)] mod test { + use clarity_types::types::StandardPrincipalData; use stacks_common::consts::{CHAIN_ID_TESTNET, PEER_VERSION_EPOCH_2_1}; use stacks_common::types::chainstate::{ BlockHeaderHash, BurnchainHeaderHash, ConsensusHash, SortitionId, StacksAddress, @@ -3229,7 +3227,7 @@ mod test { } } - fn docs_execute(store: &mut MemoryBackingStore, program: &str) { + fn docs_execute(store: &mut MemoryBackingStore, program: &str, version: ClarityVersion) { // execute the program, iterating at each ";; Returns" comment // there are maybe more rust-y ways of doing this, but this is the simplest. let mut segments = vec![]; @@ -3256,7 +3254,7 @@ mod test { &contract_id, &whole_contract, &mut (), - ClarityVersion::latest(), + version, StacksEpochId::latest(), ) .unwrap() @@ -3268,7 +3266,7 @@ mod test { &mut analysis_db, false, &StacksEpochId::latest(), - &ClarityVersion::latest(), + &version, ) .expect("Failed to type check"); } @@ -3281,7 +3279,7 @@ mod test { &contract_id, &total_example, &mut (), - ClarityVersion::latest(), + version, StacksEpochId::latest(), ) .unwrap() @@ -3294,7 +3292,7 @@ mod test { &mut analysis_db, false, &StacksEpochId::latest(), - &ClarityVersion::latest(), + &version, ) .expect("Failed to type check"); type_results.push( @@ -3308,8 +3306,7 @@ mod test { } let conn = store.as_docs_clarity_db(); - let mut contract_context = - ContractContext::new(contract_id.clone(), ClarityVersion::latest()); + let mut contract_context = ContractContext::new(contract_id.clone(), version); let mut global_context = GlobalContext::new( false, CHAIN_ID_TESTNET, @@ -3385,7 +3382,7 @@ mod test { let mut store = MemoryBackingStore::new(); // first, load the samples for contract-call - // and give the doc environment's contract some STX + // and give the doc environment sender and its contract some STX { let contract_id = QualifiedContractIdentifier::local("tokens").unwrap(); let trait_def_id = QualifiedContractIdentifier::parse( @@ -3440,6 +3437,7 @@ mod test { } let conn = store.as_docs_clarity_db(); + let sender_principal = PrincipalData::Standard(StandardPrincipalData::transient()); let docs_test_id = QualifiedContractIdentifier::local("docs-test").unwrap(); let docs_principal_id = PrincipalData::Contract(docs_test_id); let mut env = OwnedEnvironment::new(conn, StacksEpochId::latest()); @@ -3454,6 +3452,13 @@ mod test { .database .get_stx_balance_snapshot_genesis(&docs_principal_id) .unwrap(); + snapshot.set_balance(balance.clone()); + snapshot.save().unwrap(); + let mut snapshot = e + .global_context + .database + .get_stx_balance_snapshot_genesis(&sender_principal) + .unwrap(); snapshot.set_balance(balance); snapshot.save().unwrap(); e.global_context @@ -3479,7 +3484,11 @@ mod test { .collect::>() .join("\n"); let the_throws = example.lines().filter(|x| x.contains(";; Throws")); - docs_execute(&mut store, &without_throws); + docs_execute( + &mut store, + &without_throws, + func_api.max_version.unwrap_or(ClarityVersion::latest()), + ); for expect_err in the_throws { eprintln!("{expect_err}"); execute(expect_err).unwrap_err(); diff --git a/clarity/src/vm/functions/post_conditions.rs b/clarity/src/vm/functions/post_conditions.rs index a6cf691630..9b8691d13d 100644 --- a/clarity/src/vm/functions/post_conditions.rs +++ b/clarity/src/vm/functions/post_conditions.rs @@ -13,36 +13,39 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +use std::collections::{HashMap, HashSet}; + +use clarity_types::types::{AssetIdentifier, PrincipalData}; + +use crate::vm::contexts::AssetMap; use crate::vm::costs::cost_functions::ClarityCostFunction; -use crate::vm::costs::runtime_cost; +use crate::vm::costs::{constants as cost_constants, runtime_cost, CostTracker}; use crate::vm::errors::{ check_arguments_at_least, CheckErrors, InterpreterError, InterpreterResult, }; use crate::vm::representations::SymbolicExpression; -use crate::vm::types::{QualifiedContractIdentifier, Value}; +use crate::vm::types::Value; use crate::vm::{eval, Environment, LocalContext}; -struct StxAllowance { +pub struct StxAllowance { amount: u128, } -struct FtAllowance { - contract: QualifiedContractIdentifier, - token: String, +pub struct FtAllowance { + asset: AssetIdentifier, amount: u128, } -struct NftAllowance { - contract: QualifiedContractIdentifier, - token: String, +pub struct NftAllowance { + asset: AssetIdentifier, asset_id: Value, } -struct StackingAllowance { +pub struct StackingAllowance { amount: u128, } -enum Allowance { +pub enum Allowance { Stx(StxAllowance), Ft(FtAllowance), Nft(NftAllowance), @@ -51,12 +54,100 @@ enum Allowance { } fn eval_allowance( - _allowance_expr: &SymbolicExpression, - _env: &mut Environment, - _context: &LocalContext, + allowance_expr: &SymbolicExpression, + env: &mut Environment, + context: &LocalContext, ) -> InterpreterResult { - // FIXME: Placeholder - Ok(Allowance::All) + let list = allowance_expr + .match_list() + .ok_or(CheckErrors::NonFunctionApplication)?; + let (name_expr, rest) = list + .split_first() + .ok_or(CheckErrors::NonFunctionApplication)?; + let name = name_expr.match_atom().ok_or(CheckErrors::BadFunctionName)?; + + match name.as_str() { + "with-stx" => { + if rest.len() != 1 { + return Err(CheckErrors::IncorrectArgumentCount(1, rest.len()).into()); + } + let amount = eval(&rest[0], env, context)?; + let amount = amount.expect_u128()?; + Ok(Allowance::Stx(StxAllowance { amount })) + } + "with-ft" => { + if rest.len() != 3 { + return Err(CheckErrors::IncorrectArgumentCount(3, rest.len()).into()); + } + + let contract_value = eval(&rest[0], env, context)?; + let contract = contract_value.clone().expect_principal()?; + let contract_identifier = match contract { + PrincipalData::Standard(_) => { + return Err( + CheckErrors::ExpectedContractPrincipalValue(contract_value.into()).into(), + ); + } + PrincipalData::Contract(c) => c, + }; + + let asset_name = eval(&rest[1], env, context)?; + let asset_name = asset_name.expect_string_ascii()?.as_str().into(); + + let asset = AssetIdentifier { + contract_identifier, + asset_name, + }; + + let amount = eval(&rest[2], env, context)?; + let amount = amount.expect_u128()?; + + Ok(Allowance::Ft(FtAllowance { asset, amount })) + } + "with-nft" => { + if rest.len() != 3 { + return Err(CheckErrors::IncorrectArgumentCount(3, rest.len()).into()); + } + + let contract_value = eval(&rest[0], env, context)?; + let contract = contract_value.clone().expect_principal()?; + let contract_identifier = match contract { + PrincipalData::Standard(_) => { + return Err( + CheckErrors::ExpectedContractPrincipalValue(contract_value.into()).into(), + ); + } + PrincipalData::Contract(c) => c, + }; + + let asset_name = eval(&rest[1], env, context)?; + let asset_name = asset_name.expect_string_ascii()?.as_str().into(); + + let asset = AssetIdentifier { + contract_identifier, + asset_name, + }; + + let asset_id = eval(&rest[1], env, context)?; + + Ok(Allowance::Nft(NftAllowance { asset, asset_id })) + } + "with-stacking" => { + if rest.len() != 1 { + return Err(CheckErrors::IncorrectArgumentCount(1, rest.len()).into()); + } + let amount = eval(&rest[0], env, context)?; + let amount = amount.expect_u128()?; + Ok(Allowance::Stacking(StackingAllowance { amount })) + } + "with-all-assets-unsafe" => { + if !rest.is_empty() { + return Err(CheckErrors::IncorrectArgumentCount(1, rest.len()).into()); + } + Ok(Allowance::All) + } + _ => Err(CheckErrors::ExpectedAllowanceExpr(name.to_string()).into()), + } } /// Handles the function `restrict-assets?` @@ -80,7 +171,8 @@ pub fn special_restrict_assets( ))?; let body_exprs = &args[2..]; - let _asset_owner = eval(asset_owner_expr, env, context)?; + let asset_owner = eval(asset_owner_expr, env, context)?; + let asset_owner = asset_owner.expect_principal()?; runtime_cost( ClarityCostFunction::RestrictAssets, @@ -104,12 +196,24 @@ pub fn special_restrict_assets( last_result.replace(result); } - // TODO: Check the post-conditions and rollback if they are violated + let asset_maps = env.global_context.get_readonly_asset_map()?; + + // If the allowances are violated: + // - Rollback the context + // - Emit an event + if let Some(violation_index) = check_allowances(&asset_owner, &allowances, asset_maps)? { + env.global_context.roll_back()?; + // TODO: Emit an event about the allowance violation + return Value::error(Value::Int(violation_index)); + } env.global_context.commit()?; - // last_result should always be Some(...), because of the arg len check above. - last_result.ok_or_else(|| InterpreterError::Expect("Failed to get let result".into()).into()) + // Wrap the result in an `ok` value + Value::okay( + // last_result should always be Some(...), because of the arg len check above. + last_result.ok_or_else(|| InterpreterError::Expect("Failed to get let result".into()))?, + ) } /// Handles the function `as-contract?` @@ -142,23 +246,166 @@ pub fn special_as_contract( allowances.push(eval_allowance(allowance, env, context)?); } - // Create a new evaluation context, so that we can rollback if the - // post-conditions are violated - env.global_context.begin(); + let mut memory_use = 0; - // evaluate the body expressions - let mut last_result = None; - for expr in body_exprs { - let result = eval(expr, env, context)?; - last_result.replace(result); + finally_drop_memory!( env, memory_use; { + env.add_memory(cost_constants::AS_CONTRACT_MEMORY)?; + memory_use += cost_constants::AS_CONTRACT_MEMORY; + + let contract_principal: PrincipalData = env.contract_context.contract_identifier.clone().into(); + let mut nested_env = env.nest_as_principal(contract_principal.clone()); + + // Create a new evaluation context, so that we can rollback if the + // post-conditions are violated + nested_env.global_context.begin(); + + // evaluate the body expressions + let mut last_result = None; + for expr in body_exprs { + let result = eval(expr, &mut nested_env, context)?; + last_result.replace(result); + } + + let asset_maps = nested_env.global_context.get_readonly_asset_map()?; + + // If the allowances are violated: + // - Rollback the context + // - Emit an event + if let Some(violation_index) = check_allowances(&contract_principal, &allowances, asset_maps)? { + nested_env.global_context.roll_back()?; + // TODO: Emit an event about the allowance violation + return Value::error(Value::Int(violation_index)); + } + + nested_env.global_context.commit()?; + + // Wrap the result in an `ok` value + Value::okay( + // last_result should always be Some(...), because of the arg len check above. + last_result.ok_or_else(|| InterpreterError::Expect("Failed to get let result".into()))?, + ) + }) +} + +/// Check the allowances against the asset map. If any assets moved without a +/// corresponding allowance return a `Some` with an index of the violated +/// allowance, or -1 if an asset with no allowance caused the violation. If all +/// allowances are satisfied, return `Ok(None)`. +fn check_allowances( + owner: &PrincipalData, + allowances: &[Allowance], + assets: &AssetMap, +) -> InterpreterResult> { + // Elements are (index in allowances, amount) + let mut stx_allowances: Vec<(usize, u128)> = Vec::new(); + // Map assets to a vector of (index in allowances, amount) + let mut ft_allowances: HashMap<&AssetIdentifier, Vec<(usize, u128)>> = HashMap::new(); + // Map assets to a tuple with the first allowance's index and a hashset of + // serialized asset identifiers + let mut nft_allowances: HashMap<&AssetIdentifier, (usize, HashSet)> = HashMap::new(); + // Elements are (index in allowances, amount) + let mut stacking_allowances: Vec<(usize, u128)> = Vec::new(); + + for (i, allowance) in allowances.iter().enumerate() { + match allowance { + Allowance::All => { + // any asset movement is allowed + return Ok(None); + } + Allowance::Stx(stx) => { + stx_allowances.push((i, stx.amount)); + } + Allowance::Ft(ft) => { + ft_allowances + .entry(&ft.asset) + .or_default() + .push((i, ft.amount)); + } + Allowance::Nft(nft) => { + let (_, set) = nft_allowances + .entry(&nft.asset) + .or_insert_with(|| (i, HashSet::new())); + set.insert(nft.asset_id.serialize_to_hex()?); + } + Allowance::Stacking(stacking) => { + stacking_allowances.push((i, stacking.amount)); + } + } } - // TODO: Check the post-conditions and rollback if they are violated + // Check STX movements + if let Some(stx_moved) = assets.get_stx(owner) { + // If there are no allowances for STX, any movement is a violation + if stx_allowances.is_empty() { + return Ok(Some(-1)); + } - env.global_context.commit()?; + // Check against the STX allowances + for (index, allowance) in &stx_allowances { + if stx_moved > *allowance { + return Ok(Some(i128::try_from(*index).map_err(|_| { + InterpreterError::Expect("failed to convert index to i128".into()) + })?)); + } + } + } + + // Check STX burns + if let Some(stx_burned) = assets.get_stx_burned(owner) { + // If there are no allowances for STX, any burn is a violation + if stx_allowances.is_empty() { + return Ok(Some(-1)); + } + + // Check against the STX allowances + for (index, allowance) in &stx_allowances { + if stx_burned > *allowance { + return Ok(Some(i128::try_from(*index).map_err(|_| { + InterpreterError::Expect("failed to convert index to i128".into()) + })?)); + } + } + } + + // Check FT movements + if let Some(ft_moved) = assets.get_all_fungible_tokens(owner) { + for (asset, amount_moved) in ft_moved { + if let Some(allowance_vec) = ft_allowances.get(asset) { + // Check against the FT allowances + for (index, allowance) in allowance_vec { + if *amount_moved > *allowance { + return Ok(Some(i128::try_from(*index).map_err(|_| { + InterpreterError::Expect("failed to convert index to i128".into()) + })?)); + } + } + } else { + // No allowance for this asset, any movement is a violation + return Ok(Some(-1)); + } + } + } + + // Check NFT movements + if let Some(nft_moved) = assets.get_all_nonfungible_tokens(owner) { + for (asset, ids_moved) in nft_moved { + if let Some((index, allowance_map)) = nft_allowances.get(asset) { + // Check against the NFT allowances + for id_moved in ids_moved { + if !allowance_map.contains(&id_moved.serialize_to_hex()?) { + return Ok(Some(i128::try_from(*index).map_err(|_| { + InterpreterError::Expect("failed to convert index to i128".into()) + })?)); + } + } + } else { + // No allowance for this asset, any movement is a violation + return Ok(Some(-1)); + } + } + } - // last_result should always be Some(...), because of the arg len check above. - last_result.ok_or_else(|| InterpreterError::Expect("Failed to get let result".into()).into()) + Ok(None) } /// Handles all allowance functions, always returning an error, since these are diff --git a/clarity/src/vm/tests/mod.rs b/clarity/src/vm/tests/mod.rs index a10aa7b128..91c05eb936 100644 --- a/clarity/src/vm/tests/mod.rs +++ b/clarity/src/vm/tests/mod.rs @@ -30,6 +30,8 @@ mod contracts; mod conversions; mod datamaps; mod defines; +#[cfg(test)] +mod post_conditions; mod principals; #[cfg(test)] mod representations; diff --git a/clarity/src/vm/tests/post_conditions.rs b/clarity/src/vm/tests/post_conditions.rs new file mode 100644 index 0000000000..4b55f55b94 --- /dev/null +++ b/clarity/src/vm/tests/post_conditions.rs @@ -0,0 +1,406 @@ +// Copyright (C) 2025 Stacks Open Internet Foundation +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . + +use clarity_types::errors::InterpreterResult; +use clarity_types::types::{PrincipalData, QualifiedContractIdentifier, StandardPrincipalData}; +use clarity_types::Value; +use stacks_common::types::StacksEpochId; + +use crate::vm::ast::ASTRules; +use crate::vm::database::STXBalance; +use crate::vm::{execute_with_parameters_and_call_in_global_context, ClarityVersion}; + +fn execute(snippet: &str) -> InterpreterResult> { + execute_with_parameters_and_call_in_global_context( + snippet, + ClarityVersion::Clarity4, + StacksEpochId::Epoch33, + ASTRules::PrecheckSize, + false, + |g| { + // Setup initial balances for the sender and the contract + let sender_principal = PrincipalData::Standard(StandardPrincipalData::transient()); + let contract_id = QualifiedContractIdentifier::transient(); + let contract_principal = PrincipalData::Contract(contract_id); + let balance = STXBalance::initial(1000); + let mut snapshot = g + .database + .get_stx_balance_snapshot_genesis(&sender_principal) + .unwrap(); + snapshot.set_balance(balance.clone()); + snapshot.save().unwrap(); + let mut snapshot = g + .database + .get_stx_balance_snapshot_genesis(&contract_principal) + .unwrap(); + snapshot.set_balance(balance); + snapshot.save().unwrap(); + g.database.increment_ustx_liquid_supply(2000).unwrap(); + Ok(()) + }, + ) +} + +// ---------- Tests for as-contract? ---------- + +#[test] +fn test_as_contract_with_stx_ok() { + let snippet = r#" +(let ((recipient tx-sender)) + (as-contract? ((with-stx u100)) + (try! (stx-transfer? u50 tx-sender recipient)) + ) +)"#; + let expected = Value::okay_true(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_as_contract_with_stx_exceeds() { + let snippet = r#" +(let ((recipient tx-sender)) + (as-contract? ((with-stx u10)) + (try! (stx-transfer? u50 tx-sender recipient)) + ) +)"#; + let expected = Value::error(Value::Int(0)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_as_contract_with_stx_no_allowance() { + let snippet = r#" +(let ((recipient tx-sender)) + (as-contract? () + (try! (stx-transfer? u50 tx-sender recipient)) + ) +)"#; + let expected = Value::error(Value::Int(-1)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_as_contract_stx_all() { + let snippet = r#" +(let ((recipient tx-sender)) + (as-contract? ((with-all-assets-unsafe)) + (try! (stx-transfer? u50 tx-sender recipient)) + ) +)"#; + let expected = Value::okay_true(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_as_contract_stx_other_allowances() { + let snippet = r#" +(let ((recipient tx-sender)) + (as-contract? ((with-ft .token "stackaroo" u100) (with-nft .token "stackaroo" 123)) + (try! (stx-transfer? u50 tx-sender recipient)) + ) +)"#; + let expected = Value::error(Value::Int(-1)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_as_contract_with_stx_burn_ok() { + let snippet = r#" +(as-contract? ((with-stx u100)) + (try! (stx-burn? u50 tx-sender)) +)"#; + let expected = Value::okay_true(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_as_contract_with_stx_burn_exceeds() { + let snippet = r#" +(as-contract? ((with-stx u10)) + (try! (stx-burn? u50 tx-sender)) +)"#; + let expected = Value::error(Value::Int(0)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_as_contract_with_stx_burn_no_allowance() { + let snippet = r#" +(as-contract? () + (try! (stx-burn? u50 tx-sender)) +)"#; + let expected = Value::error(Value::Int(-1)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_as_contract_stx_burn_all() { + let snippet = r#" +(as-contract? ((with-all-assets-unsafe)) + (try! (stx-burn? u50 tx-sender)) +)"#; + let expected = Value::okay_true(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_as_contract_stx_burn_other_allowances() { + let snippet = r#" +(as-contract? ((with-ft .token "stackaroo" u100) (with-nft .token "stackaroo" 123)) + (try! (stx-burn? u50 tx-sender)) +)"#; + let expected = Value::error(Value::Int(-1)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_as_contract_multiple_allowances_both_low() { + let snippet = r#" +(let ((recipient tx-sender)) + (as-contract? ((with-stx u30) (with-stx u20)) + (try! (stx-transfer? u40 tx-sender recipient)) + ) +)"#; + let expected = Value::error(Value::Int(0)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_as_contract_multiple_allowances_both_ok() { + let snippet = r#" +(let ((recipient tx-sender)) + (as-contract? ((with-stx u300) (with-stx u200)) + (try! (stx-transfer? u40 tx-sender recipient)) + ) +)"#; + let expected = Value::okay_true(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_as_contract_multiple_allowances_one_low() { + let snippet = r#" +(let ((recipient tx-sender)) + (as-contract? ((with-stx u100) (with-stx u20)) + (try! (stx-transfer? u40 tx-sender recipient)) + ) +)"#; + let expected = Value::error(Value::Int(1)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +// #[test] +// fn test_as_contract_with_stacking_delegate_ok() { +// let snippet = r#" +// (as-contract? ((with-stacking u2000)) +// (try! (contract-call? 'SP000000000000000000002Q6VF78.pox-4 delegate-stx +// u1000 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM none none +// )) +// )"#; +// let expected = Value::okay_true(); +// assert_eq!(expected, execute(snippet).unwrap().unwrap()); +// } + +// #[test] +// fn test_as_contract_with_stacking_stack_ok() { +// let snippet = r#" +// (as-contract? ((with-stacking u100)) +// (try! (contract-call? 'SP000000000000000000002Q6VF78.pox-4 stack-stx +// u1100000000000 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM none none +// )) +// )"#; +// let expected = Value::okay_true(); +// assert_eq!(expected, execute(snippet).unwrap().unwrap()); +// } + +// #[test] +// fn test_as_contract_with_stacking_exceeds() { +// let snippet = r#" +// (as-contract? ((with-stacking u10)) +// (try! (contract-call? 'SP000000000000000000002Q6VF78.pox-4 delegate-stx +// u1000 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM none none +// )) +// )"#; +// let expected = Value::error(Value::Int(0)).unwrap(); +// assert_eq!(expected, execute(snippet).unwrap().unwrap()); +// } + +// ---------- Tests for restrict-assets? ---------- + +#[test] +fn test_restrict_assets_with_stx_ok() { + let snippet = r#" +(restrict-assets? tx-sender ((with-stx u100)) + (try! (stx-transfer? u50 tx-sender 'SP000000000000000000002Q6VF78)) +)"#; + let expected = Value::okay_true(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_restrict_assets_with_stx_exceeds() { + let snippet = r#" +(restrict-assets? tx-sender ((with-stx u10)) + (try! (stx-transfer? u50 tx-sender 'SP000000000000000000002Q6VF78)) +)"#; + let expected = Value::error(Value::Int(0)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_restrict_assets_with_stx_no_allowance() { + let snippet = r#" +(restrict-assets? tx-sender () + (try! (stx-transfer? u50 tx-sender 'SP000000000000000000002Q6VF78)) +)"#; + let expected = Value::error(Value::Int(-1)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_restrict_assets_stx_all() { + let snippet = r#" +(restrict-assets? tx-sender ((with-all-assets-unsafe)) + (try! (stx-transfer? u50 tx-sender 'SP000000000000000000002Q6VF78)) +)"#; + let expected = Value::okay_true(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_restrict_assets_stx_other_allowances() { + let snippet = r#" +(restrict-assets? tx-sender ((with-ft .token "stackaroo" u100) (with-nft .token "stackaroo" 123)) + (try! (stx-transfer? u50 tx-sender 'SP000000000000000000002Q6VF78)) +)"#; + let expected = Value::error(Value::Int(-1)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_restrict_assets_with_stx_burn_ok() { + let snippet = r#" +(restrict-assets? tx-sender ((with-stx u100)) + (try! (stx-burn? u50 tx-sender)) +)"#; + let expected = Value::okay_true(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_restrict_assets_with_stx_burn_exceeds() { + let snippet = r#" +(restrict-assets? tx-sender ((with-stx u10)) + (try! (stx-burn? u50 tx-sender)) +)"#; + let expected = Value::error(Value::Int(0)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_restrict_assets_with_stx_burn_no_allowance() { + let snippet = r#" +(restrict-assets? tx-sender () + (try! (stx-burn? u50 tx-sender)) +)"#; + let expected = Value::error(Value::Int(-1)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_restrict_assets_stx_burn_all() { + let snippet = r#" +(restrict-assets? tx-sender ((with-all-assets-unsafe)) + (try! (stx-burn? u50 tx-sender)) +)"#; + let expected = Value::okay_true(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_restrict_assets_stx_burn_other_allowances() { + let snippet = r#" +(restrict-assets? tx-sender ((with-ft .token "stackaroo" u100) (with-nft .token "stackaroo" 123)) + (try! (stx-burn? u50 tx-sender)) +)"#; + let expected = Value::error(Value::Int(-1)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_restrict_assets_multiple_allowances_both_low() { + let snippet = r#" +(restrict-assets? tx-sender ((with-stx u30) (with-stx u20)) + (try! (stx-transfer? u40 tx-sender 'SP000000000000000000002Q6VF78)) +)"#; + let expected = Value::error(Value::Int(0)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_restrict_assets_multiple_allowances_both_ok() { + let snippet = r#" +(restrict-assets? tx-sender ((with-stx u300) (with-stx u200)) + (try! (stx-transfer? u40 tx-sender 'SP000000000000000000002Q6VF78)) +)"#; + let expected = Value::okay_true(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_restrict_assets_multiple_allowances_one_low() { + let snippet = r#" +(restrict-assets? tx-sender ((with-stx u100) (with-stx u20)) + (try! (stx-transfer? u40 tx-sender 'SP000000000000000000002Q6VF78)) +)"#; + let expected = Value::error(Value::Int(1)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +// #[test] +// fn test_restrict_assets_with_stacking_delegate_ok() { +// let snippet = r#" +// (restrict-assets? tx-sender ((with-stacking u2000)) +// (try! (contract-call? 'SP000000000000000000002Q6VF78.pox-4 delegate-stx +// u1000 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM none none +// )) +// )"#; +// let expected = Value::okay_true(); +// assert_eq!(expected, execute(snippet).unwrap().unwrap()); +// } + +// #[test] +// fn test_restrict_assets_with_stacking_stack_ok() { +// let snippet = r#" +// (restrict-assets? tx-sender ((with-stacking u100)) +// (try! (contract-call? 'SP000000000000000000002Q6VF78.pox-4 stack-stx +// u1100000000000 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM none none +// )) +// )"#; +// let expected = Value::okay_true(); +// assert_eq!(expected, execute(snippet).unwrap().unwrap()); +// } + +// #[test] +// fn test_restrict_assets_with_stacking_exceeds() { +// let snippet = r#" +// (restrict-assets? tx-sender ((with-stacking u10)) +// (try! (contract-call? 'SP000000000000000000002Q6VF78.pox-4 delegate-stx +// u1000 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM none none +// )) +// )"#; +// let expected = Value::error(Value::Int(0)).unwrap(); +// assert_eq!(expected, execute(snippet).unwrap().unwrap()); +// } From 0a141ae92357de4e1da92184b7cb4a18150688ed Mon Sep 17 00:00:00 2001 From: Brice Dobry Date: Sun, 21 Sep 2025 09:19:35 -0400 Subject: [PATCH 06/35] fix: remove ASTRules --- clarity/src/vm/tests/post_conditions.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/clarity/src/vm/tests/post_conditions.rs b/clarity/src/vm/tests/post_conditions.rs index 4b55f55b94..e56ade010e 100644 --- a/clarity/src/vm/tests/post_conditions.rs +++ b/clarity/src/vm/tests/post_conditions.rs @@ -18,7 +18,6 @@ use clarity_types::types::{PrincipalData, QualifiedContractIdentifier, StandardP use clarity_types::Value; use stacks_common::types::StacksEpochId; -use crate::vm::ast::ASTRules; use crate::vm::database::STXBalance; use crate::vm::{execute_with_parameters_and_call_in_global_context, ClarityVersion}; @@ -27,7 +26,6 @@ fn execute(snippet: &str) -> InterpreterResult> { snippet, ClarityVersion::Clarity4, StacksEpochId::Epoch33, - ASTRules::PrecheckSize, false, |g| { // Setup initial balances for the sender and the contract From 7e8698fa0814e8c03a5b8e94eb5526a352306e92 Mon Sep 17 00:00:00 2001 From: Brice Dobry Date: Sun, 21 Sep 2025 09:54:05 -0400 Subject: [PATCH 07/35] fix: error in docs example --- clarity/src/vm/docs/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clarity/src/vm/docs/mod.rs b/clarity/src/vm/docs/mod.rs index a221064081..657761ff67 100644 --- a/clarity/src/vm/docs/mod.rs +++ b/clarity/src/vm/docs/mod.rs @@ -2662,7 +2662,7 @@ the contract. When `"*"` is used for the token name, the allowance applies to (define-fungible-token stackaroo) (ft-mint? stackaroo u200 tx-sender) (restrict-assets? tx-sender - ((with-ft current-contract "stackaroo" u50)) + ((with-ft current-contract "stackaroo" u100)) (try! (ft-transfer? stackaroo u100 tx-sender 'SPAXYA5XS51713FDTQ8H94EJ4V579CXMTRNBZKSF)) ) ;; Returns (ok true) (restrict-assets? tx-sender From 204e84874a8e0d1314e18ba7e9d7cf91cdb2556d Mon Sep 17 00:00:00 2001 From: Brice Dobry Date: Mon, 22 Sep 2025 09:51:52 -0400 Subject: [PATCH 08/35] fix: copy/paste error --- clarity/src/vm/functions/post_conditions.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/clarity/src/vm/functions/post_conditions.rs b/clarity/src/vm/functions/post_conditions.rs index 9b8691d13d..f5081d0e6b 100644 --- a/clarity/src/vm/functions/post_conditions.rs +++ b/clarity/src/vm/functions/post_conditions.rs @@ -128,7 +128,7 @@ fn eval_allowance( asset_name, }; - let asset_id = eval(&rest[1], env, context)?; + let asset_id = eval(&rest[2], env, context)?; Ok(Allowance::Nft(NftAllowance { asset, asset_id })) } @@ -368,6 +368,7 @@ fn check_allowances( } // Check FT movements + // TODO: Handle "*" asset name if let Some(ft_moved) = assets.get_all_fungible_tokens(owner) { for (asset, amount_moved) in ft_moved { if let Some(allowance_vec) = ft_allowances.get(asset) { @@ -387,6 +388,7 @@ fn check_allowances( } // Check NFT movements + // TODO: Handle "*" asset name if let Some(nft_moved) = assets.get_all_nonfungible_tokens(owner) { for (asset, ids_moved) in nft_moved { if let Some((index, allowance_map)) = nft_allowances.get(asset) { From 771262c742023ab49c65b94eb444bde009af61ae Mon Sep 17 00:00:00 2001 From: Brice Dobry Date: Mon, 22 Sep 2025 11:28:22 -0400 Subject: [PATCH 09/35] feat: update `with-nft` to support list of identifiers --- clarity-types/src/errors/analysis.rs | 4 +++ .../v2_1/natives/post_conditions.rs | 12 +++++++-- .../v2_1/tests/post_conditions.rs | 26 +++++++++---------- clarity/src/vm/docs/mod.rs | 14 +++++----- clarity/src/vm/functions/post_conditions.rs | 11 +++++--- clarity/src/vm/tests/post_conditions.rs | 8 +++--- 6 files changed, 45 insertions(+), 30 deletions(-) diff --git a/clarity-types/src/errors/analysis.rs b/clarity-types/src/errors/analysis.rs index 8573d91882..1b16162107 100644 --- a/clarity-types/src/errors/analysis.rs +++ b/clarity-types/src/errors/analysis.rs @@ -314,6 +314,8 @@ pub enum CheckErrors { ExpectedAllowanceExpr(String), WithAllAllowanceNotAllowed, WithAllAllowanceNotAlone, + WithNftExpectedListOfIdentifiers, + MaxIdentifierLengthExceeded(u32), } #[derive(Debug, PartialEq)] @@ -617,6 +619,8 @@ impl DiagnosableError for CheckErrors { CheckErrors::ExpectedAllowanceExpr(got_name) => format!("expected an allowance expression, got: {got_name}"), CheckErrors::WithAllAllowanceNotAllowed => "with-all-assets-unsafe is not allowed here, only in the allowance list for `as-contract?`".into(), CheckErrors::WithAllAllowanceNotAlone => "with-all-assets-unsafe must not be used along with other allowances".into(), + CheckErrors::WithNftExpectedListOfIdentifiers => "with-nft allowance must include a list of asset identifiers".into(), + CheckErrors::MaxIdentifierLengthExceeded(max_len) => format!("with-nft allowance identifiers list must not exceed 128 elements, got {max_len}"), } } diff --git a/clarity/src/vm/analysis/type_checker/v2_1/natives/post_conditions.rs b/clarity/src/vm/analysis/type_checker/v2_1/natives/post_conditions.rs index bf6c0f82d6..8092513bf4 100644 --- a/clarity/src/vm/analysis/type_checker/v2_1/natives/post_conditions.rs +++ b/clarity/src/vm/analysis/type_checker/v2_1/natives/post_conditions.rs @@ -17,7 +17,7 @@ use clarity_types::errors::analysis::{check_argument_count, check_arguments_at_l use clarity_types::errors::{CheckError, CheckErrors}; use clarity_types::representations::SymbolicExpression; use clarity_types::types::signatures::ASCII_128; -use clarity_types::types::TypeSignature; +use clarity_types::types::{SequenceSubtype, TypeSignature}; use crate::vm::analysis::type_checker::contexts::TypingContext; use crate::vm::analysis::type_checker::v2_1::TypeChecker; @@ -202,7 +202,15 @@ fn check_allowance_with_nft( checker.type_check_expects(&args[0], context, &TypeSignature::PrincipalType)?; checker.type_check_expects(&args[1], context, &ASCII_128)?; - // Asset ID can be any type + + // Asset identifiers must be a Clarity list with any type of elements + let id_list_ty = checker.type_check(&args[2], context)?; + let TypeSignature::SequenceType(SequenceSubtype::ListType(list_data)) = id_list_ty else { + return Err(CheckErrors::WithNftExpectedListOfIdentifiers.into()); + }; + if list_data.get_max_len() > 128 { + return Err(CheckErrors::MaxIdentifierLengthExceeded(list_data.get_max_len()).into()); + } Ok(false) } diff --git a/clarity/src/vm/analysis/type_checker/v2_1/tests/post_conditions.rs b/clarity/src/vm/analysis/type_checker/v2_1/tests/post_conditions.rs index 488d341541..b336a136a5 100644 --- a/clarity/src/vm/analysis/type_checker/v2_1/tests/post_conditions.rs +++ b/clarity/src/vm/analysis/type_checker/v2_1/tests/post_conditions.rs @@ -199,7 +199,7 @@ fn test_as_contract(#[case] version: ClarityVersion, #[case] _epoch: StacksEpoch ), // multiple allowances ( - "(as-contract? ((with-stx u1000) (with-ft .token \"foo\" u5000) (with-nft .token \"foo\" 0x01) (with-stacking u1000)) true)", + "(as-contract? ((with-stx u1000) (with-ft .token \"foo\" u5000) (with-nft .token \"foo\" (list 0x01)) (with-stacking u1000)) true)", TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap() ), // multiple body expressions @@ -551,47 +551,47 @@ fn test_with_nft_allowance(#[case] version: ClarityVersion, #[case] _epoch: Stac let good = [ // basic usage with shortcut contract principal ( - r#"(restrict-assets? tx-sender ((with-nft .token "token-name" u1000)) true)"#, + r#"(restrict-assets? tx-sender ((with-nft .token "token-name" (list u1000))) true)"#, TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(), ), // full literal principal ( - r#"(restrict-assets? tx-sender ((with-ft 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.token "token-name" u1000)) true)"#, + r#"(restrict-assets? tx-sender ((with-ft 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.token "token-name" (list u1000))) true)"#, TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(), ), // variable principal ( - r#"(let ((contract .token)) (restrict-assets? tx-sender ((with-nft contract "token-name" u1000)) true))"#, + r#"(let ((contract .token)) (restrict-assets? tx-sender ((with-nft contract "token-name" (list u1000))) true))"#, TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(), ), // variable token name ( - r#"(let ((name "token-name")) (restrict-assets? tx-sender ((with-nft .token name u1000)) true))"#, + r#"(let ((name "token-name")) (restrict-assets? tx-sender ((with-nft .token name (list u1000))) true))"#, TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(), ), // "*" token name ( - r#"(restrict-assets? tx-sender ((with-nft .token "*" u1000)) true)"#, + r#"(restrict-assets? tx-sender ((with-nft .token "*" (list u1000))) true)"#, TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(), ), // empty token name ( - r#"(restrict-assets? tx-sender ((with-nft .token "" u1000)) true)"#, + r#"(restrict-assets? tx-sender ((with-nft .token "" (list u1000))) true)"#, TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(), ), // string asset-id ( - r#"(restrict-assets? tx-sender ((with-nft .token "token-name" "asset-123")) true)"#, + r#"(restrict-assets? tx-sender ((with-nft .token "token-name" (list "asset-123"))) true)"#, TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(), ), // buffer asset-id ( - r#"(restrict-assets? tx-sender ((with-nft .token "token-name" 0x0123456789)) true)"#, + r#"(restrict-assets? tx-sender ((with-nft .token "token-name" (list 0x0123456789))) true)"#, TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(), ), // variable asset-id ( - r#"(let ((asset-id u123)) (restrict-assets? tx-sender ((with-nft .token "token-name" asset-id)) true))"#, + r#"(let ((asset-id (list u123))) (restrict-assets? tx-sender ((with-nft .token "token-name" asset-id)) true))"#, TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(), ), ]; @@ -614,12 +614,12 @@ fn test_with_nft_allowance(#[case] version: ClarityVersion, #[case] _epoch: Stac ), // too many arguments ( - r#"(restrict-assets? tx-sender ((with-nft .token "token-name" u123 u456)) true)"#, + r#"(restrict-assets? tx-sender ((with-nft .token "token-name" (list u123) (list u456))) true)"#, CheckErrors::IncorrectArgumentCount(3, 4), ), // wrong type for contract-id - uint instead of principal ( - r#"(restrict-assets? tx-sender ((with-nft u123 "token-name" u456)) true)"#, + r#"(restrict-assets? tx-sender ((with-nft u123 "token-name" (list u456))) true)"#, CheckErrors::TypeError( TypeSignature::PrincipalType.into(), TypeSignature::UIntType.into(), @@ -627,7 +627,7 @@ fn test_with_nft_allowance(#[case] version: ClarityVersion, #[case] _epoch: Stac ), // wrong type for token-name - uint instead of string ( - "(restrict-assets? tx-sender ((with-nft .token u123 u456)) true)", + "(restrict-assets? tx-sender ((with-nft .token u123 (list u456))) true)", CheckErrors::TypeError( TypeSignature::new_string_ascii(128).unwrap().into(), TypeSignature::UIntType.into(), diff --git a/clarity/src/vm/docs/mod.rs b/clarity/src/vm/docs/mod.rs index 657761ff67..ed1a94140d 100644 --- a/clarity/src/vm/docs/mod.rs +++ b/clarity/src/vm/docs/mod.rs @@ -2673,12 +2673,12 @@ the contract. When `"*"` is used for the token name, the allowance applies to }; const ALLOWANCE_WITH_NFT: SpecialAPI = SpecialAPI { - input_type: "principal (string-ascii 128) T", - snippet: "with-nft ${1:contract-id} ${2:asset-name} ${3:asset-identifier}", + input_type: "principal (string-ascii 128) (list 128 T)", + snippet: "with-nft ${1:contract-id} ${2:asset-name} ${3:asset-identifiers}", output_type: "Allowance", - signature: "(with-nft contract-id asset-name identifier)", - description: r#"Adds an outflow allowance for the non-fungible token -identified by `identifier` defined in `contract-id` with name `token-name` + signature: "(with-nft contract-id asset-name identifiers)", + description: r#"Adds an outflow allowance for the non-fungible tokens +identified by `identifiers` defined in `contract-id` with name `token-name` from the `asset-owner` of the enclosing `restrict-assets?` or `as-contract?` expression. `with-nft` is not allowed outside of `restrict-assets?` or `as-contract?` contexts. Note that `token-name` should match the name used in @@ -2690,11 +2690,11 @@ the token name, the allowance applies to **all** NFTs defined in `contract-id`." (nft-mint? stackaroo u124 tx-sender) (nft-mint? stackaroo u125 tx-sender) (restrict-assets? tx-sender - ((with-nft current-contract "stackaroo" u123)) + ((with-nft current-contract "stackaroo" (list u123))) (try! (nft-transfer? stackaroo u123 tx-sender 'SPAXYA5XS51713FDTQ8H94EJ4V579CXMTRNBZKSF)) ) ;; Returns (ok true) (restrict-assets? tx-sender - ((with-nft current-contract "stackaroo" u125)) + ((with-nft current-contract "stackaroo" (list u125))) (try! (nft-transfer? stackaroo u124 tx-sender 'SPAXYA5XS51713FDTQ8H94EJ4V579CXMTRNBZKSF)) ) ;; Returns (err 0) "#, diff --git a/clarity/src/vm/functions/post_conditions.rs b/clarity/src/vm/functions/post_conditions.rs index f5081d0e6b..b04203d12b 100644 --- a/clarity/src/vm/functions/post_conditions.rs +++ b/clarity/src/vm/functions/post_conditions.rs @@ -38,7 +38,7 @@ pub struct FtAllowance { pub struct NftAllowance { asset: AssetIdentifier, - asset_id: Value, + asset_ids: Vec, } pub struct StackingAllowance { @@ -128,9 +128,10 @@ fn eval_allowance( asset_name, }; - let asset_id = eval(&rest[2], env, context)?; + let asset_id_list = eval(&rest[2], env, context)?; + let asset_ids = asset_id_list.expect_list()?; - Ok(Allowance::Nft(NftAllowance { asset, asset_id })) + Ok(Allowance::Nft(NftAllowance { asset, asset_ids })) } "with-stacking" => { if rest.len() != 1 { @@ -325,7 +326,9 @@ fn check_allowances( let (_, set) = nft_allowances .entry(&nft.asset) .or_insert_with(|| (i, HashSet::new())); - set.insert(nft.asset_id.serialize_to_hex()?); + for id in &nft.asset_ids { + set.insert(id.serialize_to_hex()?); + } } Allowance::Stacking(stacking) => { stacking_allowances.push((i, stacking.amount)); diff --git a/clarity/src/vm/tests/post_conditions.rs b/clarity/src/vm/tests/post_conditions.rs index e56ade010e..7ed32fdfd2 100644 --- a/clarity/src/vm/tests/post_conditions.rs +++ b/clarity/src/vm/tests/post_conditions.rs @@ -105,7 +105,7 @@ fn test_as_contract_stx_all() { fn test_as_contract_stx_other_allowances() { let snippet = r#" (let ((recipient tx-sender)) - (as-contract? ((with-ft .token "stackaroo" u100) (with-nft .token "stackaroo" 123)) + (as-contract? ((with-ft .token "stackaroo" u100) (with-nft .token "stackaroo" (list 123))) (try! (stx-transfer? u50 tx-sender recipient)) ) )"#; @@ -156,7 +156,7 @@ fn test_as_contract_stx_burn_all() { #[test] fn test_as_contract_stx_burn_other_allowances() { let snippet = r#" -(as-contract? ((with-ft .token "stackaroo" u100) (with-nft .token "stackaroo" 123)) +(as-contract? ((with-ft .token "stackaroo" u100) (with-nft .token "stackaroo" (list 123))) (try! (stx-burn? u50 tx-sender)) )"#; let expected = Value::error(Value::Int(-1)).unwrap(); @@ -280,7 +280,7 @@ fn test_restrict_assets_stx_all() { #[test] fn test_restrict_assets_stx_other_allowances() { let snippet = r#" -(restrict-assets? tx-sender ((with-ft .token "stackaroo" u100) (with-nft .token "stackaroo" 123)) +(restrict-assets? tx-sender ((with-ft .token "stackaroo" u100) (with-nft .token "stackaroo" (list 123))) (try! (stx-transfer? u50 tx-sender 'SP000000000000000000002Q6VF78)) )"#; let expected = Value::error(Value::Int(-1)).unwrap(); @@ -330,7 +330,7 @@ fn test_restrict_assets_stx_burn_all() { #[test] fn test_restrict_assets_stx_burn_other_allowances() { let snippet = r#" -(restrict-assets? tx-sender ((with-ft .token "stackaroo" u100) (with-nft .token "stackaroo" 123)) +(restrict-assets? tx-sender ((with-ft .token "stackaroo" u100) (with-nft .token "stackaroo" (list 123))) (try! (stx-burn? u50 tx-sender)) )"#; let expected = Value::error(Value::Int(-1)).unwrap(); From b450cd18fa00dc6b883653e0b715fbb181c6b745 Mon Sep 17 00:00:00 2001 From: Brice Dobry Date: Mon, 22 Sep 2025 11:28:43 -0400 Subject: [PATCH 10/35] test: add tests for ft and nft post-conditions --- clarity/src/vm/tests/post_conditions.rs | 510 ++++++++++++++++++++++++ 1 file changed, 510 insertions(+) diff --git a/clarity/src/vm/tests/post_conditions.rs b/clarity/src/vm/tests/post_conditions.rs index 7ed32fdfd2..b142f8e6bc 100644 --- a/clarity/src/vm/tests/post_conditions.rs +++ b/clarity/src/vm/tests/post_conditions.rs @@ -235,6 +235,261 @@ fn test_as_contract_multiple_allowances_one_low() { // assert_eq!(expected, execute(snippet).unwrap().unwrap()); // } +#[test] +fn test_as_contract_with_ft_ok() { + let snippet = r#" +(define-fungible-token stackaroo) +(ft-mint? stackaroo u200 current-contract) +(let ((recipient tx-sender)) + (as-contract? ((with-ft current-contract "stackaroo" u100)) + (try! (ft-transfer? stackaroo u100 tx-sender recipient)) + ) +)"#; + let expected = Value::okay_true(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_as_contract_with_ft_exceeds() { + let snippet = r#" +(define-fungible-token stackaroo) +(ft-mint? stackaroo u200 current-contract) +(let ((recipient tx-sender)) + (as-contract? ((with-ft current-contract "stackaroo" u10)) + (try! (ft-transfer? stackaroo u50 tx-sender recipient)) + ) +)"#; + let expected = Value::error(Value::Int(0)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_as_contract_with_ft_no_allowance() { + let snippet = r#" +(define-fungible-token stackaroo) +(ft-mint? stackaroo u200 current-contract) +(let ((recipient tx-sender)) + (as-contract? () + (try! (ft-transfer? stackaroo u50 tx-sender recipient)) + ) +)"#; + let expected = Value::error(Value::Int(-1)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_as_contract_with_ft_all() { + let snippet = r#" +(define-fungible-token stackaroo) +(ft-mint? stackaroo u200 current-contract) +(let ((recipient tx-sender)) + (as-contract? ((with-all-assets-unsafe)) + (try! (ft-transfer? stackaroo u50 tx-sender recipient)) + ) +)"#; + let expected = Value::okay_true(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_as_contract_with_ft_other_allowances() { + let snippet = r#" +(define-fungible-token stackaroo) +(ft-mint? stackaroo u200 current-contract) +(let ((recipient tx-sender)) + (as-contract? + ( + (with-stx u200) + (with-ft .other "stackaroo" u100) ;; other contract, same token name + (with-ft current-contract "other" u100) ;; same contract, different token name + (with-nft .token "stackaroo" (list 123)) + ) + (try! (ft-transfer? stackaroo u50 tx-sender recipient)) + ) +)"#; + let expected = Value::error(Value::Int(-1)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_as_contract_with_ft_multiple_allowances_both_low() { + let snippet = r#" +(define-fungible-token stackaroo) +(ft-mint? stackaroo u200 current-contract) +(let ((recipient tx-sender)) + (as-contract? ((with-ft current-contract "stackaroo" u30) (with-ft current-contract "stackaroo" u20)) + (try! (ft-transfer? stackaroo u40 tx-sender recipient)) + ) +)"#; + let expected = Value::error(Value::Int(0)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_as_contract_with_ft_multiple_allowances_both_ok() { + let snippet = r#" +(define-fungible-token stackaroo) +(ft-mint? stackaroo u200 current-contract) +(let ((recipient tx-sender)) + (as-contract? ((with-ft current-contract "stackaroo" u300) (with-ft current-contract "stackaroo" u200)) + (try! (ft-transfer? stackaroo u40 tx-sender recipient)) + ) +)"#; + let expected = Value::okay_true(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_as_contract_with_ft_multiple_allowances_one_low() { + let snippet = r#" +(define-fungible-token stackaroo) +(ft-mint? stackaroo u200 current-contract) +(let ((recipient tx-sender)) + (as-contract? ((with-ft current-contract "stackaroo" u100) (with-ft current-contract "stackaroo" u20)) + (try! (ft-transfer? stackaroo u40 tx-sender recipient)) + ) +)"#; + let expected = Value::error(Value::Int(1)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_as_contract_with_nft_ok() { + let snippet = r#" +(define-non-fungible-token stackaroo uint) +(nft-mint? stackaroo u123 current-contract) +(let ((recipient tx-sender)) + (as-contract? ((with-nft current-contract "stackaroo" (list u123))) + (try! (nft-transfer? stackaroo u123 tx-sender recipient)) + ) +)"#; + let expected = Value::okay_true(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_as_contract_with_nft_not_allowed() { + let snippet = r#" +(define-non-fungible-token stackaroo uint) +(nft-mint? stackaroo u122 current-contract) +(nft-mint? stackaroo u123 current-contract) +(let ((recipient tx-sender)) + (as-contract? ((with-nft current-contract "stackaroo" (list u122))) + (try! (nft-transfer? stackaroo u123 tx-sender recipient)) + ) +)"#; + let expected = Value::error(Value::Int(0)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_as_contract_with_nft_no_allowance() { + let snippet = r#" +(define-non-fungible-token stackaroo uint) +(nft-mint? stackaroo u123 current-contract) +(let ((recipient tx-sender)) + (as-contract? () + (try! (nft-transfer? stackaroo u123 tx-sender recipient)) + ) +)"#; + let expected = Value::error(Value::Int(-1)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_as_contract_with_nft_all() { + let snippet = r#" +(define-non-fungible-token stackaroo uint) +(nft-mint? stackaroo u123 current-contract) +(let ((recipient tx-sender)) + (as-contract? ((with-all-assets-unsafe)) + (try! (nft-transfer? stackaroo u123 tx-sender recipient)) + ) +)"#; + let expected = Value::okay_true(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_as_contract_with_nft_other_allowances() { + let snippet = r#" +(define-non-fungible-token stackaroo uint) +(nft-mint? stackaroo u123 current-contract) +(let ((recipient tx-sender)) + (as-contract? + ( + (with-stx u123) + (with-nft .other "stackaroo" (list u123)) ;; other contract, same token name + (with-nft current-contract "other" (list u123)) ;; same contract, different token name + (with-ft .token "stackaroo" u123) + ) + (try! (nft-transfer? stackaroo u123 tx-sender recipient)) + ) +)"#; + let expected = Value::error(Value::Int(-1)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_as_contract_with_nft_multiple_allowances_both_different() { + let snippet = r#" +(define-non-fungible-token stackaroo uint) +(nft-mint? stackaroo u122 current-contract) +(nft-mint? stackaroo u123 current-contract) +(let ((recipient tx-sender)) + (as-contract? ((with-nft current-contract "stackaroo" (list u122)) (with-nft current-contract "stackaroo" (list u124))) + (try! (nft-transfer? stackaroo u123 tx-sender recipient)) + ) +)"#; + let expected = Value::error(Value::Int(0)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_as_contract_with_nft_multiple_allowances_including() { + let snippet = r#" +(define-non-fungible-token stackaroo uint) +(nft-mint? stackaroo u122 current-contract) +(nft-mint? stackaroo u123 current-contract) +(let ((recipient tx-sender)) + (as-contract? ((with-nft current-contract "stackaroo" (list u122)) (with-nft current-contract "stackaroo" (list u123))) + (try! (nft-transfer? stackaroo u123 tx-sender recipient)) + ) +)"#; + let expected = Value::okay_true(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_as_contract_with_nft_multiple_allowances_in_list() { + let snippet = r#" +(define-non-fungible-token stackaroo uint) +(nft-mint? stackaroo u122 current-contract) +(nft-mint? stackaroo u123 current-contract) +(let ((recipient tx-sender)) + (as-contract? ((with-nft current-contract "stackaroo" (list u122 u123))) + (try! (nft-transfer? stackaroo u123 tx-sender recipient)) + ) +)"#; + let expected = Value::okay_true(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_as_contract_with_nft_empty_id_list() { + let snippet = r#" +(define-non-fungible-token stackaroo uint) +(nft-mint? stackaroo u122 current-contract) +(nft-mint? stackaroo u123 current-contract) +(let ((recipient tx-sender)) + (as-contract? ((with-nft current-contract "stackaroo" (list))) + (try! (nft-transfer? stackaroo u123 tx-sender recipient)) + ) +)"#; + let expected = Value::error(Value::Int(0)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + // ---------- Tests for restrict-assets? ---------- #[test] @@ -402,3 +657,258 @@ fn test_restrict_assets_multiple_allowances_one_low() { // let expected = Value::error(Value::Int(0)).unwrap(); // assert_eq!(expected, execute(snippet).unwrap().unwrap()); // } + +#[test] +fn test_restrict_assets_with_ft_ok() { + let snippet = r#" +(define-fungible-token stackaroo) +(ft-mint? stackaroo u200 tx-sender) +(let ((recipient 'SP000000000000000000002Q6VF78)) + (restrict-assets? tx-sender ((with-ft current-contract "stackaroo" u100)) + (try! (ft-transfer? stackaroo u100 tx-sender recipient)) + ) +)"#; + let expected = Value::okay_true(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_restrict_assets_with_ft_exceeds() { + let snippet = r#" +(define-fungible-token stackaroo) +(ft-mint? stackaroo u200 tx-sender) +(let ((recipient 'SP000000000000000000002Q6VF78)) + (restrict-assets? tx-sender ((with-ft current-contract "stackaroo" u10)) + (try! (ft-transfer? stackaroo u50 tx-sender recipient)) + ) +)"#; + let expected = Value::error(Value::Int(0)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_restrict_assets_with_ft_no_allowance() { + let snippet = r#" +(define-fungible-token stackaroo) +(ft-mint? stackaroo u200 tx-sender) +(let ((recipient 'SP000000000000000000002Q6VF78)) + (restrict-assets? tx-sender () + (try! (ft-transfer? stackaroo u50 tx-sender recipient)) + ) +)"#; + let expected = Value::error(Value::Int(-1)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_restrict_assets_with_ft_all() { + let snippet = r#" +(define-fungible-token stackaroo) +(ft-mint? stackaroo u200 tx-sender) +(let ((recipient 'SP000000000000000000002Q6VF78)) + (restrict-assets? tx-sender ((with-all-assets-unsafe)) + (try! (ft-transfer? stackaroo u50 tx-sender recipient)) + ) +)"#; + let expected = Value::okay_true(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_restrict_assets_with_ft_other_allowances() { + let snippet = r#" +(define-fungible-token stackaroo) +(ft-mint? stackaroo u200 tx-sender) +(let ((recipient 'SP000000000000000000002Q6VF78)) + (restrict-assets? tx-sender + ( + (with-stx u200) + (with-ft .other "stackaroo" u100) ;; other contract, same token name + (with-ft current-contract "other" u100) ;; same contract, different token name + (with-nft .token "stackaroo" (list 123)) + ) + (try! (ft-transfer? stackaroo u50 tx-sender recipient)) + ) +)"#; + let expected = Value::error(Value::Int(-1)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_restrict_assets_with_ft_multiple_allowances_both_low() { + let snippet = r#" +(define-fungible-token stackaroo) +(ft-mint? stackaroo u200 tx-sender) +(let ((recipient 'SP000000000000000000002Q6VF78)) + (restrict-assets? tx-sender ((with-ft current-contract "stackaroo" u30) (with-ft current-contract "stackaroo" u20)) + (try! (ft-transfer? stackaroo u40 tx-sender recipient)) + ) +)"#; + let expected = Value::error(Value::Int(0)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_restrict_assets_with_ft_multiple_allowances_both_ok() { + let snippet = r#" +(define-fungible-token stackaroo) +(ft-mint? stackaroo u200 tx-sender) +(let ((recipient 'SP000000000000000000002Q6VF78)) + (restrict-assets? tx-sender ((with-ft current-contract "stackaroo" u300) (with-ft current-contract "stackaroo" u200)) + (try! (ft-transfer? stackaroo u40 tx-sender recipient)) + ) +)"#; + let expected = Value::okay_true(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_restrict_assets_with_ft_multiple_allowances_one_low() { + let snippet = r#" +(define-fungible-token stackaroo) +(ft-mint? stackaroo u200 tx-sender) +(let ((recipient 'SP000000000000000000002Q6VF78)) + (restrict-assets? tx-sender ((with-ft current-contract "stackaroo" u100) (with-ft current-contract "stackaroo" u20)) + (try! (ft-transfer? stackaroo u40 tx-sender recipient)) + ) +)"#; + let expected = Value::error(Value::Int(1)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_restrict_assets_with_nft_ok() { + let snippet = r#" +(define-non-fungible-token stackaroo uint) +(nft-mint? stackaroo u123 tx-sender) +(let ((recipient 'SP000000000000000000002Q6VF78)) + (restrict-assets? tx-sender ((with-nft current-contract "stackaroo" (list u123))) + (try! (nft-transfer? stackaroo u123 tx-sender recipient)) + ) +)"#; + let expected = Value::okay_true(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_restrict_assets_with_nft_not_allowed() { + let snippet = r#" +(define-non-fungible-token stackaroo uint) +(nft-mint? stackaroo u122 tx-sender) +(nft-mint? stackaroo u123 tx-sender) +(let ((recipient 'SP000000000000000000002Q6VF78)) + (restrict-assets? tx-sender ((with-nft current-contract "stackaroo" (list u122))) + (try! (nft-transfer? stackaroo u123 tx-sender recipient)) + ) +)"#; + let expected = Value::error(Value::Int(0)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_restrict_assets_with_nft_no_allowance() { + let snippet = r#" +(define-non-fungible-token stackaroo uint) +(nft-mint? stackaroo u123 tx-sender) +(let ((recipient 'SP000000000000000000002Q6VF78)) + (restrict-assets? tx-sender () + (try! (nft-transfer? stackaroo u123 tx-sender recipient)) + ) +)"#; + let expected = Value::error(Value::Int(-1)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_restrict_assets_with_nft_all() { + let snippet = r#" +(define-non-fungible-token stackaroo uint) +(nft-mint? stackaroo u123 tx-sender) +(let ((recipient 'SP000000000000000000002Q6VF78)) + (restrict-assets? tx-sender ((with-all-assets-unsafe)) + (try! (nft-transfer? stackaroo u123 tx-sender recipient)) + ) +)"#; + let expected = Value::okay_true(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_restrict_assets_with_nft_other_allowances() { + let snippet = r#" +(define-non-fungible-token stackaroo uint) +(nft-mint? stackaroo u123 tx-sender) +(let ((recipient 'SP000000000000000000002Q6VF78)) + (restrict-assets? tx-sender + ( + (with-stx u123) + (with-nft .other "stackaroo" (list u123)) ;; other contract, same token name + (with-nft current-contract "other" (list u123)) ;; same contract, different token name + (with-ft .token "stackaroo" u123) + ) + (try! (nft-transfer? stackaroo u123 tx-sender recipient)) + ) +)"#; + let expected = Value::error(Value::Int(-1)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_restrict_assets_with_nft_multiple_allowances_both_different() { + let snippet = r#" +(define-non-fungible-token stackaroo uint) +(nft-mint? stackaroo u122 tx-sender) +(nft-mint? stackaroo u123 tx-sender) +(let ((recipient 'SP000000000000000000002Q6VF78)) + (restrict-assets? tx-sender ((with-nft current-contract "stackaroo" (list u122)) (with-nft current-contract "stackaroo" (list u124))) + (try! (nft-transfer? stackaroo u123 tx-sender recipient)) + ) +)"#; + let expected = Value::error(Value::Int(0)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_restrict_assets_with_nft_multiple_allowances_including() { + let snippet = r#" +(define-non-fungible-token stackaroo uint) +(nft-mint? stackaroo u122 tx-sender) +(nft-mint? stackaroo u123 tx-sender) +(let ((recipient 'SP000000000000000000002Q6VF78)) + (restrict-assets? tx-sender ((with-nft current-contract "stackaroo" (list u122)) (with-nft current-contract "stackaroo" (list u123))) + (try! (nft-transfer? stackaroo u123 tx-sender recipient)) + ) +)"#; + let expected = Value::okay_true(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_restrict_assets_with_nft_multiple_allowances_in_list() { + let snippet = r#" +(define-non-fungible-token stackaroo uint) +(nft-mint? stackaroo u122 tx-sender) +(nft-mint? stackaroo u123 tx-sender) +(let ((recipient 'SP000000000000000000002Q6VF78)) + (restrict-assets? tx-sender ((with-nft current-contract "stackaroo" (list u122 u123))) + (try! (nft-transfer? stackaroo u123 tx-sender recipient)) + ) +)"#; + let expected = Value::okay_true(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_restrict_assets_with_nft_empty_id_list() { + let snippet = r#" +(define-non-fungible-token stackaroo uint) +(nft-mint? stackaroo u122 tx-sender) +(nft-mint? stackaroo u123 tx-sender) +(let ((recipient 'SP000000000000000000002Q6VF78)) + (restrict-assets? tx-sender ((with-nft current-contract "stackaroo" (list))) + (try! (nft-transfer? stackaroo u123 tx-sender recipient)) + ) +)"#; + let expected = Value::error(Value::Int(0)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} From 11c4b1412d303828b512f903a1e4df89e20c4088 Mon Sep 17 00:00:00 2001 From: Brice Dobry Date: Mon, 22 Sep 2025 12:24:47 -0400 Subject: [PATCH 11/35] feat: add checks for max number of allowances and max NFT identifiers --- clarity-types/src/errors/analysis.rs | 6 ++- .../analysis/type_checker/v2_1/natives/mod.rs | 2 +- .../v2_1/natives/post_conditions.rs | 21 ++++++++- .../v2_1/tests/post_conditions.rs | 45 +++++++++++++++++-- 4 files changed, 65 insertions(+), 9 deletions(-) diff --git a/clarity-types/src/errors/analysis.rs b/clarity-types/src/errors/analysis.rs index 1b16162107..5cb3176c02 100644 --- a/clarity-types/src/errors/analysis.rs +++ b/clarity-types/src/errors/analysis.rs @@ -315,7 +315,8 @@ pub enum CheckErrors { WithAllAllowanceNotAllowed, WithAllAllowanceNotAlone, WithNftExpectedListOfIdentifiers, - MaxIdentifierLengthExceeded(u32), + MaxIdentifierLengthExceeded(u32, u32), + TooManyAllowances(usize, usize), } #[derive(Debug, PartialEq)] @@ -620,7 +621,8 @@ impl DiagnosableError for CheckErrors { CheckErrors::WithAllAllowanceNotAllowed => "with-all-assets-unsafe is not allowed here, only in the allowance list for `as-contract?`".into(), CheckErrors::WithAllAllowanceNotAlone => "with-all-assets-unsafe must not be used along with other allowances".into(), CheckErrors::WithNftExpectedListOfIdentifiers => "with-nft allowance must include a list of asset identifiers".into(), - CheckErrors::MaxIdentifierLengthExceeded(max_len) => format!("with-nft allowance identifiers list must not exceed 128 elements, got {max_len}"), + CheckErrors::MaxIdentifierLengthExceeded(max_len, len) => format!("with-nft allowance identifiers list must not exceed {max_len} elements, got {len}"), + CheckErrors::TooManyAllowances(max_allowed, found) => format!("too many allowances specified, the maximum is {max_allowed}, found {found}"), } } diff --git a/clarity/src/vm/analysis/type_checker/v2_1/natives/mod.rs b/clarity/src/vm/analysis/type_checker/v2_1/natives/mod.rs index ee016f2dbf..26415fe7f1 100644 --- a/clarity/src/vm/analysis/type_checker/v2_1/natives/mod.rs +++ b/clarity/src/vm/analysis/type_checker/v2_1/natives/mod.rs @@ -41,7 +41,7 @@ mod assets; mod conversions; mod maps; mod options; -mod post_conditions; +pub(crate) mod post_conditions; mod sequences; #[allow(clippy::large_enum_variant)] diff --git a/clarity/src/vm/analysis/type_checker/v2_1/natives/post_conditions.rs b/clarity/src/vm/analysis/type_checker/v2_1/natives/post_conditions.rs index 8092513bf4..931cec4feb 100644 --- a/clarity/src/vm/analysis/type_checker/v2_1/natives/post_conditions.rs +++ b/clarity/src/vm/analysis/type_checker/v2_1/natives/post_conditions.rs @@ -25,6 +25,11 @@ use crate::vm::costs::cost_functions::ClarityCostFunction; use crate::vm::costs::runtime_cost; use crate::vm::functions::NativeFunctions; +/// Maximum number of allowances allowed in a `restrict-assets?` or `as-contract?` expression. +pub(crate) const MAX_ALLOWANCES: usize = 128; +/// Maximum number of asset identifiers allowed in a `with-nft` allowance expression. +pub(crate) const MAX_NFT_IDENTIFIERS: u32 = 128; + pub fn check_restrict_assets( checker: &mut TypeChecker, args: &[SymbolicExpression], @@ -41,6 +46,10 @@ pub fn check_restrict_assets( ))?; let body_exprs = &args[2..]; + if allowance_list.len() > MAX_ALLOWANCES { + return Err(CheckErrors::TooManyAllowances(MAX_ALLOWANCES, allowance_list.len()).into()); + } + runtime_cost( ClarityCostFunction::AnalysisListItemsCheck, checker, @@ -87,6 +96,10 @@ pub fn check_as_contract( ))?; let body_exprs = &args[1..]; + if allowance_list.len() > MAX_ALLOWANCES { + return Err(CheckErrors::TooManyAllowances(MAX_ALLOWANCES, allowance_list.len()).into()); + } + runtime_cost( ClarityCostFunction::AnalysisListItemsCheck, checker, @@ -208,8 +221,12 @@ fn check_allowance_with_nft( let TypeSignature::SequenceType(SequenceSubtype::ListType(list_data)) = id_list_ty else { return Err(CheckErrors::WithNftExpectedListOfIdentifiers.into()); }; - if list_data.get_max_len() > 128 { - return Err(CheckErrors::MaxIdentifierLengthExceeded(list_data.get_max_len()).into()); + if list_data.get_max_len() > MAX_NFT_IDENTIFIERS { + return Err(CheckErrors::MaxIdentifierLengthExceeded( + MAX_NFT_IDENTIFIERS, + list_data.get_max_len(), + ) + .into()); } Ok(false) diff --git a/clarity/src/vm/analysis/type_checker/v2_1/tests/post_conditions.rs b/clarity/src/vm/analysis/type_checker/v2_1/tests/post_conditions.rs index b336a136a5..60ec6e0173 100644 --- a/clarity/src/vm/analysis/type_checker/v2_1/tests/post_conditions.rs +++ b/clarity/src/vm/analysis/type_checker/v2_1/tests/post_conditions.rs @@ -14,9 +14,13 @@ // along with this program. If not, see . use clarity_types::errors::CheckErrors; +use clarity_types::representations::MAX_STRING_LEN; use clarity_types::types::TypeSignature; use stacks_common::types::StacksEpochId; +use crate::vm::analysis::type_checker::v2_1::natives::post_conditions::{ + MAX_ALLOWANCES, MAX_NFT_IDENTIFIERS, +}; use crate::vm::analysis::type_checker::v2_1::tests::type_check_helper_version; use crate::vm::tests::test_clarity_versions; use crate::vm::ClarityVersion; @@ -141,6 +145,17 @@ fn test_restrict_assets(#[case] version: ClarityVersion, #[case] _epoch: StacksE "(restrict-assets? tx-sender ((with-stx u1000)) (err u1) true)", CheckErrors::UncheckedIntermediaryResponses, ), + // too many allowances + ( + &format!( + "(restrict-assets? tx-sender ({} ) true)", + std::iter::repeat("(with-stx u1)") + .take(130) + .collect::>() + .join(" ") + ), + CheckErrors::TooManyAllowances(MAX_ALLOWANCES, 130), + ), ]; for (good_code, expected_type) in &good { @@ -280,6 +295,17 @@ fn test_as_contract(#[case] version: ClarityVersion, #[case] _epoch: StacksEpoch "(as-contract? ((with-stx u1000) (with-all-assets-unsafe)) true)", CheckErrors::WithAllAllowanceNotAlone, ), + // too many allowances + ( + &format!( + "(as-contract? ({} ) true)", + std::iter::repeat("(with-stx u1)") + .take(130) + .collect::>() + .join(" ") + ), + CheckErrors::TooManyAllowances(MAX_ALLOWANCES, 130), + ), ]; for (code, expected_type) in &good { @@ -481,7 +507,7 @@ fn test_with_ft_allowance(#[case] version: ClarityVersion, #[case] _epoch: Stack ( "(restrict-assets? tx-sender ((with-ft .token u123 u1000)) true)", CheckErrors::TypeError( - TypeSignature::new_string_ascii(128).unwrap().into(), + TypeSignature::new_string_ascii(MAX_STRING_LEN as usize).unwrap().into(), TypeSignature::UIntType.into(), ), ), @@ -505,7 +531,7 @@ fn test_with_ft_allowance(#[case] version: ClarityVersion, #[case] _epoch: Stack ( "(restrict-assets? tx-sender ((with-ft .token \"this-token-name-is-way-too-long-to-be-valid-because-it-has-more-than-one-hundred-and-twenty-eight-characters-in-it-so-it-is-not-a-valid-token-name\" u1000)) true)", CheckErrors::TypeError( - TypeSignature::new_string_ascii(128).unwrap().into(), + TypeSignature::new_string_ascii(MAX_STRING_LEN as usize).unwrap().into(), TypeSignature::new_string_ascii(146).unwrap().into(), ), ), @@ -629,7 +655,7 @@ fn test_with_nft_allowance(#[case] version: ClarityVersion, #[case] _epoch: Stac ( "(restrict-assets? tx-sender ((with-nft .token u123 (list u456))) true)", CheckErrors::TypeError( - TypeSignature::new_string_ascii(128).unwrap().into(), + TypeSignature::new_string_ascii(MAX_STRING_LEN as usize).unwrap().into(), TypeSignature::UIntType.into(), ), ), @@ -637,10 +663,21 @@ fn test_with_nft_allowance(#[case] version: ClarityVersion, #[case] _epoch: Stac ( "(restrict-assets? tx-sender ((with-ft .token \"this-token-name-is-way-too-long-to-be-valid-because-it-has-more-than-one-hundred-and-twenty-eight-characters-in-it-so-it-is-not-a-valid-token-name\" u1000)) true)", CheckErrors::TypeError( - TypeSignature::new_string_ascii(128).unwrap().into(), + TypeSignature::new_string_ascii(MAX_STRING_LEN as usize).unwrap().into(), TypeSignature::new_string_ascii(146).unwrap().into(), ), ), + // too many identifiers (more than 128) + ( + &format!( + "(restrict-assets? tx-sender ((with-nft .token \"token-name\" (list {}))) true)", + std::iter::repeat("u1") + .take(130) + .collect::>() + .join(" ") + ), + CheckErrors::MaxIdentifierLengthExceeded(MAX_NFT_IDENTIFIERS, 130), + ), ]; for (code, expected_type) in &good { From 1afa16842a78cba2ef6457724f430c811865c613 Mon Sep 17 00:00:00 2001 From: Brice Dobry Date: Mon, 22 Sep 2025 13:41:19 -0400 Subject: [PATCH 12/35] fix: update test and fix clippy issues --- .../type_checker/v2_1/tests/post_conditions.rs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/clarity/src/vm/analysis/type_checker/v2_1/tests/post_conditions.rs b/clarity/src/vm/analysis/type_checker/v2_1/tests/post_conditions.rs index 60ec6e0173..0a24369e0b 100644 --- a/clarity/src/vm/analysis/type_checker/v2_1/tests/post_conditions.rs +++ b/clarity/src/vm/analysis/type_checker/v2_1/tests/post_conditions.rs @@ -57,7 +57,7 @@ fn test_restrict_assets(#[case] version: ClarityVersion, #[case] _epoch: StacksE ), // multiple allowances ( - "(restrict-assets? tx-sender ((with-stx u1000) (with-ft .token \"foo\" u5000) (with-nft .token \"foo\" 0x01) (with-stacking u1000)) true)", + "(restrict-assets? tx-sender ((with-stx u1000) (with-ft .token \"foo\" u5000) (with-nft .token \"foo\" (list 0x01)) (with-stacking u1000)) true)", TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap() ), // multiple body expressions @@ -149,8 +149,7 @@ fn test_restrict_assets(#[case] version: ClarityVersion, #[case] _epoch: StacksE ( &format!( "(restrict-assets? tx-sender ({} ) true)", - std::iter::repeat("(with-stx u1)") - .take(130) + std::iter::repeat_n("(with-stx u1)", 130) .collect::>() .join(" ") ), @@ -299,8 +298,7 @@ fn test_as_contract(#[case] version: ClarityVersion, #[case] _epoch: StacksEpoch ( &format!( "(as-contract? ({} ) true)", - std::iter::repeat("(with-stx u1)") - .take(130) + std::iter::repeat_n("(with-stx u1)", 130) .collect::>() .join(" ") ), @@ -671,8 +669,7 @@ fn test_with_nft_allowance(#[case] version: ClarityVersion, #[case] _epoch: Stac ( &format!( "(restrict-assets? tx-sender ((with-nft .token \"token-name\" (list {}))) true)", - std::iter::repeat("u1") - .take(130) + std::iter::repeat_n("u1", 130) .collect::>() .join(" ") ), From 156724d3bce615ce953484ce25f3b75db6df9d27 Mon Sep 17 00:00:00 2001 From: Brice Dobry Date: Mon, 22 Sep 2025 14:07:32 -0400 Subject: [PATCH 13/35] test: ignore `with-stacking` in doc examples tests That infrastructure is not setup for handling PoX state, and the stacking tracking in the asset maps is not setup yet any way. --- clarity/src/vm/docs/mod.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/clarity/src/vm/docs/mod.rs b/clarity/src/vm/docs/mod.rs index ed1a94140d..bd37d3b654 100644 --- a/clarity/src/vm/docs/mod.rs +++ b/clarity/src/vm/docs/mod.rs @@ -3379,6 +3379,10 @@ mod test { ); continue; } + if func_api.name == "with-stacking" { + eprintln!("Skipping with-stacking, because it requires PoX state"); + continue; + } let mut store = MemoryBackingStore::new(); // first, load the samples for contract-call From df2b5818216006efc1397a0e4ed7e5252fabc64a Mon Sep 17 00:00:00 2001 From: Brice Dobry Date: Mon, 22 Sep 2025 15:28:40 -0400 Subject: [PATCH 14/35] fix: typo in tests --- .../src/vm/analysis/type_checker/v2_1/tests/post_conditions.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clarity/src/vm/analysis/type_checker/v2_1/tests/post_conditions.rs b/clarity/src/vm/analysis/type_checker/v2_1/tests/post_conditions.rs index 0a24369e0b..663da3c97c 100644 --- a/clarity/src/vm/analysis/type_checker/v2_1/tests/post_conditions.rs +++ b/clarity/src/vm/analysis/type_checker/v2_1/tests/post_conditions.rs @@ -580,7 +580,7 @@ fn test_with_nft_allowance(#[case] version: ClarityVersion, #[case] _epoch: Stac ), // full literal principal ( - r#"(restrict-assets? tx-sender ((with-ft 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.token "token-name" (list u1000))) true)"#, + r#"(restrict-assets? tx-sender ((with-ft 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.token "token-name" u1000)) true)"#, TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(), ), // variable principal From b20511e805c1880ebe55a4398a441b4266d32c11 Mon Sep 17 00:00:00 2001 From: Brice Dobry Date: Mon, 22 Sep 2025 17:26:19 -0400 Subject: [PATCH 15/35] feat: implement tracking of stacking for post-conditions Testing still needed. --- clarity-types/src/errors/mod.rs | 1 + clarity/src/vm/contexts.rs | 34 ++++++++++++++++++++++++++++++++- pox-locking/src/pox_4.rs | 33 ++++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 1 deletion(-) diff --git a/clarity-types/src/errors/mod.rs b/clarity-types/src/errors/mod.rs index 58b42b8a99..3c8e41e090 100644 --- a/clarity-types/src/errors/mod.rs +++ b/clarity-types/src/errors/mod.rs @@ -101,6 +101,7 @@ pub enum RuntimeErrorType { PoxAlreadyLocked, BlockTimeNotAvailable, + Unreachable, } #[derive(Debug, PartialEq)] diff --git a/clarity/src/vm/contexts.rs b/clarity/src/vm/contexts.rs index 6dba496d4b..05cefc3bd9 100644 --- a/clarity/src/vm/contexts.rs +++ b/clarity/src/vm/contexts.rs @@ -82,6 +82,7 @@ pub enum AssetMapEntry { Burn(u128), Token(u128), Asset(Vec), + Stacking(u128), } /** @@ -90,10 +91,16 @@ during the execution of a transaction. */ #[derive(Debug, Clone)] pub struct AssetMap { + /// Sum of all STX transfers by principal stx_map: HashMap, + /// Sum of all STX burns by principal burn_map: HashMap, + /// Sum of FT transfers by principal, by asset identifier token_map: HashMap>, + /// NFT transfers by principal, by asset identifier asset_map: HashMap>>, + /// Amount of STX stacked or delegated for stacking by principal + stacking_map: HashMap, } impl AssetMap { @@ -169,11 +176,23 @@ impl AssetMap { }) .collect(); + let stacking: serde_json::map::Map<_, _> = self + .stacking_map + .iter() + .map(|(principal, amount)| { + ( + format!("{principal}"), + serde_json::value::Value::String(format!("{amount}")), + ) + }) + .collect(); + json!({ "stx": stx, "burns": burns, "tokens": tokens, - "assets": assets + "assets": assets, + "stacking": stacking, }) } } @@ -264,6 +283,7 @@ impl AssetMap { burn_map: HashMap::new(), token_map: HashMap::new(), asset_map: HashMap::new(), + stacking_map: HashMap::new(), } } @@ -343,6 +363,13 @@ impl AssetMap { Ok(()) } + /// Log an amount of STX to be stacked or delegated for stacking by a + /// principal. Since any given principal can only stack once, this will + /// overwrite any previous amount for the principal. + pub fn add_stacking(&mut self, principal: &PrincipalData, amount: u128) { + self.stacking_map.insert(principal.clone(), amount); + } + // This will add any asset transfer data from other to self, // aborting _all_ changes in the event of an error, leaving self unchanged pub fn commit_other(&mut self, mut other: AssetMap) -> Result<()> { @@ -1612,6 +1639,11 @@ impl<'a, 'hooks> GlobalContext<'a, 'hooks> { self.get_asset_map()?.add_stx_burn(sender, transfered) } + pub fn log_stacking(&mut self, sender: &PrincipalData, amount: u128) -> Result<()> { + self.get_asset_map()?.add_stacking(sender, amount); + Ok(()) + } + pub fn execute(&mut self, f: F) -> Result where F: FnOnce(&mut Self) -> Result, diff --git a/pox-locking/src/pox_4.rs b/pox-locking/src/pox_4.rs index 733d6d6c54..2aa636d4d3 100644 --- a/pox-locking/src/pox_4.rs +++ b/pox-locking/src/pox_4.rs @@ -181,6 +181,11 @@ fn handle_stack_lockup_pox_v4( unlock_height, ) { Ok(_) => { + // For direct stacking, we log the locked amount in the asset map. + if function_name == "stack-stx" { + global_context.log_stacking(&stacker, locked_amount)?; + } + let event = StacksTransactionEvent::STXEvent(STXEventType::STXLockEvent(STXLockEventData { locked_amount, @@ -243,6 +248,11 @@ fn handle_stack_lockup_extension_pox_v4( match pox_lock_extend_v4(&mut global_context.database, &stacker, unlock_height) { Ok(locked_amount) => { + // For direct stacking, we log the locked amount in the asset map. + if function_name == "stack-extend" { + global_context.log_stacking(&stacker, locked_amount)?; + } + let event = StacksTransactionEvent::STXEvent(STXEventType::STXLockEvent(STXLockEventData { locked_amount, @@ -298,6 +308,11 @@ fn handle_stack_lockup_increase_pox_v4( }; match pox_lock_increase_v4(&mut global_context.database, &stacker, total_locked) { Ok(new_balance) => { + // For direct stacking, we log the locked amount in the asset map. + if function_name == "stack-increase" { + global_context.log_stacking(&stacker, new_balance.amount_locked())?; + } + let event = StacksTransactionEvent::STXEvent(STXEventType::STXLockEvent(STXLockEventData { locked_amount: new_balance.amount_locked(), @@ -379,6 +394,24 @@ pub fn handle_contract_call( None }; + if function_name == "delegate-stx" { + // Update the asset map to reflect the delegation + match (sender_opt, args.first()) { + (Some(sender), Some(Value::UInt(amount))) => { + global_context.log_stacking(sender, *amount)?; + } + _ => { + // This should be unreachable! + error!( + "Unreachable: failed to log STX delegation in PoX-4 delegate-stx call"; + "sender" => ?sender_opt, + "arg0" => ?args.first(), + ); + return Err(ClarityError::Runtime(RuntimeErrorType::Unreachable, None)); + } + } + } + // append the lockup event, so it looks as if the print event happened before the lock-up if let Some(batch) = global_context.event_batches.last_mut() { if let Some(print_event) = print_event_opt { From b396d86fa441536a3a5956cbced92119c1d1f721 Mon Sep 17 00:00:00 2001 From: Brice Dobry Date: Tue, 23 Sep 2025 11:43:44 -0400 Subject: [PATCH 16/35] feat: add support for "*" wildcard in allowances --- clarity/src/vm/functions/post_conditions.rs | 61 ++- clarity/src/vm/tests/post_conditions.rs | 514 ++++++++++++++++++++ 2 files changed, 560 insertions(+), 15 deletions(-) diff --git a/clarity/src/vm/functions/post_conditions.rs b/clarity/src/vm/functions/post_conditions.rs index b04203d12b..82bb1cf138 100644 --- a/clarity/src/vm/functions/post_conditions.rs +++ b/clarity/src/vm/functions/post_conditions.rs @@ -371,41 +371,72 @@ fn check_allowances( } // Check FT movements - // TODO: Handle "*" asset name if let Some(ft_moved) = assets.get_all_fungible_tokens(owner) { for (asset, amount_moved) in ft_moved { + // Build merged allowance list: exact-match entries + wildcard entries for the same contract + let mut merged: Vec<(usize, u128)> = Vec::new(); + if let Some(allowance_vec) = ft_allowances.get(asset) { - // Check against the FT allowances - for (index, allowance) in allowance_vec { - if *amount_moved > *allowance { - return Ok(Some(i128::try_from(*index).map_err(|_| { - InterpreterError::Expect("failed to convert index to i128".into()) - })?)); - } - } - } else { + merged.extend(allowance_vec.iter().cloned()); + } + + if let Some(wildcard_vec) = ft_allowances.get(&AssetIdentifier { + contract_identifier: asset.contract_identifier.clone(), + asset_name: "*".into(), + }) { + merged.extend(wildcard_vec.iter().cloned()); + } + + if merged.is_empty() { // No allowance for this asset, any movement is a violation return Ok(Some(-1)); } + + // Sort by allowance index so we check allowances in order + merged.sort_by_key(|(idx, _)| *idx); + + for (index, allowance) in merged { + if *amount_moved > allowance { + return Ok(Some(i128::try_from(index).map_err(|_| { + InterpreterError::Expect("failed to convert index to i128".into()) + })?)); + } + } } } // Check NFT movements - // TODO: Handle "*" asset name if let Some(nft_moved) = assets.get_all_nonfungible_tokens(owner) { for (asset, ids_moved) in nft_moved { + let mut merged: Vec<(usize, HashSet)> = Vec::new(); if let Some((index, allowance_map)) = nft_allowances.get(asset) { + merged.push((*index, allowance_map.clone())); + } + + if let Some((index, allowance_map)) = nft_allowances.get(&AssetIdentifier { + contract_identifier: asset.contract_identifier.clone(), + asset_name: "*".into(), + }) { + merged.push((*index, allowance_map.clone())); + } + + if merged.is_empty() { + // No allowance for this asset, any movement is a violation + return Ok(Some(-1)); + } + + // Sort by allowance index so we check allowances in order + merged.sort_by_key(|(idx, _)| *idx); + + for (index, allowance_map) in merged { // Check against the NFT allowances for id_moved in ids_moved { if !allowance_map.contains(&id_moved.serialize_to_hex()?) { - return Ok(Some(i128::try_from(*index).map_err(|_| { + return Ok(Some(i128::try_from(index).map_err(|_| { InterpreterError::Expect("failed to convert index to i128".into()) })?)); } } - } else { - // No allowance for this asset, any movement is a violation - return Ok(Some(-1)); } } } diff --git a/clarity/src/vm/tests/post_conditions.rs b/clarity/src/vm/tests/post_conditions.rs index b142f8e6bc..5ef6b673de 100644 --- a/clarity/src/vm/tests/post_conditions.rs +++ b/clarity/src/vm/tests/post_conditions.rs @@ -353,6 +353,124 @@ fn test_as_contract_with_ft_multiple_allowances_one_low() { assert_eq!(expected, execute(snippet).unwrap().unwrap()); } +#[test] +fn test_as_contract_with_ft_wildcard_ok() { + let snippet = r#" +(define-fungible-token stackaroo) +(ft-mint? stackaroo u200 current-contract) +(let ((recipient tx-sender)) + (as-contract? ((with-ft current-contract "*" u100)) + (try! (ft-transfer? stackaroo u100 tx-sender recipient)) + ) +)"#; + let expected = Value::okay_true(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_as_contract_with_ft_wildcard_exceeds() { + let snippet = r#" +(define-fungible-token stackaroo) +(ft-mint? stackaroo u200 current-contract) +(let ((recipient tx-sender)) + (as-contract? ((with-ft current-contract "*" u10)) + (try! (ft-transfer? stackaroo u50 tx-sender recipient)) + ) +)"#; + let expected = Value::error(Value::Int(0)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_as_contract_with_ft_wildcard_other_allowances() { + let snippet = r#" +(define-fungible-token stackaroo) +(ft-mint? stackaroo u200 current-contract) +(let ((recipient tx-sender)) + (as-contract? + ( + (with-stx u200) + (with-ft .other "*" u100) ;; other contract, same token name + (with-ft current-contract "other" u100) ;; same contract, different token name + (with-nft .token "*" (list 123)) + ) + (try! (ft-transfer? stackaroo u50 tx-sender recipient)) + ) +)"#; + let expected = Value::error(Value::Int(-1)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_as_contract_with_ft_wildcard_multiple_allowances_both_low() { + let snippet = r#" +(define-fungible-token stackaroo) +(ft-mint? stackaroo u200 current-contract) +(let ((recipient tx-sender)) + (as-contract? ((with-ft current-contract "*" u30) (with-ft current-contract "*" u20)) + (try! (ft-transfer? stackaroo u40 tx-sender recipient)) + ) +)"#; + let expected = Value::error(Value::Int(0)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_as_contract_with_ft_wildcard_multiple_allowances_both_ok() { + let snippet = r#" +(define-fungible-token stackaroo) +(ft-mint? stackaroo u200 current-contract) +(let ((recipient tx-sender)) + (as-contract? ((with-ft current-contract "*" u300) (with-ft current-contract "*" u200)) + (try! (ft-transfer? stackaroo u40 tx-sender recipient)) + ) +)"#; + let expected = Value::okay_true(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_as_contract_with_ft_wildcard_multiple_allowances_one_low() { + let snippet = r#" +(define-fungible-token stackaroo) +(ft-mint? stackaroo u200 current-contract) +(let ((recipient tx-sender)) + (as-contract? ((with-ft current-contract "*" u100) (with-ft current-contract "*" u20)) + (try! (ft-transfer? stackaroo u40 tx-sender recipient)) + ) +)"#; + let expected = Value::error(Value::Int(1)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_as_contract_with_ft_wildcard_multiple_allowances_low1() { + let snippet = r#" +(define-fungible-token stackaroo) +(ft-mint? stackaroo u200 current-contract) +(let ((recipient tx-sender)) + (as-contract? ((with-ft current-contract "*" u20) (with-ft current-contract "stackaroo" u20)) + (try! (ft-transfer? stackaroo u40 tx-sender recipient)) + ) +)"#; + let expected = Value::error(Value::Int(0)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_as_contract_with_ft_wildcard_multiple_allowances_low2() { + let snippet = r#" +(define-fungible-token stackaroo) +(ft-mint? stackaroo u200 current-contract) +(let ((recipient tx-sender)) + (as-contract? ((with-ft current-contract "stackaroo" u20) (with-ft current-contract "*" u20)) + (try! (ft-transfer? stackaroo u40 tx-sender recipient)) + ) +)"#; + let expected = Value::error(Value::Int(0)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + #[test] fn test_as_contract_with_nft_ok() { let snippet = r#" @@ -490,6 +608,145 @@ fn test_as_contract_with_nft_empty_id_list() { assert_eq!(expected, execute(snippet).unwrap().unwrap()); } +#[test] +fn test_as_contract_with_nft_wildcard_ok() { + let snippet = r#" +(define-non-fungible-token stackaroo uint) +(nft-mint? stackaroo u123 current-contract) +(let ((recipient tx-sender)) + (as-contract? ((with-nft current-contract "*" (list u123))) + (try! (nft-transfer? stackaroo u123 tx-sender recipient)) + ) +)"#; + let expected = Value::okay_true(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_as_contract_with_nft_wildcard_not_allowed() { + let snippet = r#" +(define-non-fungible-token stackaroo uint) +(nft-mint? stackaroo u122 current-contract) +(nft-mint? stackaroo u123 current-contract) +(let ((recipient tx-sender)) + (as-contract? ((with-nft current-contract "*" (list u122))) + (try! (nft-transfer? stackaroo u123 tx-sender recipient)) + ) +)"#; + let expected = Value::error(Value::Int(0)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_as_contract_with_nft_wildcard_other_allowances() { + let snippet = r#" +(define-non-fungible-token stackaroo uint) +(nft-mint? stackaroo u123 current-contract) +(let ((recipient tx-sender)) + (as-contract? + ( + (with-stx u123) + (with-nft .other "*" (list u123)) ;; other contract, same token name + (with-nft current-contract "other" (list u123)) ;; same contract, different token name + (with-ft .token "*" u123) + ) + (try! (nft-transfer? stackaroo u123 tx-sender recipient)) + ) +)"#; + let expected = Value::error(Value::Int(-1)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_as_contract_with_nft_wildcard_multiple_allowances_both_different() { + let snippet = r#" +(define-non-fungible-token stackaroo uint) +(nft-mint? stackaroo u122 current-contract) +(nft-mint? stackaroo u123 current-contract) +(let ((recipient tx-sender)) + (as-contract? ((with-nft current-contract "*" (list u122)) (with-nft current-contract "*" (list u124))) + (try! (nft-transfer? stackaroo u123 tx-sender recipient)) + ) +)"#; + let expected = Value::error(Value::Int(0)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_as_contract_with_nft_wildcard_multiple_allowances_including() { + let snippet = r#" +(define-non-fungible-token stackaroo uint) +(nft-mint? stackaroo u122 current-contract) +(nft-mint? stackaroo u123 current-contract) +(let ((recipient tx-sender)) + (as-contract? ((with-nft current-contract "*" (list u122)) (with-nft current-contract "*" (list u123))) + (try! (nft-transfer? stackaroo u123 tx-sender recipient)) + ) +)"#; + let expected = Value::okay_true(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_as_contract_with_nft_wildcard_multiple_allowances_in_list() { + let snippet = r#" +(define-non-fungible-token stackaroo uint) +(nft-mint? stackaroo u122 current-contract) +(nft-mint? stackaroo u123 current-contract) +(let ((recipient tx-sender)) + (as-contract? ((with-nft current-contract "*" (list u122 u123))) + (try! (nft-transfer? stackaroo u123 tx-sender recipient)) + ) +)"#; + let expected = Value::okay_true(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_as_contract_with_nft_wildcard_empty_id_list() { + let snippet = r#" +(define-non-fungible-token stackaroo uint) +(nft-mint? stackaroo u122 current-contract) +(nft-mint? stackaroo u123 current-contract) +(let ((recipient tx-sender)) + (as-contract? ((with-nft current-contract "*" (list))) + (try! (nft-transfer? stackaroo u123 tx-sender recipient)) + ) +)"#; + let expected = Value::error(Value::Int(0)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_as_contract_with_nft_wildcard_multiple_allowances_order1() { + let snippet = r#" +(define-non-fungible-token stackaroo uint) +(nft-mint? stackaroo u122 current-contract) +(nft-mint? stackaroo u123 current-contract) +(let ((recipient tx-sender)) + (as-contract? ((with-nft current-contract "*" (list u122)) (with-nft current-contract "stackaroo" (list u124))) + (try! (nft-transfer? stackaroo u123 tx-sender recipient)) + ) +)"#; + let expected = Value::error(Value::Int(0)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_as_contract_with_nft_wildcard_multiple_allowances_order2() { + let snippet = r#" +(define-non-fungible-token stackaroo uint) +(nft-mint? stackaroo u122 current-contract) +(nft-mint? stackaroo u123 current-contract) +(let ((recipient tx-sender)) + (as-contract? ((with-nft current-contract "stackaroo" (list u122)) (with-nft current-contract "*" (list u124))) + (try! (nft-transfer? stackaroo u123 tx-sender recipient)) + ) +)"#; + let expected = Value::error(Value::Int(0)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + // ---------- Tests for restrict-assets? ---------- #[test] @@ -776,6 +1033,124 @@ fn test_restrict_assets_with_ft_multiple_allowances_one_low() { assert_eq!(expected, execute(snippet).unwrap().unwrap()); } +#[test] +fn test_restrict_assets_with_ft_wildcard_ok() { + let snippet = r#" +(define-fungible-token stackaroo) +(ft-mint? stackaroo u200 tx-sender) +(let ((recipient 'SP000000000000000000002Q6VF78)) + (restrict-assets? tx-sender ((with-ft current-contract "*" u100)) + (try! (ft-transfer? stackaroo u100 tx-sender recipient)) + ) +)"#; + let expected = Value::okay_true(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_restrict_assets_with_ft_wildcard_exceeds() { + let snippet = r#" +(define-fungible-token stackaroo) +(ft-mint? stackaroo u200 tx-sender) +(let ((recipient 'SP000000000000000000002Q6VF78)) + (restrict-assets? tx-sender ((with-ft current-contract "*" u10)) + (try! (ft-transfer? stackaroo u50 tx-sender recipient)) + ) +)"#; + let expected = Value::error(Value::Int(0)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_restrict_assets_with_ft_wildcard_other_allowances() { + let snippet = r#" +(define-fungible-token stackaroo) +(ft-mint? stackaroo u200 tx-sender) +(let ((recipient 'SP000000000000000000002Q6VF78)) + (restrict-assets? tx-sender + ( + (with-stx u200) + (with-ft .other "*" u100) ;; other contract, same token name + (with-ft current-contract "other" u100) ;; same contract, different token name + (with-nft .token "*" (list 123)) + ) + (try! (ft-transfer? stackaroo u50 tx-sender recipient)) + ) +)"#; + let expected = Value::error(Value::Int(-1)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_restrict_assets_with_ft_wildcard_multiple_allowances_both_low() { + let snippet = r#" +(define-fungible-token stackaroo) +(ft-mint? stackaroo u200 tx-sender) +(let ((recipient 'SP000000000000000000002Q6VF78)) + (restrict-assets? tx-sender ((with-ft current-contract "*" u30) (with-ft current-contract "*" u20)) + (try! (ft-transfer? stackaroo u40 tx-sender recipient)) + ) +)"#; + let expected = Value::error(Value::Int(0)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_restrict_assets_with_ft_wildcard_multiple_allowances_both_ok() { + let snippet = r#" +(define-fungible-token stackaroo) +(ft-mint? stackaroo u200 tx-sender) +(let ((recipient 'SP000000000000000000002Q6VF78)) + (restrict-assets? tx-sender ((with-ft current-contract "*" u300) (with-ft current-contract "*" u200)) + (try! (ft-transfer? stackaroo u40 tx-sender recipient)) + ) +)"#; + let expected = Value::okay_true(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_restrict_assets_with_ft_wildcard_multiple_allowances_one_low() { + let snippet = r#" +(define-fungible-token stackaroo) +(ft-mint? stackaroo u200 tx-sender) +(let ((recipient 'SP000000000000000000002Q6VF78)) + (restrict-assets? tx-sender ((with-ft current-contract "*" u100) (with-ft current-contract "*" u20)) + (try! (ft-transfer? stackaroo u40 tx-sender recipient)) + ) +)"#; + let expected = Value::error(Value::Int(1)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_restrict_assets_with_ft_wildcard_multiple_allowances_low1() { + let snippet = r#" +(define-fungible-token stackaroo) +(ft-mint? stackaroo u200 tx-sender) +(let ((recipient 'SP000000000000000000002Q6VF78)) + (restrict-assets? tx-sender ((with-ft current-contract "*" u20) (with-ft current-contract "stackaroo" u20)) + (try! (ft-transfer? stackaroo u40 tx-sender recipient)) + ) +)"#; + let expected = Value::error(Value::Int(0)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_restrict_assets_with_ft_wildcard_multiple_allowances_low2() { + let snippet = r#" +(define-fungible-token stackaroo) +(ft-mint? stackaroo u200 tx-sender) +(let ((recipient 'SP000000000000000000002Q6VF78)) + (restrict-assets? tx-sender ((with-ft current-contract "stackaroo" u20) (with-ft current-contract "*" u20)) + (try! (ft-transfer? stackaroo u40 tx-sender recipient)) + ) +)"#; + let expected = Value::error(Value::Int(0)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + #[test] fn test_restrict_assets_with_nft_ok() { let snippet = r#" @@ -912,3 +1287,142 @@ fn test_restrict_assets_with_nft_empty_id_list() { let expected = Value::error(Value::Int(0)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } + +#[test] +fn test_restrict_assets_with_nft_wildcard_ok() { + let snippet = r#" +(define-non-fungible-token stackaroo uint) +(nft-mint? stackaroo u123 tx-sender) +(let ((recipient 'SP000000000000000000002Q6VF78)) + (restrict-assets? tx-sender ((with-nft current-contract "*" (list u123))) + (try! (nft-transfer? stackaroo u123 tx-sender recipient)) + ) +)"#; + let expected = Value::okay_true(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_restrict_assets_with_nft_wildcard_not_allowed() { + let snippet = r#" +(define-non-fungible-token stackaroo uint) +(nft-mint? stackaroo u122 tx-sender) +(nft-mint? stackaroo u123 tx-sender) +(let ((recipient 'SP000000000000000000002Q6VF78)) + (restrict-assets? tx-sender ((with-nft current-contract "*" (list u122))) + (try! (nft-transfer? stackaroo u123 tx-sender recipient)) + ) +)"#; + let expected = Value::error(Value::Int(0)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_restrict_assets_with_nft_wildcard_other_allowances() { + let snippet = r#" +(define-non-fungible-token stackaroo uint) +(nft-mint? stackaroo u123 tx-sender) +(let ((recipient 'SP000000000000000000002Q6VF78)) + (restrict-assets? tx-sender + ( + (with-stx u123) + (with-nft .other "*" (list u123)) ;; other contract, same token name + (with-nft current-contract "other" (list u123)) ;; same contract, different token name + (with-ft .token "*" u123) + ) + (try! (nft-transfer? stackaroo u123 tx-sender recipient)) + ) +)"#; + let expected = Value::error(Value::Int(-1)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_restrict_assets_with_nft_wildcard_multiple_allowances_both_different() { + let snippet = r#" +(define-non-fungible-token stackaroo uint) +(nft-mint? stackaroo u122 tx-sender) +(nft-mint? stackaroo u123 tx-sender) +(let ((recipient 'SP000000000000000000002Q6VF78)) + (restrict-assets? tx-sender ((with-nft current-contract "*" (list u122)) (with-nft current-contract "*" (list u124))) + (try! (nft-transfer? stackaroo u123 tx-sender recipient)) + ) +)"#; + let expected = Value::error(Value::Int(0)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_restrict_assets_with_nft_wildcard_multiple_allowances_including() { + let snippet = r#" +(define-non-fungible-token stackaroo uint) +(nft-mint? stackaroo u122 tx-sender) +(nft-mint? stackaroo u123 tx-sender) +(let ((recipient 'SP000000000000000000002Q6VF78)) + (restrict-assets? tx-sender ((with-nft current-contract "*" (list u122)) (with-nft current-contract "*" (list u123))) + (try! (nft-transfer? stackaroo u123 tx-sender recipient)) + ) +)"#; + let expected = Value::okay_true(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_restrict_assets_with_nft_wildcard_multiple_allowances_in_list() { + let snippet = r#" +(define-non-fungible-token stackaroo uint) +(nft-mint? stackaroo u122 tx-sender) +(nft-mint? stackaroo u123 tx-sender) +(let ((recipient 'SP000000000000000000002Q6VF78)) + (restrict-assets? tx-sender ((with-nft current-contract "*" (list u122 u123))) + (try! (nft-transfer? stackaroo u123 tx-sender recipient)) + ) +)"#; + let expected = Value::okay_true(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_restrict_assets_with_nft_wildcard_empty_id_list() { + let snippet = r#" +(define-non-fungible-token stackaroo uint) +(nft-mint? stackaroo u122 tx-sender) +(nft-mint? stackaroo u123 tx-sender) +(let ((recipient 'SP000000000000000000002Q6VF78)) + (restrict-assets? tx-sender ((with-nft current-contract "*" (list))) + (try! (nft-transfer? stackaroo u123 tx-sender recipient)) + ) +)"#; + let expected = Value::error(Value::Int(0)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_restrict_assets_with_nft_wildcard_multiple_allowances_order1() { + let snippet = r#" +(define-non-fungible-token stackaroo uint) +(nft-mint? stackaroo u122 tx-sender) +(nft-mint? stackaroo u123 tx-sender) +(let ((recipient 'SP000000000000000000002Q6VF78)) + (restrict-assets? tx-sender ((with-nft current-contract "*" (list u122)) (with-nft current-contract "stackaroo" (list u124))) + (try! (nft-transfer? stackaroo u123 tx-sender recipient)) + ) +)"#; + let expected = Value::error(Value::Int(0)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + +#[test] +fn test_restrict_assets_with_nft_wildcard_multiple_allowances_order2() { + let snippet = r#" +(define-non-fungible-token stackaroo uint) +(nft-mint? stackaroo u122 tx-sender) +(nft-mint? stackaroo u123 tx-sender) +(let ((recipient 'SP000000000000000000002Q6VF78)) + (restrict-assets? tx-sender ((with-nft current-contract "stackaroo" (list u122)) (with-nft current-contract "*" (list u124))) + (try! (nft-transfer? stackaroo u123 tx-sender recipient)) + ) +)"#; + let expected = Value::error(Value::Int(0)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} From dd13789d05e13689816438e839b34f3f0e70ab67 Mon Sep 17 00:00:00 2001 From: Brice Dobry Date: Wed, 24 Sep 2025 11:23:52 -0400 Subject: [PATCH 17/35] fix: add stacking allowance check --- clarity/src/vm/contexts.rs | 8 ++++++++ clarity/src/vm/functions/post_conditions.rs | 17 +++++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/clarity/src/vm/contexts.rs b/clarity/src/vm/contexts.rs index 05cefc3bd9..2120b508b3 100644 --- a/clarity/src/vm/contexts.rs +++ b/clarity/src/vm/contexts.rs @@ -419,6 +419,10 @@ impl AssetMap { principal_map.insert(asset, amount); } + for (principal, stacking_amount) in other.stacking_map.drain() { + self.stacking_map.insert(principal, stacking_amount); + } + Ok(()) } @@ -506,6 +510,10 @@ impl AssetMap { let assets = self.asset_map.get(principal)?; Some(assets) } + + pub fn get_stacking(&self, principal: &PrincipalData) -> Option { + self.stacking_map.get(principal).copied() + } } impl fmt::Display for AssetMap { diff --git a/clarity/src/vm/functions/post_conditions.rs b/clarity/src/vm/functions/post_conditions.rs index 82bb1cf138..40d91f3511 100644 --- a/clarity/src/vm/functions/post_conditions.rs +++ b/clarity/src/vm/functions/post_conditions.rs @@ -441,6 +441,23 @@ fn check_allowances( } } + // Check stacking + if let Some(stx_stacked) = assets.get_stacking(owner) { + // If there are no allowances for stacking, any stacking is a violation + if stacking_allowances.is_empty() { + return Ok(Some(-1)); + } + + // Check against the stacking allowances + for (index, allowance) in &stacking_allowances { + if stx_stacked > *allowance { + return Ok(Some(i128::try_from(*index).map_err(|_| { + InterpreterError::Expect("failed to convert index to i128".into()) + })?)); + } + } + } + Ok(None) } From 103027cca110aa763d9b05ba9691c92f150ca76b Mon Sep 17 00:00:00 2001 From: Brice Dobry Date: Wed, 24 Sep 2025 23:23:34 -0400 Subject: [PATCH 18/35] test: add integration test for `with-stacking` allowances --- clarity/src/vm/functions/post_conditions.rs | 17 +- clarity/src/vm/tests/post_conditions.rs | 76 +--- .../src/tests/nakamoto_integrations.rs | 397 ++++++++++++++++++ 3 files changed, 414 insertions(+), 76 deletions(-) diff --git a/clarity/src/vm/functions/post_conditions.rs b/clarity/src/vm/functions/post_conditions.rs index 40d91f3511..ad320bb021 100644 --- a/clarity/src/vm/functions/post_conditions.rs +++ b/clarity/src/vm/functions/post_conditions.rs @@ -263,6 +263,8 @@ pub fn special_as_contract( // evaluate the body expressions let mut last_result = None; for expr in body_exprs { + // TODO: handle runtime errors inside the body expressions correctly + // (ensure that the context is always popped and asset maps are checked against allowances) let result = eval(expr, &mut nested_env, context)?; last_result.replace(result); } @@ -272,10 +274,17 @@ pub fn special_as_contract( // If the allowances are violated: // - Rollback the context // - Emit an event - if let Some(violation_index) = check_allowances(&contract_principal, &allowances, asset_maps)? { - nested_env.global_context.roll_back()?; - // TODO: Emit an event about the allowance violation - return Value::error(Value::Int(violation_index)); + match check_allowances(&contract_principal, &allowances, asset_maps) { + Ok(None) => {} + Ok(Some(violation_index)) => { + nested_env.global_context.roll_back()?; + // TODO: Emit an event about the allowance violation + return Value::error(Value::Int(violation_index)); + } + Err(e) => { + nested_env.global_context.roll_back()?; + return Err(e); + } } nested_env.global_context.commit()?; diff --git a/clarity/src/vm/tests/post_conditions.rs b/clarity/src/vm/tests/post_conditions.rs index 5ef6b673de..71c7e28d60 100644 --- a/clarity/src/vm/tests/post_conditions.rs +++ b/clarity/src/vm/tests/post_conditions.rs @@ -13,6 +13,10 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . +//! This module contains unit tests for the `as-contract?` and +//! `restrict-assets?` expressions. The `with-stacking` allowances are tested +//! in integration tests, since they require changes made outside of the VM. + use clarity_types::errors::InterpreterResult; use clarity_types::types::{PrincipalData, QualifiedContractIdentifier, StandardPrincipalData}; use clarity_types::Value; @@ -199,42 +203,6 @@ fn test_as_contract_multiple_allowances_one_low() { assert_eq!(expected, execute(snippet).unwrap().unwrap()); } -// #[test] -// fn test_as_contract_with_stacking_delegate_ok() { -// let snippet = r#" -// (as-contract? ((with-stacking u2000)) -// (try! (contract-call? 'SP000000000000000000002Q6VF78.pox-4 delegate-stx -// u1000 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM none none -// )) -// )"#; -// let expected = Value::okay_true(); -// assert_eq!(expected, execute(snippet).unwrap().unwrap()); -// } - -// #[test] -// fn test_as_contract_with_stacking_stack_ok() { -// let snippet = r#" -// (as-contract? ((with-stacking u100)) -// (try! (contract-call? 'SP000000000000000000002Q6VF78.pox-4 stack-stx -// u1100000000000 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM none none -// )) -// )"#; -// let expected = Value::okay_true(); -// assert_eq!(expected, execute(snippet).unwrap().unwrap()); -// } - -// #[test] -// fn test_as_contract_with_stacking_exceeds() { -// let snippet = r#" -// (as-contract? ((with-stacking u10)) -// (try! (contract-call? 'SP000000000000000000002Q6VF78.pox-4 delegate-stx -// u1000 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM none none -// )) -// )"#; -// let expected = Value::error(Value::Int(0)).unwrap(); -// assert_eq!(expected, execute(snippet).unwrap().unwrap()); -// } - #[test] fn test_as_contract_with_ft_ok() { let snippet = r#" @@ -879,42 +847,6 @@ fn test_restrict_assets_multiple_allowances_one_low() { assert_eq!(expected, execute(snippet).unwrap().unwrap()); } -// #[test] -// fn test_restrict_assets_with_stacking_delegate_ok() { -// let snippet = r#" -// (restrict-assets? tx-sender ((with-stacking u2000)) -// (try! (contract-call? 'SP000000000000000000002Q6VF78.pox-4 delegate-stx -// u1000 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM none none -// )) -// )"#; -// let expected = Value::okay_true(); -// assert_eq!(expected, execute(snippet).unwrap().unwrap()); -// } - -// #[test] -// fn test_restrict_assets_with_stacking_stack_ok() { -// let snippet = r#" -// (restrict-assets? tx-sender ((with-stacking u100)) -// (try! (contract-call? 'SP000000000000000000002Q6VF78.pox-4 stack-stx -// u1100000000000 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM none none -// )) -// )"#; -// let expected = Value::okay_true(); -// assert_eq!(expected, execute(snippet).unwrap().unwrap()); -// } - -// #[test] -// fn test_restrict_assets_with_stacking_exceeds() { -// let snippet = r#" -// (restrict-assets? tx-sender ((with-stacking u10)) -// (try! (contract-call? 'SP000000000000000000002Q6VF78.pox-4 delegate-stx -// u1000 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM none none -// )) -// )"#; -// let expected = Value::error(Value::Int(0)).unwrap(); -// assert_eq!(expected, execute(snippet).unwrap().unwrap()); -// } - #[test] fn test_restrict_assets_with_ft_ok() { let snippet = r#" diff --git a/stacks-node/src/tests/nakamoto_integrations.rs b/stacks-node/src/tests/nakamoto_integrations.rs index 9fe72b8887..dbd5aaa9ed 100644 --- a/stacks-node/src/tests/nakamoto_integrations.rs +++ b/stacks-node/src/tests/nakamoto_integrations.rs @@ -15324,3 +15324,400 @@ fn check_block_time_keyword() { run_loop_thread.join().unwrap(); } + +#[test] +#[ignore] +/// Verify the `with-stacking` allowances work as expected +fn check_with_stacking_allowances() { + if env::var("BITCOIND_TEST") != Ok("1".into()) { + return; + } + + let mut signers = TestSigners::default(); + let (mut naka_conf, _miner_account) = naka_neon_integration_conf(None); + let http_origin = format!("http://{}", &naka_conf.node.rpc_bind); + naka_conf.burnchain.chain_id = CHAIN_ID_TESTNET + 1; + let sender_sk = Secp256k1PrivateKey::random(); + let sender_signer_sk = Secp256k1PrivateKey::random(); + let sender_signer_addr = tests::to_addr(&sender_signer_sk); + + // setup sender + recipient for some test stx transfers + // these are necessary for the interim blocks to get mined at all + let sender_addr = tests::to_addr(&sender_sk); + let deploy_fee = 3000; + let call_fee = 400; + naka_conf.add_initial_balance( + PrincipalData::from(sender_addr.clone()).to_string(), + deploy_fee + call_fee * 30, + ); + naka_conf.add_initial_balance( + PrincipalData::from(sender_signer_addr.clone()).to_string(), + 100000, + ); + + // Add epoch 3.3 to the configuration because it is not yet added to the + // default epoch list for integration tests. + naka_conf.burnchain.epochs = Some(EpochList::new(&*NAKAMOTO_INTEGRATION_3_3_EPOCHS)); + + let stacker_sk = setup_stacker(&mut naka_conf); + + test_observer::spawn(); + test_observer::register_any(&mut naka_conf); + + let mut btcd_controller = BitcoinCoreController::from_stx_config(&naka_conf); + btcd_controller + .start_bitcoind() + .expect("Failed starting bitcoind"); + let mut btc_regtest_controller = BitcoinRegtestController::new(naka_conf.clone(), None); + btc_regtest_controller.bootstrap_chain(201); + + let mut run_loop = boot_nakamoto::BootRunLoop::new(naka_conf.clone()).unwrap(); + let run_loop_stopper = run_loop.get_termination_switch(); + let Counters { + blocks_processed, .. + } = run_loop.counters(); + let counters = run_loop.counters(); + + let coord_channel = run_loop.coordinator_channels(); + + let run_loop_thread = thread::Builder::new() + .name("run_loop".into()) + .spawn(move || run_loop.start(None, 0)) + .unwrap(); + wait_for_runloop(&blocks_processed); + + boot_to_epoch_3( + &naka_conf, + &blocks_processed, + &[stacker_sk.clone()], + &[sender_signer_sk], + &mut Some(&mut signers), + &mut btc_regtest_controller, + ); + + info!("Bootstrapped to Epoch-3.0 boundary, starting nakamoto miner"); + + info!("Nakamoto miner started..."); + blind_signer(&naka_conf, &signers, &counters); + wait_for_first_naka_block_commit(60, &counters.naka_submitted_commits); + + // mine until epoch 3.3 height + loop { + next_block_and_process_new_stacks_block(&mut btc_regtest_controller, 60, &coord_channel) + .unwrap(); + + // once we actually get a block in epoch 3.3, exit + let blocks = test_observer::get_blocks(); + let last_block = blocks.last().unwrap(); + if last_block + .get("burn_block_height") + .unwrap() + .as_u64() + .unwrap() + >= naka_conf.burnchain.epochs.as_ref().unwrap()[StacksEpochId::Epoch33].start_height + { + break; + } + } + + info!( + "Nakamoto miner has advanced to bitcoin height {}", + get_chain_info_opt(&naka_conf).unwrap().burn_block_height + ); + + let info = get_chain_info_result(&naka_conf).unwrap(); + let last_stacks_block_height = info.stacks_tip_height as u128; + + next_block_and_mine_commit(&mut btc_regtest_controller, 60, &naka_conf, &counters).unwrap(); + + let mut sender_nonce = 0; + let contract_name = "test-contract"; + let contract = format!( + r#" +(define-public (delegate-stx (amount uint) (allowed uint)) + (as-contract? ((with-stacking allowed)) + (try! (contract-call? 'ST000000000000000000002AMW42H.pox-4 delegate-stx + amount 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM none none + )) + ) +) +(define-public (delegate-stx-2-allowances (amount uint) (allowed-1 uint) (allowed-2 uint)) + (as-contract? ((with-stacking allowed-1) (with-stacking allowed-2)) + (try! (contract-call? 'ST000000000000000000002AMW42H.pox-4 delegate-stx + amount 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM none none + )) + ) +) +(define-public (delegate-stx-no-allowance (amount uint)) + (as-contract? () + (try! (contract-call? 'ST000000000000000000002AMW42H.pox-4 delegate-stx + amount 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM none none + )) + ) +) +(define-public (delegate-stx-all (amount uint)) + (as-contract? ((with-all-assets-unsafe)) + (try! (contract-call? 'ST000000000000000000002AMW42H.pox-4 delegate-stx + amount 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM none none + )) + ) +) +(define-public (revoke-delegate-stx) + (as-contract? () + (try! (contract-call? 'ST000000000000000000002AMW42H.pox-4 revoke-delegate-stx)) + (ok true) + ) +) +"# + ); + + let contract_tx = make_contract_publish_versioned( + &sender_sk, + sender_nonce, + deploy_fee, + naka_conf.burnchain.chain_id, + contract_name, + &contract, + Some(ClarityVersion::Clarity4), + ); + sender_nonce += 1; + let deploy_txid = submit_tx(&http_origin, &contract_tx); + info!("Submitted deploy txid: {deploy_txid}"); + + let mut stacks_block_height = 0; + wait_for(60, || { + let cur_sender_nonce = get_account(&http_origin, &to_addr(&sender_sk)).nonce; + let info = get_chain_info_result(&naka_conf).unwrap(); + stacks_block_height = info.stacks_tip_height as u128; + Ok(stacks_block_height > last_stacks_block_height && cur_sender_nonce == sender_nonce) + }) + .expect("Timed out waiting for contracts to publish"); + + next_block_and_process_new_stacks_block(&mut btc_regtest_controller, 30, &coord_channel) + .unwrap(); + + test_observer::clear(); + + let mut expected_results = HashMap::new(); + + let delegate_ok_tx = make_contract_call( + &sender_sk, + sender_nonce, + deploy_fee, + naka_conf.burnchain.chain_id, + &sender_addr, + contract_name, + "delegate-stx", + &[Value::UInt(1000), Value::UInt(2000)], + ); + sender_nonce += 1; + let delegate_ok_txid = submit_tx(&http_origin, &delegate_ok_tx); + info!("Submitted delegate_ok txid: {delegate_ok_txid}"); + expected_results.insert(delegate_ok_txid, Value::okay_true()); + + let revoke_delegate_tx = make_contract_call( + &sender_sk, + sender_nonce, + call_fee, + naka_conf.burnchain.chain_id, + &sender_addr, + contract_name, + "revoke-delegate-stx", + &[], + ); + sender_nonce += 1; + let revoke_delegate_txid = submit_tx(&http_origin, &revoke_delegate_tx); + info!("Submitted revoke_delegate txid: {revoke_delegate_txid}"); + expected_results.insert(revoke_delegate_txid, Value::okay_true()); + + let delegate_err_tx = make_contract_call( + &sender_sk, + sender_nonce, + call_fee, + naka_conf.burnchain.chain_id, + &sender_addr, + contract_name, + "delegate-stx", + &[Value::UInt(1000), Value::UInt(200)], + ); + sender_nonce += 1; + let delegate_err_txid = submit_tx(&http_origin, &delegate_err_tx); + info!("Submitted delegate_err txid: {delegate_err_txid}"); + expected_results.insert(delegate_err_txid, Value::error(Value::Int(0)).unwrap()); + + let delegate_2_ok_tx = make_contract_call( + &sender_sk, + sender_nonce, + call_fee, + naka_conf.burnchain.chain_id, + &sender_addr, + contract_name, + "delegate-stx-2-allowances", + &[Value::UInt(1000), Value::UInt(2000), Value::UInt(3000)], + ); + sender_nonce += 1; + let delegate_2_ok_txid = submit_tx(&http_origin, &delegate_2_ok_tx); + info!("Submitted delegate_2_ok txid: {delegate_2_ok_txid}"); + expected_results.insert(delegate_2_ok_txid, Value::okay_true()); + + let revoke_delegate_tx = make_contract_call( + &sender_sk, + sender_nonce, + call_fee, + naka_conf.burnchain.chain_id, + &sender_addr, + contract_name, + "revoke-delegate-stx", + &[], + ); + sender_nonce += 1; + let revoke_delegate_txid = submit_tx(&http_origin, &revoke_delegate_tx); + info!("Submitted revoke_delegate txid: {revoke_delegate_txid}"); + expected_results.insert(revoke_delegate_txid, Value::okay_true()); + + let delegate_2_both_err_tx = make_contract_call( + &sender_sk, + sender_nonce, + call_fee, + naka_conf.burnchain.chain_id, + &sender_addr, + contract_name, + "delegate-stx-2-allowances", + &[Value::UInt(1000), Value::UInt(600), Value::UInt(700)], + ); + sender_nonce += 1; + let delegate_2_both_err_txid = submit_tx(&http_origin, &delegate_2_both_err_tx); + info!("Submitted delegate_2_both_err txid: {delegate_2_both_err_txid}"); + expected_results.insert( + delegate_2_both_err_txid, + Value::error(Value::Int(0)).unwrap(), + ); + + let delegate_2_first_err_tx = make_contract_call( + &sender_sk, + sender_nonce, + call_fee, + naka_conf.burnchain.chain_id, + &sender_addr, + contract_name, + "delegate-stx-2-allowances", + &[Value::UInt(1000), Value::UInt(600), Value::UInt(1000)], + ); + sender_nonce += 1; + let delegate_2_first_err_txid = submit_tx(&http_origin, &delegate_2_first_err_tx); + info!("Submitted delegate_2_first_err txid: {delegate_2_first_err_txid}"); + expected_results.insert( + delegate_2_first_err_txid, + Value::error(Value::Int(0)).unwrap(), + ); + + let delegate_2_second_err_tx = make_contract_call( + &sender_sk, + sender_nonce, + call_fee, + naka_conf.burnchain.chain_id, + &sender_addr, + contract_name, + "delegate-stx-2-allowances", + &[Value::UInt(1000), Value::UInt(2000), Value::UInt(100)], + ); + sender_nonce += 1; + let delegate_2_second_err_txid = submit_tx(&http_origin, &delegate_2_second_err_tx); + info!("Submitted delegate_2_second_err txid: {delegate_2_second_err_txid}"); + expected_results.insert( + delegate_2_second_err_txid, + Value::error(Value::Int(1)).unwrap(), + ); + + let delegate_no_allowance_err_tx = make_contract_call( + &sender_sk, + sender_nonce, + call_fee, + naka_conf.burnchain.chain_id, + &sender_addr, + contract_name, + "delegate-stx-no-allowance", + &[Value::UInt(1000)], + ); + sender_nonce += 1; + let delegate_no_allowance_err_txid = submit_tx(&http_origin, &delegate_no_allowance_err_tx); + info!("Submitted delegate_no_allowance_err txid: {delegate_no_allowance_err_txid}"); + expected_results.insert( + delegate_no_allowance_err_txid, + Value::error(Value::Int(-1)).unwrap(), + ); + + let delegate_all_tx = make_contract_call( + &sender_sk, + sender_nonce, + call_fee, + naka_conf.burnchain.chain_id, + &sender_addr, + contract_name, + "delegate-stx-all", + &[Value::UInt(1000)], + ); + sender_nonce += 1; + let delegate_all_txid = submit_tx(&http_origin, &delegate_all_tx); + info!("Submitted delegate_all txid: {delegate_all_txid}"); + expected_results.insert(delegate_all_txid, Value::okay_true()); + + let revoke_delegate_tx = make_contract_call( + &sender_sk, + sender_nonce, + call_fee, + naka_conf.burnchain.chain_id, + &sender_addr, + contract_name, + "revoke-delegate-stx", + &[], + ); + sender_nonce += 1; + let revoke_delegate_txid = submit_tx(&http_origin, &revoke_delegate_tx); + info!("Submitted revoke_delegate txid: {revoke_delegate_txid}"); + expected_results.insert(revoke_delegate_txid, Value::okay_true()); + + wait_for(60, || { + let cur_sender_nonce = get_account(&http_origin, &to_addr(&sender_sk)).nonce; + Ok(cur_sender_nonce == sender_nonce) + }) + .expect("Timed out waiting for contract calls"); + + let blocks = test_observer::get_blocks(); + let mut found = 0; + for block in blocks.iter() { + for tx in block.get("transactions").unwrap().as_array().unwrap() { + let txid = tx + .get("txid") + .unwrap() + .as_str() + .unwrap() + .strip_prefix("0x") + .unwrap(); + if let Some(expected) = expected_results.get(txid) { + let raw_result = tx.get("raw_result").unwrap().as_str().unwrap(); + let parsed = Value::try_deserialize_hex_untyped(&raw_result[2..]).unwrap(); + found += 1; + assert_eq!(&parsed, expected); + } else { + // If there are any txids we don't expect, panic, because it probably means + // there is an error in the test itself. + panic!("Found unexpected txid: {txid}"); + } + } + } + + assert_eq!( + found, + expected_results.len(), + "Should have found all expected txs" + ); + + coord_channel + .lock() + .expect("Mutex poisoned") + .stop_chains_coordinator(); + run_loop_stopper.store(false, Ordering::SeqCst); + + run_loop_thread.join().unwrap(); +} From c4fb73d4be74bcbbb2b9b7a7392536d2f2d73af1 Mon Sep 17 00:00:00 2001 From: Brice Dobry Date: Thu, 25 Sep 2025 13:48:46 -0400 Subject: [PATCH 19/35] refactor: return `uint` error from post-conditions --- .../v2_1/natives/post_conditions.rs | 4 +- .../v2_1/tests/post_conditions.rs | 72 +++++------ clarity/src/vm/docs/mod.rs | 20 +-- clarity/src/vm/functions/post_conditions.rs | 39 +++--- clarity/src/vm/tests/post_conditions.rs | 121 +++++++++--------- .../src/tests/nakamoto_integrations.rs | 2 +- 6 files changed, 130 insertions(+), 128 deletions(-) diff --git a/clarity/src/vm/analysis/type_checker/v2_1/natives/post_conditions.rs b/clarity/src/vm/analysis/type_checker/v2_1/natives/post_conditions.rs index 931cec4feb..4f041f0794 100644 --- a/clarity/src/vm/analysis/type_checker/v2_1/natives/post_conditions.rs +++ b/clarity/src/vm/analysis/type_checker/v2_1/natives/post_conditions.rs @@ -77,7 +77,7 @@ pub fn check_restrict_assets( let ok_type = last_return.ok_or_else(|| CheckErrors::CheckerImplementationFailure)?; Ok(TypeSignature::new_response( ok_type, - TypeSignature::IntType, + TypeSignature::UIntType, )?) } @@ -125,7 +125,7 @@ pub fn check_as_contract( let ok_type = last_return.ok_or_else(|| CheckErrors::CheckerImplementationFailure)?; Ok(TypeSignature::new_response( ok_type, - TypeSignature::IntType, + TypeSignature::UIntType, )?) } diff --git a/clarity/src/vm/analysis/type_checker/v2_1/tests/post_conditions.rs b/clarity/src/vm/analysis/type_checker/v2_1/tests/post_conditions.rs index 663da3c97c..6f6a5ae4ff 100644 --- a/clarity/src/vm/analysis/type_checker/v2_1/tests/post_conditions.rs +++ b/clarity/src/vm/analysis/type_checker/v2_1/tests/post_conditions.rs @@ -32,38 +32,38 @@ fn test_restrict_assets(#[case] version: ClarityVersion, #[case] _epoch: StacksE // simple ( "(restrict-assets? tx-sender ((with-stx u1000)) true)", - TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap() + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::UIntType).unwrap() ), // literal asset owner ( "(restrict-assets? 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4 ((with-stx u1000)) true)", - TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap() + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::UIntType).unwrap() ), // literal asset owner with contract id ( "(restrict-assets? 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token ((with-stx u1000)) true)", - TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap() + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::UIntType).unwrap() ), // variable asset owner ( "(let ((p tx-sender)) (restrict-assets? p ((with-stx u1000)) true))", - TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap() + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::UIntType).unwrap() ), // no allowances ( "(restrict-assets? tx-sender () true)", - TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap() + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::UIntType).unwrap() ), // multiple allowances ( "(restrict-assets? tx-sender ((with-stx u1000) (with-ft .token \"foo\" u5000) (with-nft .token \"foo\" (list 0x01)) (with-stacking u1000)) true)", - TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap() + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::UIntType).unwrap() ), // multiple body expressions ( "(restrict-assets? tx-sender ((with-stx u1000)) (+ u1 u2) true)", - TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap() + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::UIntType).unwrap() ), ]; let bad = [ @@ -204,27 +204,27 @@ fn test_as_contract(#[case] version: ClarityVersion, #[case] _epoch: StacksEpoch // simple ( "(as-contract? ((with-stx u1000)) true)", - TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap() + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::UIntType).unwrap() ), // no allowances ( "(as-contract? () true)", - TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap() + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::UIntType).unwrap() ), // multiple allowances ( "(as-contract? ((with-stx u1000) (with-ft .token \"foo\" u5000) (with-nft .token \"foo\" (list 0x01)) (with-stacking u1000)) true)", - TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap() + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::UIntType).unwrap() ), // multiple body expressions ( "(as-contract? ((with-stx u1000)) (+ u1 u2) true)", - TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap() + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::UIntType).unwrap() ), // with-all-assets-unsafe ( "(as-contract? ((with-all-assets-unsafe)) true)", - TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap() + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::UIntType).unwrap() ), ]; @@ -349,22 +349,22 @@ fn test_with_stx_allowance(#[case] version: ClarityVersion, #[case] _epoch: Stac // basic usage ( "(restrict-assets? tx-sender ((with-stx u1000)) true)", - TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap() + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::UIntType).unwrap() ), // zero amount ( "(restrict-assets? tx-sender ((with-stx u0)) true)", - TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap() + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::UIntType).unwrap() ), // large amount ( "(restrict-assets? tx-sender ((with-stx u340282366920938463463374607431768211455)) true)", - TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap() + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::UIntType).unwrap() ), // variable amount ( "(let ((amount u1000)) (restrict-assets? tx-sender ((with-stx amount)) true))", - TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap() + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::UIntType).unwrap() ), ]; @@ -438,37 +438,37 @@ fn test_with_ft_allowance(#[case] version: ClarityVersion, #[case] _epoch: Stack // basic usage with shortcut contract principal ( r#"(restrict-assets? tx-sender ((with-ft .token "token-name" u1000)) true)"#, - TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(), + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::UIntType).unwrap(), ), // full literal principal ( r#"(restrict-assets? tx-sender ((with-ft 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.token "token-name" u1000)) true)"#, - TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(), + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::UIntType).unwrap(), ), // variable principal ( r#"(let ((contract .token)) (restrict-assets? tx-sender ((with-ft contract "token-name" u1000)) true))"#, - TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(), + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::UIntType).unwrap(), ), // variable token name ( r#"(let ((name "token-name")) (restrict-assets? tx-sender ((with-ft .token name u1000)) true))"#, - TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(), + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::UIntType).unwrap(), ), // variable amount ( r#"(let ((amount u1000)) (restrict-assets? tx-sender ((with-ft .token "token-name" amount)) true))"#, - TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(), + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::UIntType).unwrap(), ), // "*" token name ( r#"(restrict-assets? tx-sender ((with-ft .token "*" u1000)) true)"#, - TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(), + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::UIntType).unwrap(), ), // empty token name ( r#"(restrict-assets? tx-sender ((with-ft .token "" u1000)) true)"#, - TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(), + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::UIntType).unwrap(), ), ]; @@ -576,47 +576,47 @@ fn test_with_nft_allowance(#[case] version: ClarityVersion, #[case] _epoch: Stac // basic usage with shortcut contract principal ( r#"(restrict-assets? tx-sender ((with-nft .token "token-name" (list u1000))) true)"#, - TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(), + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::UIntType).unwrap(), ), // full literal principal ( r#"(restrict-assets? tx-sender ((with-ft 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.token "token-name" u1000)) true)"#, - TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(), + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::UIntType).unwrap(), ), // variable principal ( r#"(let ((contract .token)) (restrict-assets? tx-sender ((with-nft contract "token-name" (list u1000))) true))"#, - TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(), + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::UIntType).unwrap(), ), // variable token name ( r#"(let ((name "token-name")) (restrict-assets? tx-sender ((with-nft .token name (list u1000))) true))"#, - TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(), + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::UIntType).unwrap(), ), // "*" token name ( r#"(restrict-assets? tx-sender ((with-nft .token "*" (list u1000))) true)"#, - TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(), + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::UIntType).unwrap(), ), // empty token name ( r#"(restrict-assets? tx-sender ((with-nft .token "" (list u1000))) true)"#, - TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(), + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::UIntType).unwrap(), ), // string asset-id ( r#"(restrict-assets? tx-sender ((with-nft .token "token-name" (list "asset-123"))) true)"#, - TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(), + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::UIntType).unwrap(), ), // buffer asset-id ( r#"(restrict-assets? tx-sender ((with-nft .token "token-name" (list 0x0123456789))) true)"#, - TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(), + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::UIntType).unwrap(), ), // variable asset-id ( r#"(let ((asset-id (list u123))) (restrict-assets? tx-sender ((with-nft .token "token-name" asset-id)) true))"#, - TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(), + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::UIntType).unwrap(), ), ]; @@ -718,17 +718,17 @@ fn test_with_stacking_allowance(#[case] version: ClarityVersion, #[case] _epoch: // basic usage ( "(restrict-assets? tx-sender ((with-stacking u1000)) true)", - TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(), + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::UIntType).unwrap(), ), // zero amount ( "(restrict-assets? tx-sender ((with-stacking u0)) true)", - TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(), + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::UIntType).unwrap(), ), // variable amount ( "(let ((amount u1000)) (restrict-assets? tx-sender ((with-stacking amount)) true))", - TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(), + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::UIntType).unwrap(), ), ]; @@ -805,7 +805,7 @@ fn test_with_all_assets_unsafe_allowance( // basic usage ( "(as-contract? ((with-all-assets-unsafe)) true)", - TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::IntType).unwrap(), + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::UIntType).unwrap(), ), ]; diff --git a/clarity/src/vm/docs/mod.rs b/clarity/src/vm/docs/mod.rs index bd37d3b654..8093b0fdc9 100644 --- a/clarity/src/vm/docs/mod.rs +++ b/clarity/src/vm/docs/mod.rs @@ -2563,7 +2563,7 @@ characters.", }; const RESTRICT_ASSETS: SpecialAPI = SpecialAPI { - input_type: "principal, ((Allowance)*), AnyType, ... A", + input_type: "principal, ((Allowance){0,128}), AnyType, ... A", snippet: "restrict-assets? ${1:asset-owner} (${2:allowance-1} ${3:allowance-2}) ${4:expr-1}", output_type: "(response A int)", signature: "(restrict-assets? asset-owner ((with-stx|with-ft|with-nft|with-stacking)*) expr-body1 expr-body2 ... expr-body-last)", @@ -2580,21 +2580,21 @@ error-prone). Returns: result of the final body expression and has type `A`. * `(err index)` if an allowance was violated, where `index` is the 0-based index of the first violated allowance in the list of granted allowances, - or -1 if an asset with no allowance caused the violation.", + or `u128` if an asset with no allowance caused the violation.", example: r#" (restrict-assets? tx-sender () (+ u1 u2) ) ;; Returns (ok u3) (restrict-assets? tx-sender () (try! (stx-transfer? u50 tx-sender 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM)) -) ;; Returns (err -1) +) ;; Returns (err u128) "#, }; const AS_CONTRACT_SAFE: SpecialAPI = SpecialAPI { - input_type: "((Allowance)*), AnyType, ... A", + input_type: "((Allowance){0,128}), AnyType, ... A", snippet: "as-contract? (${1:allowance-1} ${2:allowance-2}) ${3:expr-1}", - output_type: "(response A int)", + output_type: "(response A uint)", signature: "(as-contract? ((with-stx|with-ft|with-nft|with-stacking)*) expr-body1 expr-body2 ... expr-body-last)", description: "Switches the current context's `tx-sender` and `contract-caller` values to the contract's principal and executes the body @@ -2610,7 +2610,7 @@ value from `as-contract?` (nested responses are error-prone). Returns: result of the final body expression and has type `A`. * `(err index)` if an allowance was violated, where `index` is the 0-based index of the first violated allowance in the list of granted allowances, - or -1 if an asset with no allowance caused the violation.", + or `u128` if an asset with no allowance caused the violation.", example: r#" (let ((recipient tx-sender)) (as-contract? ((with-stx u100)) @@ -2621,7 +2621,7 @@ value from `as-contract?` (nested responses are error-prone). Returns: (as-contract? () (try! (stx-transfer? u50 tx-sender recipient)) ) -) ;; Returns (err -1) +) ;; Returns (err u128) "#, }; @@ -2668,7 +2668,7 @@ the contract. When `"*"` is used for the token name, the allowance applies to (restrict-assets? tx-sender ((with-ft current-contract "stackaroo" u50)) (try! (ft-transfer? stackaroo u100 tx-sender 'SPAXYA5XS51713FDTQ8H94EJ4V579CXMTRNBZKSF)) -) ;; Returns (err 0) +) ;; Returns (err u0) "#, }; @@ -2696,7 +2696,7 @@ the token name, the allowance applies to **all** NFTs defined in `contract-id`." (restrict-assets? tx-sender ((with-nft current-contract "stackaroo" (list u125))) (try! (nft-transfer? stackaroo u124 tx-sender 'SPAXYA5XS51713FDTQ8H94EJ4V579CXMTRNBZKSF)) -) ;; Returns (err 0) +) ;; Returns (err u0) "#, }; @@ -2717,7 +2717,7 @@ locked amount is limited by the amount of uSTX specified.", (try! (contract-call? 'SP000000000000000000002Q6VF78.pox-4 delegate-stx u1100000000000 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM none none )) -) ;; Returns (err 0) +) ;; Returns (err u0) (restrict-assets? tx-sender ((with-stacking u1000000000000)) (try! (contract-call? 'SP000000000000000000002Q6VF78.pox-4 delegate-stx diff --git a/clarity/src/vm/functions/post_conditions.rs b/clarity/src/vm/functions/post_conditions.rs index ad320bb021..f19bfad864 100644 --- a/clarity/src/vm/functions/post_conditions.rs +++ b/clarity/src/vm/functions/post_conditions.rs @@ -17,6 +17,7 @@ use std::collections::{HashMap, HashSet}; use clarity_types::types::{AssetIdentifier, PrincipalData}; +use crate::vm::analysis::type_checker::v2_1::natives::post_conditions::MAX_ALLOWANCES; use crate::vm::contexts::AssetMap; use crate::vm::costs::cost_functions::ClarityCostFunction; use crate::vm::costs::{constants as cost_constants, runtime_cost, CostTracker}; @@ -205,7 +206,7 @@ pub fn special_restrict_assets( if let Some(violation_index) = check_allowances(&asset_owner, &allowances, asset_maps)? { env.global_context.roll_back()?; // TODO: Emit an event about the allowance violation - return Value::error(Value::Int(violation_index)); + return Value::error(Value::UInt(violation_index)); } env.global_context.commit()?; @@ -279,7 +280,7 @@ pub fn special_as_contract( Ok(Some(violation_index)) => { nested_env.global_context.roll_back()?; // TODO: Emit an event about the allowance violation - return Value::error(Value::Int(violation_index)); + return Value::error(Value::UInt(violation_index)); } Err(e) => { nested_env.global_context.roll_back()?; @@ -299,13 +300,13 @@ pub fn special_as_contract( /// Check the allowances against the asset map. If any assets moved without a /// corresponding allowance return a `Some` with an index of the violated -/// allowance, or -1 if an asset with no allowance caused the violation. If all +/// allowance, or 128 if an asset with no allowance caused the violation. If all /// allowances are satisfied, return `Ok(None)`. fn check_allowances( owner: &PrincipalData, allowances: &[Allowance], assets: &AssetMap, -) -> InterpreterResult> { +) -> InterpreterResult> { // Elements are (index in allowances, amount) let mut stx_allowances: Vec<(usize, u128)> = Vec::new(); // Map assets to a vector of (index in allowances, amount) @@ -349,14 +350,14 @@ fn check_allowances( if let Some(stx_moved) = assets.get_stx(owner) { // If there are no allowances for STX, any movement is a violation if stx_allowances.is_empty() { - return Ok(Some(-1)); + return Ok(Some(MAX_ALLOWANCES as u128)); } // Check against the STX allowances for (index, allowance) in &stx_allowances { if stx_moved > *allowance { - return Ok(Some(i128::try_from(*index).map_err(|_| { - InterpreterError::Expect("failed to convert index to i128".into()) + return Ok(Some(u128::try_from(*index).map_err(|_| { + InterpreterError::Expect("failed to convert index to u128".into()) })?)); } } @@ -366,14 +367,14 @@ fn check_allowances( if let Some(stx_burned) = assets.get_stx_burned(owner) { // If there are no allowances for STX, any burn is a violation if stx_allowances.is_empty() { - return Ok(Some(-1)); + return Ok(Some(MAX_ALLOWANCES as u128)); } // Check against the STX allowances for (index, allowance) in &stx_allowances { if stx_burned > *allowance { - return Ok(Some(i128::try_from(*index).map_err(|_| { - InterpreterError::Expect("failed to convert index to i128".into()) + return Ok(Some(u128::try_from(*index).map_err(|_| { + InterpreterError::Expect("failed to convert index to u128".into()) })?)); } } @@ -398,7 +399,7 @@ fn check_allowances( if merged.is_empty() { // No allowance for this asset, any movement is a violation - return Ok(Some(-1)); + return Ok(Some(MAX_ALLOWANCES as u128)); } // Sort by allowance index so we check allowances in order @@ -406,8 +407,8 @@ fn check_allowances( for (index, allowance) in merged { if *amount_moved > allowance { - return Ok(Some(i128::try_from(index).map_err(|_| { - InterpreterError::Expect("failed to convert index to i128".into()) + return Ok(Some(u128::try_from(index).map_err(|_| { + InterpreterError::Expect("failed to convert index to u128".into()) })?)); } } @@ -431,7 +432,7 @@ fn check_allowances( if merged.is_empty() { // No allowance for this asset, any movement is a violation - return Ok(Some(-1)); + return Ok(Some(MAX_ALLOWANCES as u128)); } // Sort by allowance index so we check allowances in order @@ -441,8 +442,8 @@ fn check_allowances( // Check against the NFT allowances for id_moved in ids_moved { if !allowance_map.contains(&id_moved.serialize_to_hex()?) { - return Ok(Some(i128::try_from(index).map_err(|_| { - InterpreterError::Expect("failed to convert index to i128".into()) + return Ok(Some(u128::try_from(index).map_err(|_| { + InterpreterError::Expect("failed to convert index to u128".into()) })?)); } } @@ -454,14 +455,14 @@ fn check_allowances( if let Some(stx_stacked) = assets.get_stacking(owner) { // If there are no allowances for stacking, any stacking is a violation if stacking_allowances.is_empty() { - return Ok(Some(-1)); + return Ok(Some(MAX_ALLOWANCES as u128)); } // Check against the stacking allowances for (index, allowance) in &stacking_allowances { if stx_stacked > *allowance { - return Ok(Some(i128::try_from(*index).map_err(|_| { - InterpreterError::Expect("failed to convert index to i128".into()) + return Ok(Some(u128::try_from(*index).map_err(|_| { + InterpreterError::Expect("failed to convert index to u128".into()) })?)); } } diff --git a/clarity/src/vm/tests/post_conditions.rs b/clarity/src/vm/tests/post_conditions.rs index 71c7e28d60..139d80eb1f 100644 --- a/clarity/src/vm/tests/post_conditions.rs +++ b/clarity/src/vm/tests/post_conditions.rs @@ -22,6 +22,7 @@ use clarity_types::types::{PrincipalData, QualifiedContractIdentifier, StandardP use clarity_types::Value; use stacks_common::types::StacksEpochId; +use crate::vm::analysis::type_checker::v2_1::natives::post_conditions::MAX_ALLOWANCES; use crate::vm::database::STXBalance; use crate::vm::{execute_with_parameters_and_call_in_global_context, ClarityVersion}; @@ -77,7 +78,7 @@ fn test_as_contract_with_stx_exceeds() { (try! (stx-transfer? u50 tx-sender recipient)) ) )"#; - let expected = Value::error(Value::Int(0)).unwrap(); + let expected = Value::error(Value::UInt(0)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -89,7 +90,7 @@ fn test_as_contract_with_stx_no_allowance() { (try! (stx-transfer? u50 tx-sender recipient)) ) )"#; - let expected = Value::error(Value::Int(-1)).unwrap(); + let expected = Value::error(Value::UInt(MAX_ALLOWANCES as u128)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -113,7 +114,7 @@ fn test_as_contract_stx_other_allowances() { (try! (stx-transfer? u50 tx-sender recipient)) ) )"#; - let expected = Value::error(Value::Int(-1)).unwrap(); + let expected = Value::error(Value::UInt(MAX_ALLOWANCES as u128)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -133,7 +134,7 @@ fn test_as_contract_with_stx_burn_exceeds() { (as-contract? ((with-stx u10)) (try! (stx-burn? u50 tx-sender)) )"#; - let expected = Value::error(Value::Int(0)).unwrap(); + let expected = Value::error(Value::UInt(0)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -143,7 +144,7 @@ fn test_as_contract_with_stx_burn_no_allowance() { (as-contract? () (try! (stx-burn? u50 tx-sender)) )"#; - let expected = Value::error(Value::Int(-1)).unwrap(); + let expected = Value::error(Value::UInt(MAX_ALLOWANCES as u128)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -163,7 +164,7 @@ fn test_as_contract_stx_burn_other_allowances() { (as-contract? ((with-ft .token "stackaroo" u100) (with-nft .token "stackaroo" (list 123))) (try! (stx-burn? u50 tx-sender)) )"#; - let expected = Value::error(Value::Int(-1)).unwrap(); + let expected = Value::error(Value::UInt(MAX_ALLOWANCES as u128)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -175,7 +176,7 @@ fn test_as_contract_multiple_allowances_both_low() { (try! (stx-transfer? u40 tx-sender recipient)) ) )"#; - let expected = Value::error(Value::Int(0)).unwrap(); + let expected = Value::error(Value::UInt(0)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -199,7 +200,7 @@ fn test_as_contract_multiple_allowances_one_low() { (try! (stx-transfer? u40 tx-sender recipient)) ) )"#; - let expected = Value::error(Value::Int(1)).unwrap(); + let expected = Value::error(Value::UInt(1)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -227,7 +228,7 @@ fn test_as_contract_with_ft_exceeds() { (try! (ft-transfer? stackaroo u50 tx-sender recipient)) ) )"#; - let expected = Value::error(Value::Int(0)).unwrap(); + let expected = Value::error(Value::UInt(0)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -241,7 +242,7 @@ fn test_as_contract_with_ft_no_allowance() { (try! (ft-transfer? stackaroo u50 tx-sender recipient)) ) )"#; - let expected = Value::error(Value::Int(-1)).unwrap(); + let expected = Value::error(Value::UInt(MAX_ALLOWANCES as u128)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -275,7 +276,7 @@ fn test_as_contract_with_ft_other_allowances() { (try! (ft-transfer? stackaroo u50 tx-sender recipient)) ) )"#; - let expected = Value::error(Value::Int(-1)).unwrap(); + let expected = Value::error(Value::UInt(MAX_ALLOWANCES as u128)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -289,7 +290,7 @@ fn test_as_contract_with_ft_multiple_allowances_both_low() { (try! (ft-transfer? stackaroo u40 tx-sender recipient)) ) )"#; - let expected = Value::error(Value::Int(0)).unwrap(); + let expected = Value::error(Value::UInt(0)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -317,7 +318,7 @@ fn test_as_contract_with_ft_multiple_allowances_one_low() { (try! (ft-transfer? stackaroo u40 tx-sender recipient)) ) )"#; - let expected = Value::error(Value::Int(1)).unwrap(); + let expected = Value::error(Value::UInt(1)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -345,7 +346,7 @@ fn test_as_contract_with_ft_wildcard_exceeds() { (try! (ft-transfer? stackaroo u50 tx-sender recipient)) ) )"#; - let expected = Value::error(Value::Int(0)).unwrap(); + let expected = Value::error(Value::UInt(0)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -365,7 +366,7 @@ fn test_as_contract_with_ft_wildcard_other_allowances() { (try! (ft-transfer? stackaroo u50 tx-sender recipient)) ) )"#; - let expected = Value::error(Value::Int(-1)).unwrap(); + let expected = Value::error(Value::UInt(MAX_ALLOWANCES as u128)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -379,7 +380,7 @@ fn test_as_contract_with_ft_wildcard_multiple_allowances_both_low() { (try! (ft-transfer? stackaroo u40 tx-sender recipient)) ) )"#; - let expected = Value::error(Value::Int(0)).unwrap(); + let expected = Value::error(Value::UInt(0)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -407,7 +408,7 @@ fn test_as_contract_with_ft_wildcard_multiple_allowances_one_low() { (try! (ft-transfer? stackaroo u40 tx-sender recipient)) ) )"#; - let expected = Value::error(Value::Int(1)).unwrap(); + let expected = Value::error(Value::UInt(1)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -421,7 +422,7 @@ fn test_as_contract_with_ft_wildcard_multiple_allowances_low1() { (try! (ft-transfer? stackaroo u40 tx-sender recipient)) ) )"#; - let expected = Value::error(Value::Int(0)).unwrap(); + let expected = Value::error(Value::UInt(0)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -435,7 +436,7 @@ fn test_as_contract_with_ft_wildcard_multiple_allowances_low2() { (try! (ft-transfer? stackaroo u40 tx-sender recipient)) ) )"#; - let expected = Value::error(Value::Int(0)).unwrap(); + let expected = Value::error(Value::UInt(0)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -464,7 +465,7 @@ fn test_as_contract_with_nft_not_allowed() { (try! (nft-transfer? stackaroo u123 tx-sender recipient)) ) )"#; - let expected = Value::error(Value::Int(0)).unwrap(); + let expected = Value::error(Value::UInt(0)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -478,7 +479,7 @@ fn test_as_contract_with_nft_no_allowance() { (try! (nft-transfer? stackaroo u123 tx-sender recipient)) ) )"#; - let expected = Value::error(Value::Int(-1)).unwrap(); + let expected = Value::error(Value::UInt(MAX_ALLOWANCES as u128)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -512,7 +513,7 @@ fn test_as_contract_with_nft_other_allowances() { (try! (nft-transfer? stackaroo u123 tx-sender recipient)) ) )"#; - let expected = Value::error(Value::Int(-1)).unwrap(); + let expected = Value::error(Value::UInt(MAX_ALLOWANCES as u128)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -527,7 +528,7 @@ fn test_as_contract_with_nft_multiple_allowances_both_different() { (try! (nft-transfer? stackaroo u123 tx-sender recipient)) ) )"#; - let expected = Value::error(Value::Int(0)).unwrap(); + let expected = Value::error(Value::UInt(0)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -572,7 +573,7 @@ fn test_as_contract_with_nft_empty_id_list() { (try! (nft-transfer? stackaroo u123 tx-sender recipient)) ) )"#; - let expected = Value::error(Value::Int(0)).unwrap(); + let expected = Value::error(Value::UInt(0)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -601,7 +602,7 @@ fn test_as_contract_with_nft_wildcard_not_allowed() { (try! (nft-transfer? stackaroo u123 tx-sender recipient)) ) )"#; - let expected = Value::error(Value::Int(0)).unwrap(); + let expected = Value::error(Value::UInt(0)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -621,7 +622,7 @@ fn test_as_contract_with_nft_wildcard_other_allowances() { (try! (nft-transfer? stackaroo u123 tx-sender recipient)) ) )"#; - let expected = Value::error(Value::Int(-1)).unwrap(); + let expected = Value::error(Value::UInt(MAX_ALLOWANCES as u128)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -636,7 +637,7 @@ fn test_as_contract_with_nft_wildcard_multiple_allowances_both_different() { (try! (nft-transfer? stackaroo u123 tx-sender recipient)) ) )"#; - let expected = Value::error(Value::Int(0)).unwrap(); + let expected = Value::error(Value::UInt(0)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -681,7 +682,7 @@ fn test_as_contract_with_nft_wildcard_empty_id_list() { (try! (nft-transfer? stackaroo u123 tx-sender recipient)) ) )"#; - let expected = Value::error(Value::Int(0)).unwrap(); + let expected = Value::error(Value::UInt(0)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -696,7 +697,7 @@ fn test_as_contract_with_nft_wildcard_multiple_allowances_order1() { (try! (nft-transfer? stackaroo u123 tx-sender recipient)) ) )"#; - let expected = Value::error(Value::Int(0)).unwrap(); + let expected = Value::error(Value::UInt(0)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -711,7 +712,7 @@ fn test_as_contract_with_nft_wildcard_multiple_allowances_order2() { (try! (nft-transfer? stackaroo u123 tx-sender recipient)) ) )"#; - let expected = Value::error(Value::Int(0)).unwrap(); + let expected = Value::error(Value::UInt(0)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -733,7 +734,7 @@ fn test_restrict_assets_with_stx_exceeds() { (restrict-assets? tx-sender ((with-stx u10)) (try! (stx-transfer? u50 tx-sender 'SP000000000000000000002Q6VF78)) )"#; - let expected = Value::error(Value::Int(0)).unwrap(); + let expected = Value::error(Value::UInt(0)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -743,7 +744,7 @@ fn test_restrict_assets_with_stx_no_allowance() { (restrict-assets? tx-sender () (try! (stx-transfer? u50 tx-sender 'SP000000000000000000002Q6VF78)) )"#; - let expected = Value::error(Value::Int(-1)).unwrap(); + let expected = Value::error(Value::UInt(MAX_ALLOWANCES as u128)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -763,7 +764,7 @@ fn test_restrict_assets_stx_other_allowances() { (restrict-assets? tx-sender ((with-ft .token "stackaroo" u100) (with-nft .token "stackaroo" (list 123))) (try! (stx-transfer? u50 tx-sender 'SP000000000000000000002Q6VF78)) )"#; - let expected = Value::error(Value::Int(-1)).unwrap(); + let expected = Value::error(Value::UInt(MAX_ALLOWANCES as u128)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -783,7 +784,7 @@ fn test_restrict_assets_with_stx_burn_exceeds() { (restrict-assets? tx-sender ((with-stx u10)) (try! (stx-burn? u50 tx-sender)) )"#; - let expected = Value::error(Value::Int(0)).unwrap(); + let expected = Value::error(Value::UInt(0)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -793,7 +794,7 @@ fn test_restrict_assets_with_stx_burn_no_allowance() { (restrict-assets? tx-sender () (try! (stx-burn? u50 tx-sender)) )"#; - let expected = Value::error(Value::Int(-1)).unwrap(); + let expected = Value::error(Value::UInt(MAX_ALLOWANCES as u128)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -813,7 +814,7 @@ fn test_restrict_assets_stx_burn_other_allowances() { (restrict-assets? tx-sender ((with-ft .token "stackaroo" u100) (with-nft .token "stackaroo" (list 123))) (try! (stx-burn? u50 tx-sender)) )"#; - let expected = Value::error(Value::Int(-1)).unwrap(); + let expected = Value::error(Value::UInt(MAX_ALLOWANCES as u128)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -823,7 +824,7 @@ fn test_restrict_assets_multiple_allowances_both_low() { (restrict-assets? tx-sender ((with-stx u30) (with-stx u20)) (try! (stx-transfer? u40 tx-sender 'SP000000000000000000002Q6VF78)) )"#; - let expected = Value::error(Value::Int(0)).unwrap(); + let expected = Value::error(Value::UInt(0)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -843,7 +844,7 @@ fn test_restrict_assets_multiple_allowances_one_low() { (restrict-assets? tx-sender ((with-stx u100) (with-stx u20)) (try! (stx-transfer? u40 tx-sender 'SP000000000000000000002Q6VF78)) )"#; - let expected = Value::error(Value::Int(1)).unwrap(); + let expected = Value::error(Value::UInt(1)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -871,7 +872,7 @@ fn test_restrict_assets_with_ft_exceeds() { (try! (ft-transfer? stackaroo u50 tx-sender recipient)) ) )"#; - let expected = Value::error(Value::Int(0)).unwrap(); + let expected = Value::error(Value::UInt(0)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -885,7 +886,7 @@ fn test_restrict_assets_with_ft_no_allowance() { (try! (ft-transfer? stackaroo u50 tx-sender recipient)) ) )"#; - let expected = Value::error(Value::Int(-1)).unwrap(); + let expected = Value::error(Value::UInt(MAX_ALLOWANCES as u128)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -919,7 +920,7 @@ fn test_restrict_assets_with_ft_other_allowances() { (try! (ft-transfer? stackaroo u50 tx-sender recipient)) ) )"#; - let expected = Value::error(Value::Int(-1)).unwrap(); + let expected = Value::error(Value::UInt(MAX_ALLOWANCES as u128)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -933,7 +934,7 @@ fn test_restrict_assets_with_ft_multiple_allowances_both_low() { (try! (ft-transfer? stackaroo u40 tx-sender recipient)) ) )"#; - let expected = Value::error(Value::Int(0)).unwrap(); + let expected = Value::error(Value::UInt(0)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -961,7 +962,7 @@ fn test_restrict_assets_with_ft_multiple_allowances_one_low() { (try! (ft-transfer? stackaroo u40 tx-sender recipient)) ) )"#; - let expected = Value::error(Value::Int(1)).unwrap(); + let expected = Value::error(Value::UInt(1)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -989,7 +990,7 @@ fn test_restrict_assets_with_ft_wildcard_exceeds() { (try! (ft-transfer? stackaroo u50 tx-sender recipient)) ) )"#; - let expected = Value::error(Value::Int(0)).unwrap(); + let expected = Value::error(Value::UInt(0)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -1009,7 +1010,7 @@ fn test_restrict_assets_with_ft_wildcard_other_allowances() { (try! (ft-transfer? stackaroo u50 tx-sender recipient)) ) )"#; - let expected = Value::error(Value::Int(-1)).unwrap(); + let expected = Value::error(Value::UInt(MAX_ALLOWANCES as u128)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -1023,7 +1024,7 @@ fn test_restrict_assets_with_ft_wildcard_multiple_allowances_both_low() { (try! (ft-transfer? stackaroo u40 tx-sender recipient)) ) )"#; - let expected = Value::error(Value::Int(0)).unwrap(); + let expected = Value::error(Value::UInt(0)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -1051,7 +1052,7 @@ fn test_restrict_assets_with_ft_wildcard_multiple_allowances_one_low() { (try! (ft-transfer? stackaroo u40 tx-sender recipient)) ) )"#; - let expected = Value::error(Value::Int(1)).unwrap(); + let expected = Value::error(Value::UInt(1)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -1065,7 +1066,7 @@ fn test_restrict_assets_with_ft_wildcard_multiple_allowances_low1() { (try! (ft-transfer? stackaroo u40 tx-sender recipient)) ) )"#; - let expected = Value::error(Value::Int(0)).unwrap(); + let expected = Value::error(Value::UInt(0)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -1079,7 +1080,7 @@ fn test_restrict_assets_with_ft_wildcard_multiple_allowances_low2() { (try! (ft-transfer? stackaroo u40 tx-sender recipient)) ) )"#; - let expected = Value::error(Value::Int(0)).unwrap(); + let expected = Value::error(Value::UInt(0)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -1108,7 +1109,7 @@ fn test_restrict_assets_with_nft_not_allowed() { (try! (nft-transfer? stackaroo u123 tx-sender recipient)) ) )"#; - let expected = Value::error(Value::Int(0)).unwrap(); + let expected = Value::error(Value::UInt(0)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -1122,7 +1123,7 @@ fn test_restrict_assets_with_nft_no_allowance() { (try! (nft-transfer? stackaroo u123 tx-sender recipient)) ) )"#; - let expected = Value::error(Value::Int(-1)).unwrap(); + let expected = Value::error(Value::UInt(MAX_ALLOWANCES as u128)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -1156,7 +1157,7 @@ fn test_restrict_assets_with_nft_other_allowances() { (try! (nft-transfer? stackaroo u123 tx-sender recipient)) ) )"#; - let expected = Value::error(Value::Int(-1)).unwrap(); + let expected = Value::error(Value::UInt(MAX_ALLOWANCES as u128)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -1171,7 +1172,7 @@ fn test_restrict_assets_with_nft_multiple_allowances_both_different() { (try! (nft-transfer? stackaroo u123 tx-sender recipient)) ) )"#; - let expected = Value::error(Value::Int(0)).unwrap(); + let expected = Value::error(Value::UInt(0)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -1216,7 +1217,7 @@ fn test_restrict_assets_with_nft_empty_id_list() { (try! (nft-transfer? stackaroo u123 tx-sender recipient)) ) )"#; - let expected = Value::error(Value::Int(0)).unwrap(); + let expected = Value::error(Value::UInt(0)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -1245,7 +1246,7 @@ fn test_restrict_assets_with_nft_wildcard_not_allowed() { (try! (nft-transfer? stackaroo u123 tx-sender recipient)) ) )"#; - let expected = Value::error(Value::Int(0)).unwrap(); + let expected = Value::error(Value::UInt(0)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -1265,7 +1266,7 @@ fn test_restrict_assets_with_nft_wildcard_other_allowances() { (try! (nft-transfer? stackaroo u123 tx-sender recipient)) ) )"#; - let expected = Value::error(Value::Int(-1)).unwrap(); + let expected = Value::error(Value::UInt(MAX_ALLOWANCES as u128)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -1280,7 +1281,7 @@ fn test_restrict_assets_with_nft_wildcard_multiple_allowances_both_different() { (try! (nft-transfer? stackaroo u123 tx-sender recipient)) ) )"#; - let expected = Value::error(Value::Int(0)).unwrap(); + let expected = Value::error(Value::UInt(0)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -1325,7 +1326,7 @@ fn test_restrict_assets_with_nft_wildcard_empty_id_list() { (try! (nft-transfer? stackaroo u123 tx-sender recipient)) ) )"#; - let expected = Value::error(Value::Int(0)).unwrap(); + let expected = Value::error(Value::UInt(0)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -1340,7 +1341,7 @@ fn test_restrict_assets_with_nft_wildcard_multiple_allowances_order1() { (try! (nft-transfer? stackaroo u123 tx-sender recipient)) ) )"#; - let expected = Value::error(Value::Int(0)).unwrap(); + let expected = Value::error(Value::UInt(0)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } @@ -1355,6 +1356,6 @@ fn test_restrict_assets_with_nft_wildcard_multiple_allowances_order2() { (try! (nft-transfer? stackaroo u123 tx-sender recipient)) ) )"#; - let expected = Value::error(Value::Int(0)).unwrap(); + let expected = Value::error(Value::UInt(0)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } diff --git a/stacks-node/src/tests/nakamoto_integrations.rs b/stacks-node/src/tests/nakamoto_integrations.rs index dbd5aaa9ed..62cb77c87a 100644 --- a/stacks-node/src/tests/nakamoto_integrations.rs +++ b/stacks-node/src/tests/nakamoto_integrations.rs @@ -15465,7 +15465,7 @@ fn check_with_stacking_allowances() { (define-public (revoke-delegate-stx) (as-contract? () (try! (contract-call? 'ST000000000000000000002AMW42H.pox-4 revoke-delegate-stx)) - (ok true) + true ) ) "# From 22449dd6d772d48405f5469ef74a1b02ccfa9650 Mon Sep 17 00:00:00 2001 From: Brice Dobry Date: Thu, 25 Sep 2025 15:23:44 -0400 Subject: [PATCH 20/35] fix: handle errors in `restrict-assets?` and `as-contract?` body --- .../v2_1/natives/post_conditions.rs | 4 +- .../v2_1/tests/post_conditions.rs | 156 ++++++++++++------ clarity/src/vm/functions/post_conditions.rs | 72 +++++--- clarity/src/vm/tests/post_conditions.rs | 32 +++- .../src/tests/nakamoto_integrations.rs | 33 ++-- 5 files changed, 208 insertions(+), 89 deletions(-) diff --git a/clarity/src/vm/analysis/type_checker/v2_1/natives/post_conditions.rs b/clarity/src/vm/analysis/type_checker/v2_1/natives/post_conditions.rs index 4f041f0794..8f0a0cbbac 100644 --- a/clarity/src/vm/analysis/type_checker/v2_1/natives/post_conditions.rs +++ b/clarity/src/vm/analysis/type_checker/v2_1/natives/post_conditions.rs @@ -25,7 +25,9 @@ use crate::vm::costs::cost_functions::ClarityCostFunction; use crate::vm::costs::runtime_cost; use crate::vm::functions::NativeFunctions; -/// Maximum number of allowances allowed in a `restrict-assets?` or `as-contract?` expression. +/// Maximum number of allowances allowed in a `restrict-assets?` or +/// `as-contract?` expression. This value is also used to indicate an allowance +/// violation for an asset with no allowances. pub(crate) const MAX_ALLOWANCES: usize = 128; /// Maximum number of asset identifiers allowed in a `with-nft` allowance expression. pub(crate) const MAX_NFT_IDENTIFIERS: u32 = 128; diff --git a/clarity/src/vm/analysis/type_checker/v2_1/tests/post_conditions.rs b/clarity/src/vm/analysis/type_checker/v2_1/tests/post_conditions.rs index 6f6a5ae4ff..21885f1604 100644 --- a/clarity/src/vm/analysis/type_checker/v2_1/tests/post_conditions.rs +++ b/clarity/src/vm/analysis/type_checker/v2_1/tests/post_conditions.rs @@ -155,43 +155,65 @@ fn test_restrict_assets(#[case] version: ClarityVersion, #[case] _epoch: StacksE ), CheckErrors::TooManyAllowances(MAX_ALLOWANCES, 130), ), + // different error types thrown from body expressions + ( + "(define-public (test) + (restrict-assets? tx-sender () + (try! (if true (ok true) (err u1))) + (try! (if true (ok 1) (err 2))) + u0 + ) + )", + CheckErrors::ReturnTypesMustMatch( + TypeSignature::new_response( + TypeSignature::NoType.into(), + TypeSignature::UIntType.into(), + ) + .unwrap() + .into(), + TypeSignature::new_response( + TypeSignature::NoType.into(), + TypeSignature::IntType.into(), + ) + .unwrap() + .into(), + ), + ), ]; - for (good_code, expected_type) in &good { - info!("test good code: '{}'", good_code); + for (code, expected_type) in &good { if version < ClarityVersion::Clarity4 { // restrict-assets? is only available in Clarity 4+ assert_eq!( CheckErrors::UnknownFunction("restrict-assets?".to_string()), - *type_check_helper_version(good_code, version) - .unwrap_err() - .err + *type_check_helper_version(code, version).unwrap_err().err, + "{code}", ); } else { assert_eq!( expected_type, - &type_check_helper_version(good_code, version).unwrap() + &type_check_helper_version(code, version).unwrap(), + "{code}", ); } } - for (bad_code, expected_err) in &bad { - info!("test bad code: '{}'", bad_code); + for (code, expected_err) in &bad { if version < ClarityVersion::Clarity4 { // restrict-assets? is only available in Clarity 4+ assert_eq!( CheckErrors::UnknownFunction("restrict-assets?".to_string()), - *type_check_helper_version(bad_code, version) - .unwrap_err() - .err + *type_check_helper_version(code, version).unwrap_err().err, + "{code}", ); } else { assert_eq!( expected_err, - type_check_helper_version(bad_code, version) + type_check_helper_version(code, version) .unwrap_err() .err - .as_ref() + .as_ref(), + "{code}", ); } } @@ -304,31 +326,56 @@ fn test_as_contract(#[case] version: ClarityVersion, #[case] _epoch: StacksEpoch ), CheckErrors::TooManyAllowances(MAX_ALLOWANCES, 130), ), + // different error types thrown from body expressions + ( + "(define-public (test) + (as-contract? () + (try! (if true (ok true) (err u1))) + (try! (if true (ok 1) (err 2))) + u0 + ) + )", + CheckErrors::ReturnTypesMustMatch( + TypeSignature::new_response( + TypeSignature::NoType.into(), + TypeSignature::UIntType.into(), + ) + .unwrap() + .into(), + TypeSignature::new_response( + TypeSignature::NoType.into(), + TypeSignature::IntType.into(), + ) + .unwrap() + .into(), + ), + ), ]; for (code, expected_type) in &good { - info!("test code: '{}'", code); if version < ClarityVersion::Clarity4 { // as-contract? is only available in Clarity 4+ assert_eq!( CheckErrors::UnknownFunction("as-contract?".to_string()), - *type_check_helper_version(code, version).unwrap_err().err + *type_check_helper_version(code, version).unwrap_err().err, + "{code}" ); } else { assert_eq!( expected_type, - &type_check_helper_version(code, version).unwrap() + &type_check_helper_version(code, version).unwrap(), + "{code}" ); } } for (code, expected_err) in &bad { - info!("test code: '{}'", code); if version < ClarityVersion::Clarity4 { // as-contract? is only available in Clarity 4+ assert_eq!( CheckErrors::UnknownFunction("as-contract?".to_string()), - *type_check_helper_version(code, version).unwrap_err().err + *type_check_helper_version(code, version).unwrap_err().err, + "{code}" ); } else { assert_eq!( @@ -336,7 +383,8 @@ fn test_as_contract(#[case] version: ClarityVersion, #[case] _epoch: StacksEpoch type_check_helper_version(code, version) .unwrap_err() .err - .as_ref() + .as_ref(), + "{code}" ); } } @@ -398,26 +446,27 @@ fn test_with_stx_allowance(#[case] version: ClarityVersion, #[case] _epoch: Stac ]; for (code, expected_type) in &good { - info!("test code: '{}'", code); if version < ClarityVersion::Clarity4 { assert_eq!( CheckErrors::UnknownFunction("restrict-assets?".to_string()), - *type_check_helper_version(code, version).unwrap_err().err + *type_check_helper_version(code, version).unwrap_err().err, + "{code}" ); } else { assert_eq!( expected_type, - &type_check_helper_version(code, version).unwrap() + &type_check_helper_version(code, version).unwrap(), + "{code}" ); } } for (code, expected_err) in &bad { - info!("test code: '{}'", code); if version < ClarityVersion::Clarity4 { assert_eq!( CheckErrors::UnknownFunction("restrict-assets?".to_string()), - *type_check_helper_version(code, version).unwrap_err().err + *type_check_helper_version(code, version).unwrap_err().err, + "{code}" ); } else { assert_eq!( @@ -425,7 +474,8 @@ fn test_with_stx_allowance(#[case] version: ClarityVersion, #[case] _epoch: Stac type_check_helper_version(code, version) .unwrap_err() .err - .as_ref() + .as_ref(), + "{code}" ); } } @@ -536,26 +586,27 @@ fn test_with_ft_allowance(#[case] version: ClarityVersion, #[case] _epoch: Stack ]; for (code, expected_type) in &good { - info!("test code: '{}'", code); if version < ClarityVersion::Clarity4 { assert_eq!( CheckErrors::UnknownFunction("restrict-assets?".to_string()), - *type_check_helper_version(code, version).unwrap_err().err + *type_check_helper_version(code, version).unwrap_err().err, + "{code}", ); } else { assert_eq!( expected_type, - &type_check_helper_version(code, version).unwrap() + &type_check_helper_version(code, version).unwrap(), + "{code}", ); } } for (code, expected_err) in &bad { - info!("test code: '{}'", code); if version < ClarityVersion::Clarity4 { assert_eq!( CheckErrors::UnknownFunction("restrict-assets?".to_string()), - *type_check_helper_version(code, version).unwrap_err().err + *type_check_helper_version(code, version).unwrap_err().err, + "{code}", ); } else { assert_eq!( @@ -563,7 +614,8 @@ fn test_with_ft_allowance(#[case] version: ClarityVersion, #[case] _epoch: Stack type_check_helper_version(code, version) .unwrap_err() .err - .as_ref() + .as_ref(), + "{code}", ); } } @@ -678,26 +730,27 @@ fn test_with_nft_allowance(#[case] version: ClarityVersion, #[case] _epoch: Stac ]; for (code, expected_type) in &good { - info!("test code: '{}'", code); if version < ClarityVersion::Clarity4 { assert_eq!( CheckErrors::UnknownFunction("restrict-assets?".to_string()), - *type_check_helper_version(code, version).unwrap_err().err + *type_check_helper_version(code, version).unwrap_err().err, + "{code}", ); } else { assert_eq!( expected_type, - &type_check_helper_version(code, version).unwrap() + &type_check_helper_version(code, version).unwrap(), + "{code}", ); } } for (code, expected_err) in &bad { - info!("test code: '{}'", code); if version < ClarityVersion::Clarity4 { assert_eq!( CheckErrors::UnknownFunction("restrict-assets?".to_string()), - *type_check_helper_version(code, version).unwrap_err().err + *type_check_helper_version(code, version).unwrap_err().err, + "{code}", ); } else { assert_eq!( @@ -705,7 +758,8 @@ fn test_with_nft_allowance(#[case] version: ClarityVersion, #[case] _epoch: Stac type_check_helper_version(code, version) .unwrap_err() .err - .as_ref() + .as_ref(), + "{code}", ); } } @@ -762,26 +816,27 @@ fn test_with_stacking_allowance(#[case] version: ClarityVersion, #[case] _epoch: ]; for (code, expected_type) in &good { - info!("test code: '{}'", code); if version < ClarityVersion::Clarity4 { assert_eq!( CheckErrors::UnknownFunction("restrict-assets?".to_string()), - *type_check_helper_version(code, version).unwrap_err().err + *type_check_helper_version(code, version).unwrap_err().err, + "{code}", ); } else { assert_eq!( expected_type, - &type_check_helper_version(code, version).unwrap() + &type_check_helper_version(code, version).unwrap(), + "{code}", ); } } for (code, expected_err) in &bad { - info!("test code: '{}'", code); if version < ClarityVersion::Clarity4 { assert_eq!( CheckErrors::UnknownFunction("restrict-assets?".to_string()), - *type_check_helper_version(code, version).unwrap_err().err + *type_check_helper_version(code, version).unwrap_err().err, + "{code}", ); } else { assert_eq!( @@ -789,7 +844,8 @@ fn test_with_stacking_allowance(#[case] version: ClarityVersion, #[case] _epoch: type_check_helper_version(code, version) .unwrap_err() .err - .as_ref() + .as_ref(), + "{code}", ); } } @@ -823,26 +879,27 @@ fn test_with_all_assets_unsafe_allowance( ]; for (code, expected_type) in &good { - info!("test code: '{}'", code); if version < ClarityVersion::Clarity4 { assert_eq!( CheckErrors::UnknownFunction("as-contract?".to_string()), - *type_check_helper_version(code, version).unwrap_err().err + *type_check_helper_version(code, version).unwrap_err().err, + "{code}", ); } else { assert_eq!( expected_type, - &type_check_helper_version(code, version).unwrap() + &type_check_helper_version(code, version).unwrap(), + "{code}", ); } } for (code, expected_err) in &bad { - info!("test code: '{}'", code); if version < ClarityVersion::Clarity4 { assert_eq!( CheckErrors::UnknownFunction("restrict-assets?".to_string()), - *type_check_helper_version(code, version).unwrap_err().err + *type_check_helper_version(code, version).unwrap_err().err, + "{code}", ); } else { assert_eq!( @@ -850,7 +907,8 @@ fn test_with_all_assets_unsafe_allowance( type_check_helper_version(code, version) .unwrap_err() .err - .as_ref() + .as_ref(), + "{code}", ); } } diff --git a/clarity/src/vm/functions/post_conditions.rs b/clarity/src/vm/functions/post_conditions.rs index f19bfad864..12dd2f3294 100644 --- a/clarity/src/vm/functions/post_conditions.rs +++ b/clarity/src/vm/functions/post_conditions.rs @@ -191,12 +191,15 @@ pub fn special_restrict_assets( // post-conditions are violated env.global_context.begin(); - // evaluate the body expressions - let mut last_result = None; - for expr in body_exprs { - let result = eval(expr, env, context)?; - last_result.replace(result); - } + // Evaluate the body expressions inside a closure so `?` only exits the closure + let eval_result: InterpreterResult> = (|| -> InterpreterResult> { + let mut last_result = None; + for expr in body_exprs { + let result = eval(expr, env, context)?; + last_result.replace(result); + } + Ok(last_result) + })(); let asset_maps = env.global_context.get_readonly_asset_map()?; @@ -211,11 +214,21 @@ pub fn special_restrict_assets( env.global_context.commit()?; - // Wrap the result in an `ok` value - Value::okay( - // last_result should always be Some(...), because of the arg len check above. - last_result.ok_or_else(|| InterpreterError::Expect("Failed to get let result".into()))?, - ) + // No allowance violation, so handle the result of the body evaluation + match eval_result { + Ok(Some(last)) => { + // body completed successfully — commit and return ok(last) + Value::okay(last) + } + Ok(None) => { + // Body had no expressions (shouldn't happen due to argument checks) + Err(InterpreterError::Expect("Failed to get body result".into()).into()) + } + Err(e) => { + // Runtime error inside body, pass it up + Err(e) + } + } } /// Handles the function `as-contract?` @@ -261,14 +274,15 @@ pub fn special_as_contract( // post-conditions are violated nested_env.global_context.begin(); - // evaluate the body expressions - let mut last_result = None; - for expr in body_exprs { - // TODO: handle runtime errors inside the body expressions correctly - // (ensure that the context is always popped and asset maps are checked against allowances) - let result = eval(expr, &mut nested_env, context)?; - last_result.replace(result); - } + // Evaluate the body expressions inside a closure so `?` only exits the closure + let eval_result: InterpreterResult> = (|| -> InterpreterResult> { + let mut last_result = None; + for expr in body_exprs { + let result = eval(expr, &mut nested_env, context)?; + last_result.replace(result); + } + Ok(last_result) + })(); let asset_maps = nested_env.global_context.get_readonly_asset_map()?; @@ -290,11 +304,21 @@ pub fn special_as_contract( nested_env.global_context.commit()?; - // Wrap the result in an `ok` value - Value::okay( - // last_result should always be Some(...), because of the arg len check above. - last_result.ok_or_else(|| InterpreterError::Expect("Failed to get let result".into()))?, - ) + // No allowance violation, so handle the result of the body evaluation + match eval_result { + Ok(Some(last)) => { + // body completed successfully — commit and return ok(last) + Value::okay(last) + } + Ok(None) => { + // Body had no expressions (shouldn't happen due to argument checks) + Err(InterpreterError::Expect("Failed to get body result".into()).into()) + } + Err(e) => { + // Runtime error inside body, pass it up + Err(e) + } + } }) } diff --git a/clarity/src/vm/tests/post_conditions.rs b/clarity/src/vm/tests/post_conditions.rs index 139d80eb1f..7a966b56d7 100644 --- a/clarity/src/vm/tests/post_conditions.rs +++ b/clarity/src/vm/tests/post_conditions.rs @@ -17,7 +17,7 @@ //! `restrict-assets?` expressions. The `with-stacking` allowances are tested //! in integration tests, since they require changes made outside of the VM. -use clarity_types::errors::InterpreterResult; +use clarity_types::errors::{Error as ClarityError, InterpreterResult, ShortReturnType}; use clarity_types::types::{PrincipalData, QualifiedContractIdentifier, StandardPrincipalData}; use clarity_types::Value; use stacks_common::types::StacksEpochId; @@ -716,6 +716,21 @@ fn test_as_contract_with_nft_wildcard_multiple_allowances_order2() { assert_eq!(expected, execute(snippet).unwrap().unwrap()); } +#[test] +fn test_as_contract_with_error_in_body() { + let snippet = r#" +(let ((recipient tx-sender)) + (as-contract? () + (try! (if false (ok true) (err u200))) + true + ) +)"#; + let expected_err = Value::error(Value::UInt(200)).unwrap(); + let short_return = + ClarityError::ShortReturn(ShortReturnType::ExpectedValue(expected_err.into())); + assert_eq!(short_return, execute(snippet).unwrap_err()); +} + // ---------- Tests for restrict-assets? ---------- #[test] @@ -1359,3 +1374,18 @@ fn test_restrict_assets_with_nft_wildcard_multiple_allowances_order2() { let expected = Value::error(Value::UInt(0)).unwrap(); assert_eq!(expected, execute(snippet).unwrap().unwrap()); } + +#[test] +fn test_restrict_assets_with_error_in_body() { + let snippet = r#" +(let ((recipient 'SP000000000000000000002Q6VF78)) + (restrict-assets? tx-sender () + (try! (if false (ok true) (err u200))) + true + ) +)"#; + let expected_err = Value::error(Value::UInt(200)).unwrap(); + let short_return = + ClarityError::ShortReturn(ShortReturnType::ExpectedValue(expected_err.into())); + assert_eq!(short_return, execute(snippet).unwrap_err()); +} diff --git a/stacks-node/src/tests/nakamoto_integrations.rs b/stacks-node/src/tests/nakamoto_integrations.rs index 62cb77c87a..d51fc869f6 100644 --- a/stacks-node/src/tests/nakamoto_integrations.rs +++ b/stacks-node/src/tests/nakamoto_integrations.rs @@ -15436,35 +15436,35 @@ fn check_with_stacking_allowances() { r#" (define-public (delegate-stx (amount uint) (allowed uint)) (as-contract? ((with-stacking allowed)) - (try! (contract-call? 'ST000000000000000000002AMW42H.pox-4 delegate-stx + (unwrap! (contract-call? 'ST000000000000000000002AMW42H.pox-4 delegate-stx amount 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM none none - )) + ) (err u1)) ) ) (define-public (delegate-stx-2-allowances (amount uint) (allowed-1 uint) (allowed-2 uint)) (as-contract? ((with-stacking allowed-1) (with-stacking allowed-2)) - (try! (contract-call? 'ST000000000000000000002AMW42H.pox-4 delegate-stx + (unwrap! (contract-call? 'ST000000000000000000002AMW42H.pox-4 delegate-stx amount 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM none none - )) + ) (err u1)) ) ) (define-public (delegate-stx-no-allowance (amount uint)) (as-contract? () - (try! (contract-call? 'ST000000000000000000002AMW42H.pox-4 delegate-stx + (unwrap! (contract-call? 'ST000000000000000000002AMW42H.pox-4 delegate-stx amount 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM none none - )) + ) (err u1)) ) ) (define-public (delegate-stx-all (amount uint)) (as-contract? ((with-all-assets-unsafe)) - (try! (contract-call? 'ST000000000000000000002AMW42H.pox-4 delegate-stx + (unwrap! (contract-call? 'ST000000000000000000002AMW42H.pox-4 delegate-stx amount 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM none none - )) + ) (err u1)) ) ) (define-public (revoke-delegate-stx) (as-contract? () - (try! (contract-call? 'ST000000000000000000002AMW42H.pox-4 revoke-delegate-stx)) + (unwrap! (contract-call? 'ST000000000000000000002AMW42H.pox-4 revoke-delegate-stx) (err u1)) true ) ) @@ -15543,7 +15543,7 @@ fn check_with_stacking_allowances() { sender_nonce += 1; let delegate_err_txid = submit_tx(&http_origin, &delegate_err_tx); info!("Submitted delegate_err txid: {delegate_err_txid}"); - expected_results.insert(delegate_err_txid, Value::error(Value::Int(0)).unwrap()); + expected_results.insert(delegate_err_txid, Value::error(Value::UInt(0)).unwrap()); let delegate_2_ok_tx = make_contract_call( &sender_sk, @@ -15590,7 +15590,7 @@ fn check_with_stacking_allowances() { info!("Submitted delegate_2_both_err txid: {delegate_2_both_err_txid}"); expected_results.insert( delegate_2_both_err_txid, - Value::error(Value::Int(0)).unwrap(), + Value::error(Value::UInt(0)).unwrap(), ); let delegate_2_first_err_tx = make_contract_call( @@ -15608,7 +15608,7 @@ fn check_with_stacking_allowances() { info!("Submitted delegate_2_first_err txid: {delegate_2_first_err_txid}"); expected_results.insert( delegate_2_first_err_txid, - Value::error(Value::Int(0)).unwrap(), + Value::error(Value::UInt(0)).unwrap(), ); let delegate_2_second_err_tx = make_contract_call( @@ -15626,7 +15626,7 @@ fn check_with_stacking_allowances() { info!("Submitted delegate_2_second_err txid: {delegate_2_second_err_txid}"); expected_results.insert( delegate_2_second_err_txid, - Value::error(Value::Int(1)).unwrap(), + Value::error(Value::UInt(1)).unwrap(), ); let delegate_no_allowance_err_tx = make_contract_call( @@ -15644,7 +15644,7 @@ fn check_with_stacking_allowances() { info!("Submitted delegate_no_allowance_err txid: {delegate_no_allowance_err_txid}"); expected_results.insert( delegate_no_allowance_err_txid, - Value::error(Value::Int(-1)).unwrap(), + Value::error(Value::UInt(128)).unwrap(), ); let delegate_all_tx = make_contract_call( @@ -15721,3 +15721,8 @@ fn check_with_stacking_allowances() { run_loop_thread.join().unwrap(); } + +// TODO: +// - Test stack-stx +// - Test roll backs +// - Test successful asset movement \ No newline at end of file From 00601292919541f1bb1eba02125fadd609487a51 Mon Sep 17 00:00:00 2001 From: Brice Dobry Date: Thu, 25 Sep 2025 15:31:51 -0400 Subject: [PATCH 21/35] chore: fix clippy and formatting --- .../v2_1/tests/post_conditions.rs | 36 +++++++------------ .../src/tests/nakamoto_integrations.rs | 2 +- 2 files changed, 13 insertions(+), 25 deletions(-) diff --git a/clarity/src/vm/analysis/type_checker/v2_1/tests/post_conditions.rs b/clarity/src/vm/analysis/type_checker/v2_1/tests/post_conditions.rs index 21885f1604..1ba8c086cd 100644 --- a/clarity/src/vm/analysis/type_checker/v2_1/tests/post_conditions.rs +++ b/clarity/src/vm/analysis/type_checker/v2_1/tests/post_conditions.rs @@ -165,18 +165,12 @@ fn test_restrict_assets(#[case] version: ClarityVersion, #[case] _epoch: StacksE ) )", CheckErrors::ReturnTypesMustMatch( - TypeSignature::new_response( - TypeSignature::NoType.into(), - TypeSignature::UIntType.into(), - ) - .unwrap() - .into(), - TypeSignature::new_response( - TypeSignature::NoType.into(), - TypeSignature::IntType.into(), - ) - .unwrap() - .into(), + TypeSignature::new_response(TypeSignature::NoType, TypeSignature::UIntType) + .unwrap() + .into(), + TypeSignature::new_response(TypeSignature::NoType, TypeSignature::IntType) + .unwrap() + .into(), ), ), ]; @@ -336,18 +330,12 @@ fn test_as_contract(#[case] version: ClarityVersion, #[case] _epoch: StacksEpoch ) )", CheckErrors::ReturnTypesMustMatch( - TypeSignature::new_response( - TypeSignature::NoType.into(), - TypeSignature::UIntType.into(), - ) - .unwrap() - .into(), - TypeSignature::new_response( - TypeSignature::NoType.into(), - TypeSignature::IntType.into(), - ) - .unwrap() - .into(), + TypeSignature::new_response(TypeSignature::NoType, TypeSignature::UIntType) + .unwrap() + .into(), + TypeSignature::new_response(TypeSignature::NoType, TypeSignature::IntType) + .unwrap() + .into(), ), ), ]; diff --git a/stacks-node/src/tests/nakamoto_integrations.rs b/stacks-node/src/tests/nakamoto_integrations.rs index d51fc869f6..6f98a85c59 100644 --- a/stacks-node/src/tests/nakamoto_integrations.rs +++ b/stacks-node/src/tests/nakamoto_integrations.rs @@ -15725,4 +15725,4 @@ fn check_with_stacking_allowances() { // TODO: // - Test stack-stx // - Test roll backs -// - Test successful asset movement \ No newline at end of file +// - Test successful asset movement From 489793b77f9d6d6c244b41139d5152b6b48278d6 Mon Sep 17 00:00:00 2001 From: Brice Dobry Date: Sat, 27 Sep 2025 09:57:37 -0400 Subject: [PATCH 22/35] test: begin adding rollback integration tests --- .../src/tests/nakamoto_integrations.rs | 410 ++++++++++++++++++ 1 file changed, 410 insertions(+) diff --git a/stacks-node/src/tests/nakamoto_integrations.rs b/stacks-node/src/tests/nakamoto_integrations.rs index 6f98a85c59..584abbe708 100644 --- a/stacks-node/src/tests/nakamoto_integrations.rs +++ b/stacks-node/src/tests/nakamoto_integrations.rs @@ -15722,6 +15722,416 @@ fn check_with_stacking_allowances() { run_loop_thread.join().unwrap(); } +#[test] +#[ignore] +/// Verify the error handling and rollback works as expected in +/// `restrict-assets?` expressions +fn check_restrict_assets_rollback() { + if env::var("BITCOIND_TEST") != Ok("1".into()) { + return; + } + + let mut signers = TestSigners::default(); + let (mut naka_conf, _miner_account) = naka_neon_integration_conf(None); + let http_origin = format!("http://{}", &naka_conf.node.rpc_bind); + naka_conf.burnchain.chain_id = CHAIN_ID_TESTNET + 1; + let sender_sk = Secp256k1PrivateKey::random(); + let recipient_sk = Secp256k1PrivateKey::random(); + let recipient = tests::to_addr(&recipient_sk); + let sender_signer_sk = Secp256k1PrivateKey::random(); + let sender_signer_addr = tests::to_addr(&sender_signer_sk); + + // setup sender + recipient for some test stx transfers + // these are necessary for the interim blocks to get mined at all + let sender_addr = tests::to_addr(&sender_sk); + let deploy_fee = 3000; + let call_fee = 400; + let max_transfer_amt = 1000; + naka_conf.add_initial_balance( + PrincipalData::from(sender_addr.clone()).to_string(), + deploy_fee + (max_transfer_amt + call_fee) * 30, + ); + naka_conf.add_initial_balance( + PrincipalData::from(sender_signer_addr.clone()).to_string(), + 100000, + ); + + // Add epoch 3.3 to the configuration because it is not yet added to the + // default epoch list for integration tests. + naka_conf.burnchain.epochs = Some(EpochList::new(&*NAKAMOTO_INTEGRATION_3_3_EPOCHS)); + + let stacker_sk = setup_stacker(&mut naka_conf); + + test_observer::spawn(); + test_observer::register_any(&mut naka_conf); + + let mut btcd_controller = BitcoinCoreController::from_stx_config(&naka_conf); + btcd_controller + .start_bitcoind() + .expect("Failed starting bitcoind"); + let mut btc_regtest_controller = BitcoinRegtestController::new(naka_conf.clone(), None); + btc_regtest_controller.bootstrap_chain(201); + + let mut run_loop = boot_nakamoto::BootRunLoop::new(naka_conf.clone()).unwrap(); + let run_loop_stopper = run_loop.get_termination_switch(); + let Counters { + blocks_processed, .. + } = run_loop.counters(); + let counters = run_loop.counters(); + + let coord_channel = run_loop.coordinator_channels(); + + let run_loop_thread = thread::Builder::new() + .name("run_loop".into()) + .spawn(move || run_loop.start(None, 0)) + .unwrap(); + wait_for_runloop(&blocks_processed); + + boot_to_epoch_3( + &naka_conf, + &blocks_processed, + &[stacker_sk.clone()], + &[sender_signer_sk], + &mut Some(&mut signers), + &mut btc_regtest_controller, + ); + + info!("Bootstrapped to Epoch-3.0 boundary, starting nakamoto miner"); + + info!("Nakamoto miner started..."); + blind_signer(&naka_conf, &signers, &counters); + wait_for_first_naka_block_commit(60, &counters.naka_submitted_commits); + + // mine until epoch 3.3 height + loop { + next_block_and_process_new_stacks_block(&mut btc_regtest_controller, 60, &coord_channel) + .unwrap(); + + // once we actually get a block in epoch 3.3, exit + let blocks = test_observer::get_blocks(); + let last_block = blocks.last().unwrap(); + if last_block + .get("burn_block_height") + .unwrap() + .as_u64() + .unwrap() + >= naka_conf.burnchain.epochs.as_ref().unwrap()[StacksEpochId::Epoch33].start_height + { + break; + } + } + + info!( + "Nakamoto miner has advanced to bitcoin height {}", + get_chain_info_opt(&naka_conf).unwrap().burn_block_height + ); + + let info = get_chain_info_result(&naka_conf).unwrap(); + let last_stacks_block_height = info.stacks_tip_height as u128; + + next_block_and_mine_commit(&mut btc_regtest_controller, 60, &naka_conf, &counters).unwrap(); + + let mut sender_nonce = 0; + let contract_name = "test-contract"; + let contract = format!( + r#" +(define-public (single-transfer (recipient principal) (amount uint) (allowed uint)) + (restrict-assets? tx-sender ((with-stx allowed)) + (unwrap! (stx-transfer? amount tx-sender recipient) (err u200)) + ) +) +(define-public (two-transfers (recipient principal) (amount-1 uint) (amount-2 uint) (allowed uint)) + (restrict-assets? tx-sender ((with-stx allowed)) + (try! (match (stx-transfer? amount-1 tx-sender recipient) + v (ok v) e (err (+ u200 e)))) + (try! (match (stx-transfer? amount-2 tx-sender recipient) + v (ok v) e (err (+ u300 e)))) + ) +) +(define-public (transfer-then-err (recipient principal) (amount uint) (allowed uint)) + (restrict-assets? tx-sender ((with-stx allowed)) + (try! (match (stx-transfer? amount tx-sender recipient) + v (ok v) e (err (+ u200 e)))) + (try! (if false (ok true) (err u200))) + ) +) +(define-public (err-then-transfer (recipient principal) (amount uint) (allowed uint)) + (restrict-assets? tx-sender ((with-stx allowed)) + (try! (if false (ok true) (err u200))) + (try! (match (stx-transfer? amount tx-sender recipient) + v (ok v) e (err (+ u200 e)))) + ) +) +(define-public (transfer-before (recipient principal) (amount-1 uint) (amount-2 uint) (allowed uint)) + (begin + (try! (match (stx-transfer? amount-1 tx-sender recipient) + v (ok v) e (err (+ u200 e)))) + (restrict-assets? tx-sender ((with-stx allowed)) + (try! (match (stx-transfer? amount-2 tx-sender recipient) + v (ok v) e (err (+ u300 e)))) + ) + ) +) +(define-public (transfer-after (recipient principal) (amount-1 uint) (amount-2 uint) (allowed uint)) + (begin + (unwrap! (restrict-assets? tx-sender ((with-stx allowed)) + (try! (match (stx-transfer? amount-1 tx-sender recipient) + v (ok v) e (err (+ u200 e)))) + ) (err u300)) + (match (stx-transfer? amount-2 tx-sender recipient) + v (ok v) e (err (+ u200 e))) + ) +) +"# + ); + + let contract_tx = make_contract_publish_versioned( + &sender_sk, + sender_nonce, + deploy_fee, + naka_conf.burnchain.chain_id, + contract_name, + &contract, + Some(ClarityVersion::Clarity4), + ); + sender_nonce += 1; + let deploy_txid = submit_tx(&http_origin, &contract_tx); + info!("Submitted deploy txid: {deploy_txid}"); + + let mut stacks_block_height = 0; + wait_for(60, || { + let cur_sender_nonce = get_account(&http_origin, &to_addr(&sender_sk)).nonce; + let info = get_chain_info_result(&naka_conf).unwrap(); + stacks_block_height = info.stacks_tip_height as u128; + Ok(stacks_block_height > last_stacks_block_height && cur_sender_nonce == sender_nonce) + }) + .expect("Timed out waiting for contracts to publish"); + + next_block_and_process_new_stacks_block(&mut btc_regtest_controller, 30, &coord_channel) + .unwrap(); + + let mut sender_balance = get_account(&http_origin, &sender_addr).balance; + let mut recipient_balance = get_account(&http_origin, &recipient).balance; + + // helper to submit a call, wait for it to be mined/processed, and return the parsed result + fn submit_call_and_get_result( + http_origin: &str, + sender_sk: &Secp256k1PrivateKey, + sender_nonce: &mut u64, + call_fee: u64, + chain_id: u32, + sender_addr: &StacksAddress, + contract_name: &str, + function_name: &str, + function_args: &[Value], + recipient: &StacksAddress, + ) -> (Value, u128, u128) { + let sender_balance = get_account(http_origin, sender_addr).balance; + let recipient_balance = get_account(http_origin, recipient).balance; + info!("sender balance: {sender_balance}"); + info!("recipient balance: {recipient_balance}"); + + test_observer::clear(); + + let call_tx = make_contract_call( + sender_sk, + *sender_nonce, + call_fee, + chain_id, + sender_addr, + contract_name, + function_name, + function_args, + ); + *sender_nonce += 1; + let call_txid = submit_tx(http_origin, &call_tx); + info!("Submitted call txid: {call_txid}"); + + wait_for(60, || { + let cur_sender_nonce = get_account(http_origin, sender_addr).nonce; + Ok(cur_sender_nonce == *sender_nonce) + }) + .expect("Timed out waiting for contract calls"); + + let mut found = false; + let blocks = test_observer::get_blocks(); + let mut parsed: Option = None; + for block in blocks.iter() { + for tx in block.get("transactions").unwrap().as_array().unwrap() { + let txid = tx + .get("txid") + .unwrap() + .as_str() + .unwrap() + .strip_prefix("0x") + .unwrap(); + if txid == call_txid { + let raw_result = tx.get("raw_result").unwrap().as_str().unwrap(); + parsed = Some(Value::try_deserialize_hex_untyped(&raw_result[2..]).unwrap()); + found = true; + break; + } + } + if found { + break; + } + } + assert!(found, "Should have found expected tx"); + + let parsed = parsed.expect("parsed value"); + let sender_balance = get_account(http_origin, sender_addr).balance; + let recipient_balance = get_account(http_origin, recipient).balance; + (parsed, sender_balance, recipient_balance) + } + + info!("Test: Successful transfer"); + let amount = 1000; + + let (parsed, new_sender_balance, new_recipient_balance) = submit_call_and_get_result( + &http_origin, + &sender_sk, + &mut sender_nonce, + call_fee, + naka_conf.burnchain.chain_id, + &sender_addr, + contract_name, + "single-transfer", + &[ + Value::Principal(recipient.clone().into()), + Value::UInt(amount), + Value::UInt(max_transfer_amt.into()), + ], + &recipient, + ); + let expected = Value::okay_true(); + assert_eq!(expected, parsed); + let sender_expected = sender_balance - amount - call_fee as u128; + let recipient_expected = recipient_balance + amount; + assert_eq!( + sender_expected, new_sender_balance, + "incorrect sender balance" + ); + assert_eq!( + recipient_expected, new_recipient_balance, + "incorrect recipient balance" + ); + sender_balance = new_sender_balance; + recipient_balance = new_recipient_balance; + + info!("Test: Transfer that exceeds allowance"); + + let (parsed, new_sender_balance, new_recipient_balance) = submit_call_and_get_result( + &http_origin, + &sender_sk, + &mut sender_nonce, + call_fee, + naka_conf.burnchain.chain_id, + &sender_addr, + contract_name, + "single-transfer", + &[ + Value::Principal(recipient.clone().into()), + Value::UInt(1000), + Value::UInt(500), + ], + &recipient, + ); + let expected = Value::err_uint(0); + assert_eq!(expected, parsed); + let sender_expected = sender_balance - call_fee as u128; + let recipient_expected = recipient_balance; + assert_eq!( + sender_expected, new_sender_balance, + "incorrect sender balance" + ); + assert_eq!( + recipient_expected, new_recipient_balance, + "incorrect recipient balance" + ); + sender_balance = new_sender_balance; + recipient_balance = new_recipient_balance; + + info!("Test: 2 transfers within allowance"); + let amount1 = 200; + let amount2 = 600; + + let (parsed, new_sender_balance, new_recipient_balance) = submit_call_and_get_result( + &http_origin, + &sender_sk, + &mut sender_nonce, + call_fee, + naka_conf.burnchain.chain_id, + &sender_addr, + contract_name, + "two-transfers", + &[ + Value::Principal(recipient.clone().into()), + Value::UInt(amount1), + Value::UInt(amount2), + Value::UInt(max_transfer_amt.into()), + ], + &recipient, + ); + let expected = Value::okay_true(); + assert_eq!(expected, parsed); + let sender_expected = sender_balance - call_fee as u128 - amount1 - amount2; + let recipient_expected = recipient_balance + amount1 + amount2; + assert_eq!( + sender_expected, new_sender_balance, + "incorrect sender balance" + ); + assert_eq!( + recipient_expected, new_recipient_balance, + "incorrect recipient balance" + ); + sender_balance = new_sender_balance; + recipient_balance = new_recipient_balance; + + info!("Test: 2 transfers that exceed allowance"); + let amount1 = 500; + let amount2 = 600; + + let (parsed, new_sender_balance, new_recipient_balance) = submit_call_and_get_result( + &http_origin, + &sender_sk, + &mut sender_nonce, + call_fee, + naka_conf.burnchain.chain_id, + &sender_addr, + contract_name, + "two-transfers", + &[ + Value::Principal(recipient.clone().into()), + Value::UInt(amount1), + Value::UInt(amount2), + Value::UInt(max_transfer_amt.into()), + ], + &recipient, + ); + let expected = Value::err_uint(0); + assert_eq!(expected, parsed); + let sender_expected = sender_balance - call_fee as u128; + let recipient_expected = recipient_balance; + assert_eq!( + sender_expected, new_sender_balance, + "incorrect sender balance" + ); + assert_eq!( + recipient_expected, new_recipient_balance, + "incorrect recipient balance" + ); + sender_balance = new_sender_balance; + recipient_balance = new_recipient_balance; + + coord_channel + .lock() + .expect("Mutex poisoned") + .stop_chains_coordinator(); + run_loop_stopper.store(false, Ordering::SeqCst); + + run_loop_thread.join().unwrap(); +} + // TODO: // - Test stack-stx // - Test roll backs From ce562a36e03da55eb28e1b161a13b8713210bf18 Mon Sep 17 00:00:00 2001 From: Brice Dobry Date: Sat, 27 Sep 2025 12:52:48 -0400 Subject: [PATCH 23/35] test: additional rollback tests --- .../src/tests/nakamoto_integrations.rs | 357 ++++++++++++++++-- 1 file changed, 331 insertions(+), 26 deletions(-) diff --git a/stacks-node/src/tests/nakamoto_integrations.rs b/stacks-node/src/tests/nakamoto_integrations.rs index 584abbe708..6d6c21b3e1 100644 --- a/stacks-node/src/tests/nakamoto_integrations.rs +++ b/stacks-node/src/tests/nakamoto_integrations.rs @@ -15744,7 +15744,7 @@ fn check_restrict_assets_rollback() { // setup sender + recipient for some test stx transfers // these are necessary for the interim blocks to get mined at all let sender_addr = tests::to_addr(&sender_sk); - let deploy_fee = 3000; + let deploy_fee = 4000; let call_fee = 400; let max_transfer_amt = 1000; naka_conf.add_initial_balance( @@ -15835,51 +15835,145 @@ fn check_restrict_assets_rollback() { let contract_name = "test-contract"; let contract = format!( r#" -(define-public (single-transfer (recipient principal) (amount uint) (allowed uint)) +(define-public (single-transfer + (recipient principal) + (amount uint) + (allowed uint) + ) (restrict-assets? tx-sender ((with-stx allowed)) (unwrap! (stx-transfer? amount tx-sender recipient) (err u200)) ) ) -(define-public (two-transfers (recipient principal) (amount-1 uint) (amount-2 uint) (allowed uint)) +(define-public (two-transfers + (recipient principal) + (amount-1 uint) + (amount-2 uint) + (allowed uint) + ) (restrict-assets? tx-sender ((with-stx allowed)) (try! (match (stx-transfer? amount-1 tx-sender recipient) - v (ok v) e (err (+ u200 e)))) + v (ok v) + e (err (+ u200 e)) + )) (try! (match (stx-transfer? amount-2 tx-sender recipient) - v (ok v) e (err (+ u300 e)))) + v (ok v) + e (err (+ u300 e)) + )) ) ) -(define-public (transfer-then-err (recipient principal) (amount uint) (allowed uint)) +(define-public (transfer-then-err + (recipient principal) + (amount uint) + (allowed uint) + ) (restrict-assets? tx-sender ((with-stx allowed)) (try! (match (stx-transfer? amount tx-sender recipient) - v (ok v) e (err (+ u200 e)))) - (try! (if false (ok true) (err u200))) + v (ok v) + e (err (+ u200 e)) + )) + (try! (if false + (ok true) + (err u300) + )) ) ) -(define-public (err-then-transfer (recipient principal) (amount uint) (allowed uint)) +(define-public (err-then-transfer + (recipient principal) + (amount uint) + (allowed uint) + ) (restrict-assets? tx-sender ((with-stx allowed)) - (try! (if false (ok true) (err u200))) + (try! (if false + (ok true) + (err u200) + )) (try! (match (stx-transfer? amount tx-sender recipient) - v (ok v) e (err (+ u200 e)))) + v (ok v) + e (err (+ u300 e)) + )) ) ) -(define-public (transfer-before (recipient principal) (amount-1 uint) (amount-2 uint) (allowed uint)) +(define-public (transfer-before + (recipient principal) + (amount-1 uint) + (amount-2 uint) + (allowed uint) + ) (begin (try! (match (stx-transfer? amount-1 tx-sender recipient) - v (ok v) e (err (+ u200 e)))) + v (ok v) + e (err (+ u200 e)) + )) (restrict-assets? tx-sender ((with-stx allowed)) (try! (match (stx-transfer? amount-2 tx-sender recipient) - v (ok v) e (err (+ u300 e)))) + v (ok v) + e (err (+ u300 e)) + )) + ) + ) +) +(define-public (transfer-before-catch-err + (recipient principal) + (amount-1 uint) + (amount-2 uint) + (allowed uint) + ) + (begin + (try! (match (stx-transfer? amount-1 tx-sender recipient) + v (ok v) + e (err (+ u200 e)) + )) + (unwrap-err! (restrict-assets? tx-sender ((with-stx allowed)) + (try! (match (stx-transfer? amount-2 tx-sender recipient) + v (ok v) + e (err (+ u300 e)) + )) + ) + (err u400) + ) + (ok true) + ) +) +(define-public (transfer-after + (recipient principal) + (amount-1 uint) + (amount-2 uint) + (allowed uint) + ) + (begin + (unwrap! + (restrict-assets? tx-sender ((with-stx allowed)) + (try! (match (stx-transfer? amount-1 tx-sender recipient) + v (ok v) + e (err (+ u200 e)) + )) + ) + (err u300) + ) + (match (stx-transfer? amount-2 tx-sender recipient) + v (ok v) + e (err (+ u200 e)) ) ) ) -(define-public (transfer-after (recipient principal) (amount-1 uint) (amount-2 uint) (allowed uint)) +(define-public (transfer-after-catch-err + (recipient principal) + (amount-1 uint) + (amount-2 uint) + (allowed uint) + ) (begin - (unwrap! (restrict-assets? tx-sender ((with-stx allowed)) + (unwrap-err! (restrict-assets? tx-sender ((with-stx allowed)) (try! (match (stx-transfer? amount-1 tx-sender recipient) - v (ok v) e (err (+ u200 e)))) - ) (err u300)) + v (ok v) + e (err (+ u300 e)) + ))) + (err u400) + ) (match (stx-transfer? amount-2 tx-sender recipient) - v (ok v) e (err (+ u200 e))) + v (ok v) + e (err (+ u200 e)) + ) ) ) "# @@ -16015,10 +16109,11 @@ fn check_restrict_assets_rollback() { recipient_expected, new_recipient_balance, "incorrect recipient balance" ); - sender_balance = new_sender_balance; - recipient_balance = new_recipient_balance; info!("Test: Transfer that exceeds allowance"); + sender_balance = new_sender_balance; + recipient_balance = new_recipient_balance; + let amount = 1000; let (parsed, new_sender_balance, new_recipient_balance) = submit_call_and_get_result( &http_origin, @@ -16031,7 +16126,7 @@ fn check_restrict_assets_rollback() { "single-transfer", &[ Value::Principal(recipient.clone().into()), - Value::UInt(1000), + Value::UInt(amount), Value::UInt(500), ], &recipient, @@ -16048,10 +16143,10 @@ fn check_restrict_assets_rollback() { recipient_expected, new_recipient_balance, "incorrect recipient balance" ); - sender_balance = new_sender_balance; - recipient_balance = new_recipient_balance; info!("Test: 2 transfers within allowance"); + sender_balance = new_sender_balance; + recipient_balance = new_recipient_balance; let amount1 = 200; let amount2 = 600; @@ -16084,10 +16179,10 @@ fn check_restrict_assets_rollback() { recipient_expected, new_recipient_balance, "incorrect recipient balance" ); - sender_balance = new_sender_balance; - recipient_balance = new_recipient_balance; info!("Test: 2 transfers that exceed allowance"); + sender_balance = new_sender_balance; + recipient_balance = new_recipient_balance; let amount1 = 500; let amount2 = 600; @@ -16120,8 +16215,218 @@ fn check_restrict_assets_rollback() { recipient_expected, new_recipient_balance, "incorrect recipient balance" ); + + info!("Test: transfer then trigger an error in restrict-assets?"); + sender_balance = new_sender_balance; + recipient_balance = new_recipient_balance; + let amount = 500; + + let (parsed, new_sender_balance, new_recipient_balance) = submit_call_and_get_result( + &http_origin, + &sender_sk, + &mut sender_nonce, + call_fee, + naka_conf.burnchain.chain_id, + &sender_addr, + contract_name, + "transfer-then-err", + &[ + Value::Principal(recipient.clone().into()), + Value::UInt(amount), + Value::UInt(max_transfer_amt.into()), + ], + &recipient, + ); + let expected = Value::err_uint(300); + assert_eq!(expected, parsed); + let sender_expected = sender_balance - call_fee as u128; + let recipient_expected = recipient_balance; + assert_eq!( + sender_expected, new_sender_balance, + "incorrect sender balance" + ); + assert_eq!( + recipient_expected, new_recipient_balance, + "incorrect recipient balance" + ); + + info!("Test: error then transfer in restrict-assets?"); + sender_balance = new_sender_balance; + recipient_balance = new_recipient_balance; + let amount = 500; + + let (parsed, new_sender_balance, new_recipient_balance) = submit_call_and_get_result( + &http_origin, + &sender_sk, + &mut sender_nonce, + call_fee, + naka_conf.burnchain.chain_id, + &sender_addr, + contract_name, + "err-then-transfer", + &[ + Value::Principal(recipient.clone().into()), + Value::UInt(amount), + Value::UInt(max_transfer_amt.into()), + ], + &recipient, + ); + let expected = Value::err_uint(200); + assert_eq!(expected, parsed); + let sender_expected = sender_balance - call_fee as u128; + let recipient_expected = recipient_balance; + assert_eq!( + sender_expected, new_sender_balance, + "incorrect sender balance" + ); + assert_eq!( + recipient_expected, new_recipient_balance, + "incorrect recipient balance" + ); + + info!("Test: transfer before successful restrict-assets?"); + sender_balance = new_sender_balance; + recipient_balance = new_recipient_balance; + let amount1 = 700; + let amount2 = 500; + + let (parsed, new_sender_balance, new_recipient_balance) = submit_call_and_get_result( + &http_origin, + &sender_sk, + &mut sender_nonce, + call_fee, + naka_conf.burnchain.chain_id, + &sender_addr, + contract_name, + "transfer-before", + &[ + Value::Principal(recipient.clone().into()), + Value::UInt(amount1), + Value::UInt(amount2), + Value::UInt(max_transfer_amt.into()), + ], + &recipient, + ); + let expected = Value::okay_true(); + assert_eq!(expected, parsed); + let sender_expected = sender_balance - call_fee as u128 - amount1 - amount2; + let recipient_expected = recipient_balance + amount1 + amount2; + assert_eq!( + sender_expected, new_sender_balance, + "incorrect sender balance" + ); + assert_eq!( + recipient_expected, new_recipient_balance, + "incorrect recipient balance" + ); + + info!("Test: transfer before restrict-assets? violation"); + sender_balance = new_sender_balance; + recipient_balance = new_recipient_balance; + let amount1 = 700; + let amount2 = 1200; + + let (parsed, new_sender_balance, new_recipient_balance) = submit_call_and_get_result( + &http_origin, + &sender_sk, + &mut sender_nonce, + call_fee, + naka_conf.burnchain.chain_id, + &sender_addr, + contract_name, + "transfer-before-catch-err", + &[ + Value::Principal(recipient.clone().into()), + Value::UInt(amount1), + Value::UInt(amount2), + Value::UInt(max_transfer_amt.into()), + ], + &recipient, + ); + let expected = Value::okay_true(); + assert_eq!(expected, parsed); + let sender_expected = sender_balance - call_fee as u128 - amount1; + let recipient_expected = recipient_balance + amount1; + assert_eq!( + sender_expected, new_sender_balance, + "incorrect sender balance" + ); + assert_eq!( + recipient_expected, new_recipient_balance, + "incorrect recipient balance" + ); + + info!("Test: transfer after successful restrict-assets?"); + sender_balance = new_sender_balance; + recipient_balance = new_recipient_balance; + let amount1 = 700; + let amount2 = 500; + + let (parsed, new_sender_balance, new_recipient_balance) = submit_call_and_get_result( + &http_origin, + &sender_sk, + &mut sender_nonce, + call_fee, + naka_conf.burnchain.chain_id, + &sender_addr, + contract_name, + "transfer-after", + &[ + Value::Principal(recipient.clone().into()), + Value::UInt(amount1), + Value::UInt(amount2), + Value::UInt(max_transfer_amt.into()), + ], + &recipient, + ); + let expected = Value::okay_true(); + assert_eq!(expected, parsed); + let sender_expected = sender_balance - call_fee as u128 - amount1 - amount2; + let recipient_expected = recipient_balance + amount1 + amount2; + assert_eq!( + sender_expected, new_sender_balance, + "incorrect sender balance" + ); + assert_eq!( + recipient_expected, new_recipient_balance, + "incorrect recipient balance" + ); + + info!("Test: transfer after restrict-assets? violation"); sender_balance = new_sender_balance; recipient_balance = new_recipient_balance; + let amount1 = 1200; + let amount2 = 700; + + let (parsed, new_sender_balance, new_recipient_balance) = submit_call_and_get_result( + &http_origin, + &sender_sk, + &mut sender_nonce, + call_fee, + naka_conf.burnchain.chain_id, + &sender_addr, + contract_name, + "transfer-after-catch-err", + &[ + Value::Principal(recipient.clone().into()), + Value::UInt(amount1), + Value::UInt(amount2), + Value::UInt(max_transfer_amt.into()), + ], + &recipient, + ); + let expected = Value::okay_true(); + assert_eq!(expected, parsed); + let sender_expected = sender_balance - call_fee as u128 - amount2; + let recipient_expected = recipient_balance + amount2; + assert_eq!( + sender_expected, new_sender_balance, + "incorrect sender balance" + ); + assert_eq!( + recipient_expected, new_recipient_balance, + "incorrect recipient balance" + ); coord_channel .lock() From 7806db3152e2e7a0abc6c0a88d498f6692ce6e3b Mon Sep 17 00:00:00 2001 From: Brice Dobry Date: Tue, 30 Sep 2025 09:50:47 -0400 Subject: [PATCH 24/35] docs: update doc example --- clarity/src/vm/docs/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/clarity/src/vm/docs/mod.rs b/clarity/src/vm/docs/mod.rs index 8093b0fdc9..13f7de54f8 100644 --- a/clarity/src/vm/docs/mod.rs +++ b/clarity/src/vm/docs/mod.rs @@ -2642,7 +2642,7 @@ expression. `with-stx` is not allowed outside of `restrict-assets?` or (restrict-assets? tx-sender ((with-stx u50)) (try! (stx-transfer? u100 tx-sender 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM)) -) ;; Returns (err 0) +) ;; Returns (err u0) "#, }; From 610c42174750858f31c0f30e7f58b18bad73e032 Mon Sep 17 00:00:00 2001 From: Brice Dobry Date: Tue, 30 Sep 2025 10:13:14 -0400 Subject: [PATCH 25/35] refactor: use non-panicking array access --- .../v2_1/natives/post_conditions.rs | 75 +++++++++++++++---- 1 file changed, 62 insertions(+), 13 deletions(-) diff --git a/clarity/src/vm/analysis/type_checker/v2_1/natives/post_conditions.rs b/clarity/src/vm/analysis/type_checker/v2_1/natives/post_conditions.rs index 8f0a0cbbac..0c60496a51 100644 --- a/clarity/src/vm/analysis/type_checker/v2_1/natives/post_conditions.rs +++ b/clarity/src/vm/analysis/type_checker/v2_1/natives/post_conditions.rs @@ -39,14 +39,20 @@ pub fn check_restrict_assets( ) -> Result { check_arguments_at_least(3, args)?; - let asset_owner = &args[0]; - let allowance_list = args[1] + let asset_owner = args + .first() + .ok_or(CheckErrors::CheckerImplementationFailure)?; + let allowance_list = args + .get(1) + .ok_or(CheckErrors::CheckerImplementationFailure)? .match_list() .ok_or(CheckErrors::ExpectedListOfAllowances( "restrict-assets?".into(), 2, ))?; - let body_exprs = &args[2..]; + let body_exprs = args + .get(2..) + .ok_or(CheckErrors::CheckerImplementationFailure)?; if allowance_list.len() > MAX_ALLOWANCES { return Err(CheckErrors::TooManyAllowances(MAX_ALLOWANCES, allowance_list.len()).into()); @@ -90,13 +96,17 @@ pub fn check_as_contract( ) -> Result { check_arguments_at_least(2, args)?; - let allowance_list = args[0] + let allowance_list = args + .first() + .ok_or(CheckErrors::CheckerImplementationFailure)? .match_list() .ok_or(CheckErrors::ExpectedListOfAllowances( "as-contract?".into(), 1, ))?; - let body_exprs = &args[1..]; + let body_exprs = args + .get(1..) + .ok_or(CheckErrors::CheckerImplementationFailure)?; if allowance_list.len() > MAX_ALLOWANCES { return Err(CheckErrors::TooManyAllowances(MAX_ALLOWANCES, allowance_list.len()).into()); @@ -185,7 +195,12 @@ fn check_allowance_with_stx( ) -> Result { check_argument_count(1, args)?; - checker.type_check_expects(&args[0], context, &TypeSignature::UIntType)?; + checker.type_check_expects( + args.first() + .ok_or(CheckErrors::CheckerImplementationFailure)?, + context, + &TypeSignature::UIntType, + )?; Ok(false) } @@ -199,9 +214,24 @@ fn check_allowance_with_ft( ) -> Result { check_argument_count(3, args)?; - checker.type_check_expects(&args[0], context, &TypeSignature::PrincipalType)?; - checker.type_check_expects(&args[1], context, &ASCII_128)?; - checker.type_check_expects(&args[2], context, &TypeSignature::UIntType)?; + checker.type_check_expects( + args.first() + .ok_or(CheckErrors::CheckerImplementationFailure)?, + context, + &TypeSignature::PrincipalType, + )?; + checker.type_check_expects( + args.get(1) + .ok_or(CheckErrors::CheckerImplementationFailure)?, + context, + &ASCII_128, + )?; + checker.type_check_expects( + args.get(2) + .ok_or(CheckErrors::CheckerImplementationFailure)?, + context, + &TypeSignature::UIntType, + )?; Ok(false) } @@ -215,11 +245,25 @@ fn check_allowance_with_nft( ) -> Result { check_argument_count(3, args)?; - checker.type_check_expects(&args[0], context, &TypeSignature::PrincipalType)?; - checker.type_check_expects(&args[1], context, &ASCII_128)?; + checker.type_check_expects( + args.first() + .ok_or(CheckErrors::CheckerImplementationFailure)?, + context, + &TypeSignature::PrincipalType, + )?; + checker.type_check_expects( + args.get(1) + .ok_or(CheckErrors::CheckerImplementationFailure)?, + context, + &ASCII_128, + )?; // Asset identifiers must be a Clarity list with any type of elements - let id_list_ty = checker.type_check(&args[2], context)?; + let id_list_ty = checker.type_check( + args.get(2) + .ok_or(CheckErrors::CheckerImplementationFailure)?, + context, + )?; let TypeSignature::SequenceType(SequenceSubtype::ListType(list_data)) = id_list_ty else { return Err(CheckErrors::WithNftExpectedListOfIdentifiers.into()); }; @@ -243,7 +287,12 @@ fn check_allowance_with_stacking( ) -> Result { check_argument_count(1, args)?; - checker.type_check_expects(&args[0], context, &TypeSignature::UIntType)?; + checker.type_check_expects( + args.first() + .ok_or(CheckErrors::CheckerImplementationFailure)?, + context, + &TypeSignature::UIntType, + )?; Ok(false) } From 43b17c53eab9739ebbe27fd76f8bcdc3d7ed14cd Mon Sep 17 00:00:00 2001 From: Brice Dobry Date: Tue, 30 Sep 2025 15:27:25 -0400 Subject: [PATCH 26/35] test: add `cheeck_as_contract_rollback` integration test --- .../src/tests/nakamoto_integrations.rs | 755 +++++++++++++++++- stackslib/src/config/mod.rs | 2 +- 2 files changed, 752 insertions(+), 5 deletions(-) diff --git a/stacks-node/src/tests/nakamoto_integrations.rs b/stacks-node/src/tests/nakamoto_integrations.rs index 6d6c21b3e1..1a159169fa 100644 --- a/stacks-node/src/tests/nakamoto_integrations.rs +++ b/stacks-node/src/tests/nakamoto_integrations.rs @@ -16437,7 +16437,754 @@ fn check_restrict_assets_rollback() { run_loop_thread.join().unwrap(); } -// TODO: -// - Test stack-stx -// - Test roll backs -// - Test successful asset movement +#[test] +#[ignore] +/// Verify the error handling and rollback works as expected in +/// `as-contract?` expressions +fn check_as_contract_rollback() { + if env::var("BITCOIND_TEST") != Ok("1".into()) { + return; + } + + let mut signers = TestSigners::default(); + let (mut naka_conf, _miner_account) = naka_neon_integration_conf(None); + let http_origin = format!("http://{}", &naka_conf.node.rpc_bind); + naka_conf.burnchain.chain_id = CHAIN_ID_TESTNET + 1; + let sender_sk = Secp256k1PrivateKey::random(); + let recipient_sk = Secp256k1PrivateKey::random(); + let recipient = tests::to_addr(&recipient_sk); + let sender_signer_sk = Secp256k1PrivateKey::random(); + let sender_signer_addr = tests::to_addr(&sender_signer_sk); + + // setup sender + recipient for some test stx transfers + // these are necessary for the interim blocks to get mined at all + let sender_addr = tests::to_addr(&sender_sk); + let contract_name = "test-contract"; + let contract_addr = PrincipalData::Contract(QualifiedContractIdentifier { + issuer: sender_addr.clone().into(), + name: contract_name.into(), + }); + let deploy_fee = 4000; + let call_fee = 400; + let max_transfer_amt = 1000; + naka_conf.add_initial_balance( + PrincipalData::from(sender_addr.clone()).to_string(), + deploy_fee + call_fee * 30, + ); + naka_conf.add_initial_balance(contract_addr.to_string(), max_transfer_amt * 30); + naka_conf.add_initial_balance( + PrincipalData::from(sender_signer_addr.clone()).to_string(), + 100000, + ); + + // Add epoch 3.3 to the configuration because it is not yet added to the + // default epoch list for integration tests. + naka_conf.burnchain.epochs = Some(EpochList::new(&*NAKAMOTO_INTEGRATION_3_3_EPOCHS)); + + let stacker_sk = setup_stacker(&mut naka_conf); + + test_observer::spawn(); + test_observer::register_any(&mut naka_conf); + + let mut btcd_controller = BitcoinCoreController::from_stx_config(&naka_conf); + btcd_controller + .start_bitcoind() + .expect("Failed starting bitcoind"); + let mut btc_regtest_controller = BitcoinRegtestController::new(naka_conf.clone(), None); + btc_regtest_controller.bootstrap_chain(201); + + let mut run_loop = boot_nakamoto::BootRunLoop::new(naka_conf.clone()).unwrap(); + let run_loop_stopper = run_loop.get_termination_switch(); + let Counters { + blocks_processed, .. + } = run_loop.counters(); + let counters = run_loop.counters(); + + let coord_channel = run_loop.coordinator_channels(); + + let run_loop_thread = thread::Builder::new() + .name("run_loop".into()) + .spawn(move || run_loop.start(None, 0)) + .unwrap(); + wait_for_runloop(&blocks_processed); + + boot_to_epoch_3( + &naka_conf, + &blocks_processed, + &[stacker_sk.clone()], + &[sender_signer_sk], + &mut Some(&mut signers), + &mut btc_regtest_controller, + ); + + info!("Bootstrapped to Epoch-3.0 boundary, starting nakamoto miner"); + + info!("Nakamoto miner started..."); + blind_signer(&naka_conf, &signers, &counters); + wait_for_first_naka_block_commit(60, &counters.naka_submitted_commits); + + // mine until epoch 3.3 height + loop { + next_block_and_process_new_stacks_block(&mut btc_regtest_controller, 60, &coord_channel) + .unwrap(); + + // once we actually get a block in epoch 3.3, exit + let blocks = test_observer::get_blocks(); + let last_block = blocks.last().unwrap(); + if last_block + .get("burn_block_height") + .unwrap() + .as_u64() + .unwrap() + >= naka_conf.burnchain.epochs.as_ref().unwrap()[StacksEpochId::Epoch33].start_height + { + break; + } + } + + info!( + "Nakamoto miner has advanced to bitcoin height {}", + get_chain_info_opt(&naka_conf).unwrap().burn_block_height + ); + + let info = get_chain_info_result(&naka_conf).unwrap(); + let last_stacks_block_height = info.stacks_tip_height as u128; + + next_block_and_mine_commit(&mut btc_regtest_controller, 60, &naka_conf, &counters).unwrap(); + + let mut sender_nonce = 0; + let contract = format!( + r#" +(define-public (single-transfer + (recipient principal) + (amount uint) + (allowed uint) + ) + (as-contract? ((with-stx allowed)) + (unwrap! (stx-transfer? amount tx-sender recipient) (err u200)) + ) +) +(define-public (two-transfers + (recipient principal) + (amount-1 uint) + (amount-2 uint) + (allowed uint) + ) + (as-contract? ((with-stx allowed)) + (try! (match (stx-transfer? amount-1 tx-sender recipient) + v (ok v) + e (err (+ u200 e)) + )) + (try! (match (stx-transfer? amount-2 tx-sender recipient) + v (ok v) + e (err (+ u300 e)) + )) + ) +) +(define-public (transfer-then-err + (recipient principal) + (amount uint) + (allowed uint) + ) + (as-contract? ((with-stx allowed)) + (try! (match (stx-transfer? amount tx-sender recipient) + v (ok v) + e (err (+ u200 e)) + )) + (try! (if false + (ok true) + (err u300) + )) + ) +) +(define-public (err-then-transfer + (recipient principal) + (amount uint) + (allowed uint) + ) + (as-contract? ((with-stx allowed)) + (try! (if false + (ok true) + (err u200) + )) + (try! (match (stx-transfer? amount tx-sender recipient) + v (ok v) + e (err (+ u300 e)) + )) + ) +) +(define-public (transfer-before + (recipient principal) + (amount-1 uint) + (amount-2 uint) + (allowed uint) + ) + (begin + (try! (match (stx-transfer? amount-1 tx-sender recipient) + v (ok v) + e (err (+ u200 e)) + )) + (as-contract? ((with-stx allowed)) + (try! (match (stx-transfer? amount-2 tx-sender recipient) + v (ok v) + e (err (+ u300 e)) + )) + ) + ) +) +(define-public (transfer-before-catch-err + (recipient principal) + (amount-1 uint) + (amount-2 uint) + (allowed uint) + ) + (begin + (try! (match (stx-transfer? amount-1 tx-sender recipient) + v (ok v) + e (err (+ u200 e)) + )) + (unwrap-err! (as-contract? ((with-stx allowed)) + (try! (match (stx-transfer? amount-2 tx-sender recipient) + v (ok v) + e (err (+ u300 e)) + )) + ) + (err u400) + ) + (ok true) + ) +) +(define-public (transfer-after + (recipient principal) + (amount-1 uint) + (amount-2 uint) + (allowed uint) + ) + (begin + (unwrap! + (as-contract? ((with-stx allowed)) + (try! (match (stx-transfer? amount-1 tx-sender recipient) + v (ok v) + e (err (+ u200 e)) + )) + ) + (err u300) + ) + (match (stx-transfer? amount-2 tx-sender recipient) + v (ok v) + e (err (+ u200 e)) + ) + ) +) +(define-public (transfer-after-catch-err + (recipient principal) + (amount-1 uint) + (amount-2 uint) + (allowed uint) + ) + (begin + (unwrap-err! (as-contract? ((with-stx allowed)) + (try! (match (stx-transfer? amount-1 tx-sender recipient) + v (ok v) + e (err (+ u300 e)) + ))) + (err u400) + ) + (match (stx-transfer? amount-2 tx-sender recipient) + v (ok v) + e (err (+ u200 e)) + ) + ) +) +"# + ); + + let contract_tx = make_contract_publish_versioned( + &sender_sk, + sender_nonce, + deploy_fee, + naka_conf.burnchain.chain_id, + contract_name, + &contract, + Some(ClarityVersion::Clarity4), + ); + sender_nonce += 1; + let deploy_txid = submit_tx(&http_origin, &contract_tx); + info!("Submitted deploy txid: {deploy_txid}"); + + let mut stacks_block_height = 0; + wait_for(60, || { + let cur_sender_nonce = get_account(&http_origin, &to_addr(&sender_sk)).nonce; + let info = get_chain_info_result(&naka_conf).unwrap(); + stacks_block_height = info.stacks_tip_height as u128; + Ok(stacks_block_height > last_stacks_block_height && cur_sender_nonce == sender_nonce) + }) + .expect("Timed out waiting for contracts to publish"); + + next_block_and_process_new_stacks_block(&mut btc_regtest_controller, 30, &coord_channel) + .unwrap(); + + let mut contract_balance = get_account(&http_origin, &contract_addr).balance; + let mut recipient_balance = get_account(&http_origin, &recipient).balance; + + // helper to submit a call, wait for it to be mined/processed, and return the parsed result + fn submit_call_and_get_result( + http_origin: &str, + sender_sk: &Secp256k1PrivateKey, + sender_nonce: &mut u64, + call_fee: u64, + chain_id: u32, + sender_addr: &StacksAddress, + contract_name: &str, + function_name: &str, + function_args: &[Value], + recipient: &StacksAddress, + ) -> (Value, u128, u128) { + let contract_addr = PrincipalData::Contract(QualifiedContractIdentifier { + issuer: sender_addr.clone().into(), + name: contract_name.into(), + }); + let contract_balance = get_account(http_origin, &contract_addr).balance; + let recipient_balance = get_account(http_origin, recipient).balance; + info!("contract balance: {contract_balance}"); + info!("recipient balance: {recipient_balance}"); + + test_observer::clear(); + + let call_tx = make_contract_call( + sender_sk, + *sender_nonce, + call_fee, + chain_id, + sender_addr, + contract_name, + function_name, + function_args, + ); + *sender_nonce += 1; + let call_txid = submit_tx(http_origin, &call_tx); + info!("Submitted call txid: {call_txid}"); + + wait_for(60, || { + let cur_sender_nonce = get_account(http_origin, sender_addr).nonce; + Ok(cur_sender_nonce == *sender_nonce) + }) + .expect("Timed out waiting for contract calls"); + + let mut found = false; + let blocks = test_observer::get_blocks(); + let mut parsed: Option = None; + for block in blocks.iter() { + for tx in block.get("transactions").unwrap().as_array().unwrap() { + let txid = tx + .get("txid") + .unwrap() + .as_str() + .unwrap() + .strip_prefix("0x") + .unwrap(); + if txid == call_txid { + let raw_result = tx.get("raw_result").unwrap().as_str().unwrap(); + parsed = Some(Value::try_deserialize_hex_untyped(&raw_result[2..]).unwrap()); + found = true; + break; + } + } + if found { + break; + } + } + assert!(found, "Should have found expected tx"); + + let parsed = parsed.expect("parsed value"); + let contract_balance = get_account(http_origin, &contract_addr).balance; + let recipient_balance = get_account(http_origin, recipient).balance; + (parsed, contract_balance, recipient_balance) + } + + info!("Test: Successful transfer"); + let amount = 1000; + + let (parsed, new_contract_balance, new_recipient_balance) = submit_call_and_get_result( + &http_origin, + &sender_sk, + &mut sender_nonce, + call_fee, + naka_conf.burnchain.chain_id, + &sender_addr, + contract_name, + "single-transfer", + &[ + Value::Principal(recipient.clone().into()), + Value::UInt(amount), + Value::UInt(max_transfer_amt.into()), + ], + &recipient, + ); + let expected = Value::okay_true(); + assert_eq!(expected, parsed); + let contract_expected = contract_balance - amount; + let recipient_expected = recipient_balance + amount; + assert_eq!( + contract_expected, new_contract_balance, + "incorrect contract balance" + ); + assert_eq!( + recipient_expected, new_recipient_balance, + "incorrect recipient balance" + ); + + info!("Test: Transfer that exceeds allowance"); + contract_balance = new_contract_balance; + recipient_balance = new_recipient_balance; + let amount = 1000; + + let (parsed, new_contract_balance, new_recipient_balance) = submit_call_and_get_result( + &http_origin, + &sender_sk, + &mut sender_nonce, + call_fee, + naka_conf.burnchain.chain_id, + &sender_addr, + contract_name, + "single-transfer", + &[ + Value::Principal(recipient.clone().into()), + Value::UInt(amount), + Value::UInt(500), + ], + &recipient, + ); + let expected = Value::err_uint(0); + assert_eq!(expected, parsed); + let contract_expected = contract_balance; + let recipient_expected = recipient_balance; + assert_eq!( + contract_expected, new_contract_balance, + "incorrect contract balance" + ); + assert_eq!( + recipient_expected, new_recipient_balance, + "incorrect recipient balance" + ); + + info!("Test: 2 transfers within allowance"); + contract_balance = new_contract_balance; + recipient_balance = new_recipient_balance; + let amount1 = 200; + let amount2 = 600; + + let (parsed, new_contract_balance, new_recipient_balance) = submit_call_and_get_result( + &http_origin, + &sender_sk, + &mut sender_nonce, + call_fee, + naka_conf.burnchain.chain_id, + &sender_addr, + contract_name, + "two-transfers", + &[ + Value::Principal(recipient.clone().into()), + Value::UInt(amount1), + Value::UInt(amount2), + Value::UInt(max_transfer_amt.into()), + ], + &recipient, + ); + let expected = Value::okay_true(); + assert_eq!(expected, parsed); + let contract_expected = contract_balance - amount1 - amount2; + let recipient_expected = recipient_balance + amount1 + amount2; + assert_eq!( + contract_expected, new_contract_balance, + "incorrect contract balance" + ); + assert_eq!( + recipient_expected, new_recipient_balance, + "incorrect recipient balance" + ); + + info!("Test: 2 transfers that exceed allowance"); + contract_balance = new_contract_balance; + recipient_balance = new_recipient_balance; + let amount1 = 500; + let amount2 = 600; + + let (parsed, new_contract_balance, new_recipient_balance) = submit_call_and_get_result( + &http_origin, + &sender_sk, + &mut sender_nonce, + call_fee, + naka_conf.burnchain.chain_id, + &sender_addr, + contract_name, + "two-transfers", + &[ + Value::Principal(recipient.clone().into()), + Value::UInt(amount1), + Value::UInt(amount2), + Value::UInt(max_transfer_amt.into()), + ], + &recipient, + ); + let expected = Value::err_uint(0); + assert_eq!(expected, parsed); + let sender_expected = contract_balance; + let recipient_expected = recipient_balance; + assert_eq!( + sender_expected, new_contract_balance, + "incorrect contract balance" + ); + assert_eq!( + recipient_expected, new_recipient_balance, + "incorrect recipient balance" + ); + + info!("Test: transfer then trigger an error in restrict-assets?"); + contract_balance = new_contract_balance; + recipient_balance = new_recipient_balance; + let amount = 500; + + let (parsed, new_contract_balance, new_recipient_balance) = submit_call_and_get_result( + &http_origin, + &sender_sk, + &mut sender_nonce, + call_fee, + naka_conf.burnchain.chain_id, + &sender_addr, + contract_name, + "transfer-then-err", + &[ + Value::Principal(recipient.clone().into()), + Value::UInt(amount), + Value::UInt(max_transfer_amt.into()), + ], + &recipient, + ); + let expected = Value::err_uint(300); + assert_eq!(expected, parsed); + let contract_expected = contract_balance; + let recipient_expected = recipient_balance; + assert_eq!( + contract_expected, new_contract_balance, + "incorrect contract balance" + ); + assert_eq!( + recipient_expected, new_recipient_balance, + "incorrect recipient balance" + ); + + info!("Test: error then transfer in restrict-assets?"); + contract_balance = new_contract_balance; + recipient_balance = new_recipient_balance; + let amount = 500; + + let (parsed, new_contract_balance, new_recipient_balance) = submit_call_and_get_result( + &http_origin, + &sender_sk, + &mut sender_nonce, + call_fee, + naka_conf.burnchain.chain_id, + &sender_addr, + contract_name, + "err-then-transfer", + &[ + Value::Principal(recipient.clone().into()), + Value::UInt(amount), + Value::UInt(max_transfer_amt.into()), + ], + &recipient, + ); + let expected = Value::err_uint(200); + assert_eq!(expected, parsed); + let contract_expected = contract_balance; + let recipient_expected = recipient_balance; + assert_eq!( + contract_expected, new_contract_balance, + "incorrect contract balance" + ); + assert_eq!( + recipient_expected, new_recipient_balance, + "incorrect recipient balance" + ); + + info!("Test: transfer before successful restrict-assets?"); + let sender_balance = get_account(&http_origin, &sender_addr).balance; + contract_balance = new_contract_balance; + recipient_balance = new_recipient_balance; + let amount1 = 700; + let amount2 = 500; + + let (parsed, new_contract_balance, new_recipient_balance) = submit_call_and_get_result( + &http_origin, + &sender_sk, + &mut sender_nonce, + call_fee, + naka_conf.burnchain.chain_id, + &sender_addr, + contract_name, + "transfer-before", + &[ + Value::Principal(recipient.clone().into()), + Value::UInt(amount1), + Value::UInt(amount2), + Value::UInt(max_transfer_amt.into()), + ], + &recipient, + ); + let expected = Value::okay_true(); + assert_eq!(expected, parsed); + let contract_expected = contract_balance - amount2; + let recipient_expected = recipient_balance + amount1 + amount2; + let sender_expected = sender_balance - call_fee as u128 - amount1; + assert_eq!( + contract_expected, new_contract_balance, + "incorrect contract balance" + ); + assert_eq!( + recipient_expected, new_recipient_balance, + "incorrect recipient balance" + ); + assert_eq!( + sender_expected, + get_account(&http_origin, &sender_addr).balance, + "incorrect sender balance" + ); + + info!("Test: transfer before restrict-assets? violation"); + contract_balance = new_contract_balance; + recipient_balance = new_recipient_balance; + let sender_balance = get_account(&http_origin, &sender_addr).balance; + let amount1 = 700; + let amount2 = 1200; + + let (parsed, new_contract_balance, new_recipient_balance) = submit_call_and_get_result( + &http_origin, + &sender_sk, + &mut sender_nonce, + call_fee, + naka_conf.burnchain.chain_id, + &sender_addr, + contract_name, + "transfer-before-catch-err", + &[ + Value::Principal(recipient.clone().into()), + Value::UInt(amount1), + Value::UInt(amount2), + Value::UInt(max_transfer_amt.into()), + ], + &recipient, + ); + let expected = Value::okay_true(); + assert_eq!(expected, parsed); + let contract_expected = contract_balance; + let recipient_expected = recipient_balance + amount1; + let sender_expected = sender_balance - call_fee as u128 - amount1; + assert_eq!( + contract_expected, new_contract_balance, + "incorrect contract balance" + ); + assert_eq!( + recipient_expected, new_recipient_balance, + "incorrect recipient balance" + ); + assert_eq!( + sender_expected, + get_account(&http_origin, &sender_addr).balance, + "incorrect sender balance" + ); + + info!("Test: transfer after successful restrict-assets?"); + contract_balance = new_contract_balance; + recipient_balance = new_recipient_balance; + let sender_balance = get_account(&http_origin, &sender_addr).balance; + let amount1 = 700; + let amount2 = 500; + + let (parsed, new_contract_balance, new_recipient_balance) = submit_call_and_get_result( + &http_origin, + &sender_sk, + &mut sender_nonce, + call_fee, + naka_conf.burnchain.chain_id, + &sender_addr, + contract_name, + "transfer-after", + &[ + Value::Principal(recipient.clone().into()), + Value::UInt(amount1), + Value::UInt(amount2), + Value::UInt(max_transfer_amt.into()), + ], + &recipient, + ); + let expected = Value::okay_true(); + assert_eq!(expected, parsed); + let contract_expected = contract_balance - amount1; + let recipient_expected = recipient_balance + amount1 + amount2; + let sender_expected = sender_balance - call_fee as u128 - amount2; + assert_eq!( + contract_expected, new_contract_balance, + "incorrect contract balance" + ); + assert_eq!( + recipient_expected, new_recipient_balance, + "incorrect recipient balance" + ); + assert_eq!( + sender_expected, + get_account(&http_origin, &sender_addr).balance, + "incorrect sender balance" + ); + + info!("Test: transfer after restrict-assets? violation"); + contract_balance = new_contract_balance; + recipient_balance = new_recipient_balance; + let sender_balance = get_account(&http_origin, &sender_addr).balance; + let amount1 = 1200; + let amount2 = 700; + + let (parsed, new_contract_balance, new_recipient_balance) = submit_call_and_get_result( + &http_origin, + &sender_sk, + &mut sender_nonce, + call_fee, + naka_conf.burnchain.chain_id, + &sender_addr, + contract_name, + "transfer-after-catch-err", + &[ + Value::Principal(recipient.clone().into()), + Value::UInt(amount1), + Value::UInt(amount2), + Value::UInt(max_transfer_amt.into()), + ], + &recipient, + ); + let expected = Value::okay_true(); + assert_eq!(expected, parsed); + let contract_expected = contract_balance; + let recipient_expected = recipient_balance + amount2; + let sender_expected = sender_balance - call_fee as u128 - amount2; + assert_eq!( + contract_expected, new_contract_balance, + "incorrect contract balance" + ); + assert_eq!( + recipient_expected, new_recipient_balance, + "incorrect recipient balance" + ); + assert_eq!( + sender_expected, + get_account(&http_origin, &sender_addr).balance, + "incorrect sender balance" + ); + + coord_channel + .lock() + .expect("Mutex poisoned") + .stop_chains_coordinator(); + run_loop_stopper.store(false, Ordering::SeqCst); + + run_loop_thread.join().unwrap(); +} diff --git a/stackslib/src/config/mod.rs b/stackslib/src/config/mod.rs index b7d1919752..bfd13535ae 100644 --- a/stackslib/src/config/mod.rs +++ b/stackslib/src/config/mod.rs @@ -1101,7 +1101,7 @@ impl Config { pub fn add_initial_balance(&mut self, address: String, amount: u64) { let new_balance = InitialBalance { - address: PrincipalData::parse_standard_principal(&address) + address: PrincipalData::parse(&address) .unwrap() .into(), amount, From a65dda44f67016a5056023ca938bda1d4f742516 Mon Sep 17 00:00:00 2001 From: Brice Dobry Date: Wed, 1 Oct 2025 15:40:25 -0400 Subject: [PATCH 27/35] test: add integration tests for `stack-stx` allowances --- .../src/tests/nakamoto_integrations.rs | 597 +++++++++++++++++- 1 file changed, 595 insertions(+), 2 deletions(-) diff --git a/stacks-node/src/tests/nakamoto_integrations.rs b/stacks-node/src/tests/nakamoto_integrations.rs index 1a159169fa..e6ae447829 100644 --- a/stacks-node/src/tests/nakamoto_integrations.rs +++ b/stacks-node/src/tests/nakamoto_integrations.rs @@ -15327,8 +15327,8 @@ fn check_block_time_keyword() { #[test] #[ignore] -/// Verify the `with-stacking` allowances work as expected -fn check_with_stacking_allowances() { +/// Verify the `with-stacking` allowances work as expected when delegating STX. +fn check_with_stacking_allowances_delegate_stx() { if env::var("BITCOIND_TEST") != Ok("1".into()) { return; } @@ -15722,6 +15722,599 @@ fn check_with_stacking_allowances() { run_loop_thread.join().unwrap(); } +#[test] +#[ignore] +/// Verify the `with-stacking` allowances work as expected when stacking STX +fn check_with_stacking_allowances_stack_stx() { + if env::var("BITCOIND_TEST") != Ok("1".into()) { + return; + } + + let mut signers = TestSigners::default(); + let (mut naka_conf, _miner_account) = naka_neon_integration_conf(None); + let http_origin = format!("http://{}", &naka_conf.node.rpc_bind); + naka_conf.burnchain.chain_id = CHAIN_ID_TESTNET + 1; + let sender_sk = Secp256k1PrivateKey::random(); + let sender_signer_sk = Secp256k1PrivateKey::random(); + let sender_signer_addr = tests::to_addr(&sender_signer_sk); + + let signer_sk = signers.signer_keys[0].clone(); + let signer_pk = StacksPublicKey::from_private(&signer_sk); + + // setup sender + recipient for some test stx transfers + // these are necessary for the interim blocks to get mined at all + let sender_addr = tests::to_addr(&sender_sk); + let deploy_fee = 3000; + let call_fee = 400; + naka_conf.add_initial_balance( + PrincipalData::from(sender_addr.clone()).to_string(), + deploy_fee + call_fee * 30, + ); + naka_conf.add_initial_balance( + PrincipalData::from(sender_signer_addr.clone()).to_string(), + 100000, + ); + + // Add epoch 3.3 to the configuration because it is not yet added to the + // default epoch list for integration tests. + naka_conf.burnchain.epochs = Some(EpochList::new(&*NAKAMOTO_INTEGRATION_3_3_EPOCHS)); + + // Default stacker used for bootstrapping + let stacker_sk = setup_stacker(&mut naka_conf); + + // Stackers used for testing + let stackers: Vec<_> = (0..3).map(|_| setup_stacker(&mut naka_conf)).collect(); + + test_observer::spawn(); + test_observer::register_any(&mut naka_conf); + + let mut btcd_controller = BitcoinCoreController::from_stx_config(&naka_conf); + btcd_controller + .start_bitcoind() + .expect("Failed starting bitcoind"); + let mut btc_regtest_controller = BitcoinRegtestController::new(naka_conf.clone(), None); + btc_regtest_controller.bootstrap_chain(201); + + let mut run_loop = boot_nakamoto::BootRunLoop::new(naka_conf.clone()).unwrap(); + let run_loop_stopper = run_loop.get_termination_switch(); + let Counters { + blocks_processed, .. + } = run_loop.counters(); + let counters = run_loop.counters(); + + let coord_channel = run_loop.coordinator_channels(); + + let run_loop_thread = thread::Builder::new() + .name("run_loop".into()) + .spawn(move || run_loop.start(None, 0)) + .unwrap(); + wait_for_runloop(&blocks_processed); + + boot_to_epoch_3( + &naka_conf, + &blocks_processed, + &[stacker_sk.clone()], + &[sender_signer_sk], + &mut Some(&mut signers), + &mut btc_regtest_controller, + ); + + info!("Bootstrapped to Epoch-3.0 boundary, starting nakamoto miner"); + + info!("Nakamoto miner started..."); + blind_signer(&naka_conf, &signers, &counters); + wait_for_first_naka_block_commit(60, &counters.naka_submitted_commits); + + // mine until epoch 3.3 height + loop { + next_block_and_process_new_stacks_block(&mut btc_regtest_controller, 60, &coord_channel) + .unwrap(); + + // once we actually get a block in epoch 3.3, exit + let blocks = test_observer::get_blocks(); + let last_block = blocks.last().unwrap(); + if last_block + .get("burn_block_height") + .unwrap() + .as_u64() + .unwrap() + >= naka_conf.burnchain.epochs.as_ref().unwrap()[StacksEpochId::Epoch33].start_height + { + break; + } + } + + info!( + "Nakamoto miner has advanced to bitcoin height {}", + get_chain_info_opt(&naka_conf).unwrap().burn_block_height + ); + + let info = get_chain_info_result(&naka_conf).unwrap(); + let last_stacks_block_height = info.stacks_tip_height as u128; + + next_block_and_mine_commit(&mut btc_regtest_controller, 60, &naka_conf, &counters).unwrap(); + + let signer_key_hex = Value::buff_from(signer_pk.to_bytes_compressed()).unwrap(); + let mut sender_nonce = 0; + let contract_name = "test-contract"; + let contract = format!( + r#" +(define-constant signer-key {signer_key_hex}) +(define-public (stack-stx (amount uint) (pox-addr (tuple (version (buff 1)) (hashbytes (buff 32)))) (signature (optional (buff 65))) (auth-id uint) (allowed uint)) + (restrict-assets? tx-sender ((with-stacking allowed)) + (match + (contract-call? 'ST000000000000000000002AMW42H.pox-4 stack-stx + amount pox-addr burn-block-height u12 signature signer-key amount auth-id + ) + v true + e (try! (if false (ok true) (err (to-uint e)))) + ) + ) +) +(define-public (stack-stx-2-allowances (amount uint) (pox-addr (tuple (version (buff 1)) (hashbytes (buff 32)))) (signature (optional (buff 65))) (auth-id uint) (allowed-1 uint) (allowed-2 uint)) + (restrict-assets? tx-sender ((with-stacking allowed-1) (with-stacking allowed-2)) + (match + (contract-call? 'ST000000000000000000002AMW42H.pox-4 stack-stx + amount pox-addr burn-block-height u12 signature signer-key amount auth-id + ) + v true + e (try! (if false (ok true) (err (to-uint e)))) + ) + ) +) +(define-public (stack-stx-no-allowance (amount uint) (pox-addr (tuple (version (buff 1)) (hashbytes (buff 32)))) (signature (optional (buff 65))) (auth-id uint)) + (restrict-assets? tx-sender () + (match + (contract-call? 'ST000000000000000000002AMW42H.pox-4 stack-stx + amount pox-addr burn-block-height u12 signature signer-key amount auth-id + ) + v true + e (try! (if false (ok true) (err (to-uint e)))) + ) + ) +) +(define-public (stack-stx-all (amount uint) (pox-addr (tuple (version (buff 1)) (hashbytes (buff 32)))) (signature (optional (buff 65))) (auth-id uint)) + (begin + (try! (stx-transfer? amount tx-sender current-contract)) + (as-contract? ((with-all-assets-unsafe)) + (match + (contract-call? 'ST000000000000000000002AMW42H.pox-4 stack-stx + amount pox-addr burn-block-height u12 signature signer-key amount auth-id + ) + v true + e (try! (if false (ok true) (err (to-uint e)))) + ) + ) + ) +) +"# + ); + + let contract_tx = make_contract_publish_versioned( + &sender_sk, + sender_nonce, + deploy_fee, + naka_conf.burnchain.chain_id, + contract_name, + &contract, + Some(ClarityVersion::Clarity4), + ); + sender_nonce += 1; + let deploy_txid = submit_tx(&http_origin, &contract_tx); + info!("Submitted deploy txid: {deploy_txid}"); + + let mut stacks_block_height = 0; + wait_for(60, || { + let cur_sender_nonce = get_account(&http_origin, &to_addr(&sender_sk)).nonce; + let info = get_chain_info_result(&naka_conf).unwrap(); + stacks_block_height = info.stacks_tip_height as u128; + Ok(stacks_block_height > last_stacks_block_height && cur_sender_nonce == sender_nonce) + }) + .expect("Timed out waiting for contracts to publish"); + + next_block_and_process_new_stacks_block(&mut btc_regtest_controller, 30, &coord_channel) + .unwrap(); + + let block_height = btc_regtest_controller.get_headers_height(); + let reward_cycle = btc_regtest_controller + .get_burnchain() + .block_height_to_reward_cycle(block_height) + .unwrap(); + + test_observer::clear(); + + // Amount to stack + let amount = Value::UInt(POX_4_DEFAULT_STACKER_STX_AMT); + + // Map txid to expected result, `true` for ok, `false` for error + let mut expected_results = HashMap::new(); + let mut wait_for_nonce = HashMap::new(); + + // ***** Successfully stack with stackers[0] + let stacker = &stackers[0]; + let stacker_addr = tests::to_addr(stacker); + let mut stacker_nonce = 0; + + // Authorize the contract + let authorize_tx = make_contract_call( + stacker, + stacker_nonce, + call_fee, + naka_conf.burnchain.chain_id, + &boot_code_addr(false), + "pox-4", + "allow-contract-caller", + &[ + QualifiedContractIdentifier::new(sender_addr.clone().into(), contract_name.into()) + .into(), + Value::none(), + ], + ); + stacker_nonce += 1; + let authorize_txid = submit_tx(&http_origin, &authorize_tx); + info!("Submitted authorize txid: {authorize_txid}"); + expected_results.insert(authorize_txid, Value::okay_true()); + + let auth_id = 1; + let pox_addr = PoxAddress::from_legacy( + AddressHashMode::SerializeP2PKH, + stacker_addr.bytes().clone(), + ); + let pox_addr_tuple: clarity::vm::Value = pox_addr.clone().as_clarity_tuple().unwrap().into(); + let signature_bytes = make_pox_4_signer_key_signature( + &pox_addr, + &signer_sk, + reward_cycle.into(), + &Pox4SignatureTopic::StackStx, + naka_conf.burnchain.chain_id, + 12_u128, + POX_4_DEFAULT_STACKER_STX_AMT, + auth_id, + ) + .unwrap() + .to_rsv(); + let signature = Value::some(clarity::vm::Value::buff_from(signature_bytes).unwrap()).unwrap(); + let stack_ok_tx = make_contract_call( + stacker, + stacker_nonce, + call_fee, + naka_conf.burnchain.chain_id, + &sender_addr, + contract_name, + "stack-stx", + &[ + amount.clone(), + pox_addr_tuple, + signature, + Value::UInt(auth_id), + amount.clone(), + ], + ); + stacker_nonce += 1; + let stack_ok_txid = submit_tx(&http_origin, &stack_ok_tx); + info!("Submitted stack_ok txid: {stack_ok_txid}"); + expected_results.insert(stack_ok_txid, Value::okay_true()); + wait_for_nonce.insert(stacker_addr.clone(), stacker_nonce); + + // ***** Fail to stack with stackers[1] + let stacker = &stackers[1]; + let stacker_addr = tests::to_addr(stacker); + let mut stacker_nonce = 0; + + // Authorize the contract + let authorize_tx = make_contract_call( + stacker, + stacker_nonce, + call_fee, + naka_conf.burnchain.chain_id, + &boot_code_addr(false), + "pox-4", + "allow-contract-caller", + &[ + QualifiedContractIdentifier::new(sender_addr.clone().into(), contract_name.into()) + .into(), + Value::none(), + ], + ); + stacker_nonce += 1; + let authorize_txid = submit_tx(&http_origin, &authorize_tx); + info!("Submitted authorize txid: {authorize_txid}"); + expected_results.insert(authorize_txid, Value::okay_true()); + + let auth_id = 1; + let allowed = Value::UInt(POX_4_DEFAULT_STACKER_STX_AMT - 1); + let pox_addr = PoxAddress::from_legacy( + AddressHashMode::SerializeP2PKH, + stacker_addr.bytes().clone(), + ); + let pox_addr_tuple: clarity::vm::Value = pox_addr.clone().as_clarity_tuple().unwrap().into(); + let signature_bytes = make_pox_4_signer_key_signature( + &pox_addr, + &signer_sk, + reward_cycle.into(), + &Pox4SignatureTopic::StackStx, + naka_conf.burnchain.chain_id, + 12_u128, + POX_4_DEFAULT_STACKER_STX_AMT, + auth_id, + ) + .unwrap() + .to_rsv(); + let signature = Value::some(clarity::vm::Value::buff_from(signature_bytes).unwrap()).unwrap(); + let stack_err_tx = make_contract_call( + stacker, + stacker_nonce, + call_fee, + naka_conf.burnchain.chain_id, + &sender_addr, + contract_name, + "stack-stx", + &[ + amount.clone(), + pox_addr_tuple.clone(), + signature.clone(), + Value::UInt(auth_id), + allowed, + ], + ); + stacker_nonce += 1; + let stack_err_txid = submit_tx(&http_origin, &stack_err_tx); + info!("Submitted stack_err txid: {stack_err_txid}"); + expected_results.insert(stack_err_txid, Value::error(Value::UInt(0)).unwrap()); + wait_for_nonce.insert(stacker_addr.clone(), stacker_nonce); + + // ***** Stack successfully with stackers[1] with two allowances + let allowed1 = Value::UInt(POX_4_DEFAULT_STACKER_STX_AMT); + let allowed2 = Value::UInt(POX_4_DEFAULT_STACKER_STX_AMT + 100); + let stack_2_ok_tx = make_contract_call( + stacker, + stacker_nonce, + call_fee, + naka_conf.burnchain.chain_id, + &sender_addr, + contract_name, + "stack-stx-2-allowances", + &[ + amount.clone(), + pox_addr_tuple, + signature, + Value::UInt(auth_id), + allowed1, + allowed2, + ], + ); + stacker_nonce += 1; + let stack_2_ok_txid = submit_tx(&http_origin, &stack_2_ok_tx); + info!("Submitted stack_2_ok_txid txid: {stack_2_ok_txid}"); + expected_results.insert(stack_2_ok_txid, Value::okay_true()); + wait_for_nonce.insert(stacker_addr.clone(), stacker_nonce); + + // ***** Fail to stack with stackers[2] with two allowances (both too small) + let stacker = &stackers[2]; + let stacker_addr = tests::to_addr(stacker); + let mut stacker_nonce = 0; + + // Authorize the contract + let authorize_tx = make_contract_call( + stacker, + stacker_nonce, + call_fee, + naka_conf.burnchain.chain_id, + &boot_code_addr(false), + "pox-4", + "allow-contract-caller", + &[ + QualifiedContractIdentifier::new(sender_addr.clone().into(), contract_name.into()) + .into(), + Value::none(), + ], + ); + stacker_nonce += 1; + let authorize_txid = submit_tx(&http_origin, &authorize_tx); + info!("Submitted authorize txid: {authorize_txid}"); + expected_results.insert(authorize_txid, Value::okay_true()); + + let auth_id = 1; + let allowed1 = Value::UInt(POX_4_DEFAULT_STACKER_STX_AMT - 100); + let allowed2 = Value::UInt(POX_4_DEFAULT_STACKER_STX_AMT - 1000); + let pox_addr = PoxAddress::from_legacy( + AddressHashMode::SerializeP2PKH, + stacker_addr.bytes().clone(), + ); + let pox_addr_tuple: clarity::vm::Value = pox_addr.clone().as_clarity_tuple().unwrap().into(); + let signature_bytes = make_pox_4_signer_key_signature( + &pox_addr, + &signer_sk, + reward_cycle.into(), + &Pox4SignatureTopic::StackStx, + naka_conf.burnchain.chain_id, + 12_u128, + POX_4_DEFAULT_STACKER_STX_AMT, + auth_id, + ) + .unwrap() + .to_rsv(); + let signature = Value::some(clarity::vm::Value::buff_from(signature_bytes).unwrap()).unwrap(); + let stack_2_both_err_tx = make_contract_call( + stacker, + stacker_nonce, + call_fee, + naka_conf.burnchain.chain_id, + &sender_addr, + contract_name, + "stack-stx-2-allowances", + &[ + amount.clone(), + pox_addr_tuple.clone(), + signature.clone(), + Value::UInt(auth_id), + allowed1, + allowed2, + ], + ); + stacker_nonce += 1; + let stack_2_both_err_txid = submit_tx(&http_origin, &stack_2_both_err_tx); + info!("Submitted stack_2_both_err txid: {stack_2_both_err_txid}"); + expected_results.insert(stack_2_both_err_txid, Value::error(Value::UInt(0)).unwrap()); + wait_for_nonce.insert(stacker_addr.clone(), stacker_nonce); + + // ***** Fail to stack with stackers[2] with two allowances (first too small) + let allowed1 = Value::UInt(POX_4_DEFAULT_STACKER_STX_AMT - 100); + let allowed2 = Value::UInt(POX_4_DEFAULT_STACKER_STX_AMT); + + let stack_2_first_err_tx = make_contract_call( + stacker, + stacker_nonce, + call_fee, + naka_conf.burnchain.chain_id, + &sender_addr, + contract_name, + "stack-stx-2-allowances", + &[ + amount.clone(), + pox_addr_tuple.clone(), + signature.clone(), + Value::UInt(auth_id), + allowed1, + allowed2, + ], + ); + stacker_nonce += 1; + let stack_2_first_err_txid = submit_tx(&http_origin, &stack_2_first_err_tx); + info!("Submitted stack_2_first_err txid: {stack_2_first_err_txid}"); + expected_results.insert( + stack_2_first_err_txid, + Value::error(Value::UInt(0)).unwrap(), + ); + wait_for_nonce.insert(stacker_addr.clone(), stacker_nonce); + + // ***** Fail to stack with stackers[2] with two allowances (second too small) + let allowed1 = Value::UInt(POX_4_DEFAULT_STACKER_STX_AMT); + let allowed2 = Value::UInt(POX_4_DEFAULT_STACKER_STX_AMT - 100); + + let stack_2_second_err_tx = make_contract_call( + stacker, + stacker_nonce, + call_fee, + naka_conf.burnchain.chain_id, + &sender_addr, + contract_name, + "stack-stx-2-allowances", + &[ + amount.clone(), + pox_addr_tuple.clone(), + signature.clone(), + Value::UInt(auth_id), + allowed1, + allowed2, + ], + ); + stacker_nonce += 1; + let stack_2_second_err_txid = submit_tx(&http_origin, &stack_2_second_err_tx); + info!("Submitted stack_2_second_err txid: {stack_2_second_err_txid}"); + expected_results.insert( + stack_2_second_err_txid, + Value::error(Value::UInt(1)).unwrap(), + ); + wait_for_nonce.insert(stacker_addr.clone(), stacker_nonce); + + // ***** Fail to stack with stackers[2] with no allowance + let stack_no_allowance_err_tx = make_contract_call( + stacker, + stacker_nonce, + call_fee, + naka_conf.burnchain.chain_id, + &sender_addr, + contract_name, + "stack-stx-no-allowance", + &[ + amount.clone(), + pox_addr_tuple.clone(), + signature.clone(), + Value::UInt(auth_id), + ], + ); + stacker_nonce += 1; + let stack_no_allowance_err_txid = submit_tx(&http_origin, &stack_no_allowance_err_tx); + info!("Submitted stack_no_allowance_err txid: {stack_no_allowance_err_txid}"); + expected_results.insert( + stack_no_allowance_err_txid, + Value::error(Value::UInt(128)).unwrap(), + ); + wait_for_nonce.insert(stacker_addr.clone(), stacker_nonce); + + // ***** Stack successfully with stackers[2] with with-all-assets-unsafe + let stack_all_tx = make_contract_call( + stacker, + stacker_nonce, + call_fee, + naka_conf.burnchain.chain_id, + &sender_addr, + contract_name, + "stack-stx-all", + &[ + amount.clone(), + pox_addr_tuple.clone(), + signature.clone(), + Value::UInt(auth_id), + ], + ); + stacker_nonce += 1; + let stack_all_txid = submit_tx(&http_origin, &stack_all_tx); + info!("Submitted stack_all txid: {stack_all_txid}"); + expected_results.insert(stack_all_txid, Value::okay_true()); + wait_for_nonce.insert(stacker_addr.clone(), stacker_nonce); + + wait_for(60, || { + for (addr, expected_nonce) in &wait_for_nonce { + let cur_nonce = get_account(&http_origin, addr).nonce; + if cur_nonce != *expected_nonce { + return Ok(false); + } + } + Ok(true) + }) + .expect("Timed out waiting for contract calls"); + + let blocks = test_observer::get_blocks(); + let mut found = 0; + for block in blocks.iter() { + for tx in block.get("transactions").unwrap().as_array().unwrap() { + let txid = tx + .get("txid") + .unwrap() + .as_str() + .unwrap() + .strip_prefix("0x") + .unwrap(); + if let Some(expected) = expected_results.get(txid) { + let raw_result = tx.get("raw_result").unwrap().as_str().unwrap(); + let parsed = Value::try_deserialize_hex_untyped(&raw_result[2..]).unwrap(); + found += 1; + assert_eq!(&parsed, expected); + } else { + // If there are any txids we don't expect, panic, because it probably means + // there is an error in the test itself. + panic!("Found unexpected txid: {txid}"); + } + } + } + + assert_eq!( + found, + expected_results.len(), + "Should have found all expected txs" + ); + + coord_channel + .lock() + .expect("Mutex poisoned") + .stop_chains_coordinator(); + run_loop_stopper.store(false, Ordering::SeqCst); + + run_loop_thread.join().unwrap(); +} + #[test] #[ignore] /// Verify the error handling and rollback works as expected in From c824cde92810d9abe4e322160cefc4c8b3a5f45f Mon Sep 17 00:00:00 2001 From: Brice Dobry Date: Wed, 1 Oct 2025 16:11:50 -0400 Subject: [PATCH 28/35] chore: remove todo for event on allowance violation We decided to remove this because it doesn't really make sense. --- clarity/src/vm/functions/post_conditions.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/clarity/src/vm/functions/post_conditions.rs b/clarity/src/vm/functions/post_conditions.rs index 12dd2f3294..c5621f3386 100644 --- a/clarity/src/vm/functions/post_conditions.rs +++ b/clarity/src/vm/functions/post_conditions.rs @@ -208,7 +208,6 @@ pub fn special_restrict_assets( // - Emit an event if let Some(violation_index) = check_allowances(&asset_owner, &allowances, asset_maps)? { env.global_context.roll_back()?; - // TODO: Emit an event about the allowance violation return Value::error(Value::UInt(violation_index)); } @@ -293,7 +292,6 @@ pub fn special_as_contract( Ok(None) => {} Ok(Some(violation_index)) => { nested_env.global_context.roll_back()?; - // TODO: Emit an event about the allowance violation return Value::error(Value::UInt(violation_index)); } Err(e) => { From a6afc031234e90cbc00f8b3ce1558cf40951efb7 Mon Sep 17 00:00:00 2001 From: Brice Dobry Date: Wed, 1 Oct 2025 16:13:09 -0400 Subject: [PATCH 29/35] chore: formatting --- stackslib/src/config/mod.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/stackslib/src/config/mod.rs b/stackslib/src/config/mod.rs index bfd13535ae..c09bed9848 100644 --- a/stackslib/src/config/mod.rs +++ b/stackslib/src/config/mod.rs @@ -1101,9 +1101,7 @@ impl Config { pub fn add_initial_balance(&mut self, address: String, amount: u64) { let new_balance = InitialBalance { - address: PrincipalData::parse(&address) - .unwrap() - .into(), + address: PrincipalData::parse(&address).unwrap().into(), amount, }; self.initial_balances.push(new_balance); From 7c47871846e5b5f8371f2667f025a9c4236dd99b Mon Sep 17 00:00:00 2001 From: Brice Dobry Date: Thu, 9 Oct 2025 14:06:39 -0400 Subject: [PATCH 30/35] fix: various improvements suggested in PR review --- clarity/src/vm/docs/mod.rs | 6 +- clarity/src/vm/functions/post_conditions.rs | 95 ++++++++++++++++----- 2 files changed, 80 insertions(+), 21 deletions(-) diff --git a/clarity/src/vm/docs/mod.rs b/clarity/src/vm/docs/mod.rs index 9ee424d1e0..9eb1190532 100644 --- a/clarity/src/vm/docs/mod.rs +++ b/clarity/src/vm/docs/mod.rs @@ -2683,7 +2683,11 @@ from the `asset-owner` of the enclosing `restrict-assets?` or `as-contract?` expression. `with-nft` is not allowed outside of `restrict-assets?` or `as-contract?` contexts. Note that `token-name` should match the name used in the `define-non-fungible-token` call in the contract. When `"*"` is used for -the token name, the allowance applies to **all** NFTs defined in `contract-id`."#, +the token name, the allowance applies to **all** NFTs defined in `contract-id`. +Note that the type of the elements in `asset-identifiers` should match the type +defined in the `define-non-fungible-token` call in the contract, but this is +**not** checked by the type-checker. An identifier with the wrong type will +simply never match any asset."#, example: r#" (define-non-fungible-token stackaroo uint) (nft-mint? stackaroo u123 tx-sender) diff --git a/clarity/src/vm/functions/post_conditions.rs b/clarity/src/vm/functions/post_conditions.rs index c5621f3386..22f16caad7 100644 --- a/clarity/src/vm/functions/post_conditions.rs +++ b/clarity/src/vm/functions/post_conditions.rs @@ -15,15 +15,16 @@ use std::collections::{HashMap, HashSet}; -use clarity_types::types::{AssetIdentifier, PrincipalData}; +use clarity_types::types::{AssetIdentifier, PrincipalData, StandardPrincipalData}; use crate::vm::analysis::type_checker::v2_1::natives::post_conditions::MAX_ALLOWANCES; use crate::vm::contexts::AssetMap; use crate::vm::costs::cost_functions::ClarityCostFunction; -use crate::vm::costs::{constants as cost_constants, runtime_cost, CostTracker}; +use crate::vm::costs::{constants as cost_constants, runtime_cost, CostTracker, MemoryConsumer}; use crate::vm::errors::{ check_arguments_at_least, CheckErrors, InterpreterError, InterpreterResult, }; +use crate::vm::functions::NativeFunctions; use crate::vm::representations::SymbolicExpression; use crate::vm::types::Value; use crate::vm::{eval, Environment, LocalContext}; @@ -54,6 +55,38 @@ pub enum Allowance { All, } +impl Allowance { + /// Returns the size in bytes of the allowance when stored in memory. + /// This is used to account for memory usage when evaluating `as-contract?` + /// and `restrict-assets?` expressions. + pub fn size_in_bytes(&self) -> Result { + match self { + Allowance::Stx(_) => Ok(std::mem::size_of::()), + Allowance::Ft(ft) => Ok(std::mem::size_of::() + + std::mem::size_of::() + + ft.asset.contract_identifier.name.len() as usize + + ft.asset.asset_name.len() as usize), + Allowance::Nft(nft) => { + let mut total_size = std::mem::size_of::() + + std::mem::size_of::() + + nft.asset.contract_identifier.name.len() as usize + + nft.asset.asset_name.len() as usize; + + for id in &nft.asset_ids { + let memory_use = id.get_memory_use().map_err(|e| { + InterpreterError::Expect(format!("Failed to calculate memory use: {e}")) + })?; + total_size += memory_use as usize; + } + + Ok(total_size) + } + Allowance::Stacking(_) => Ok(std::mem::size_of::()), + Allowance::All => Ok(0), + } + } +} + fn eval_allowance( allowance_expr: &SymbolicExpression, env: &mut Environment, @@ -66,9 +99,15 @@ fn eval_allowance( .split_first() .ok_or(CheckErrors::NonFunctionApplication)?; let name = name_expr.match_atom().ok_or(CheckErrors::BadFunctionName)?; - - match name.as_str() { - "with-stx" => { + let Some(ref native_function) = NativeFunctions::lookup_by_name_at_version( + name, + env.contract_context.get_clarity_version(), + ) else { + return Err(CheckErrors::ExpectedAllowanceExpr(name.to_string()).into()); + }; + + match native_function { + NativeFunctions::AllowanceWithStx => { if rest.len() != 1 { return Err(CheckErrors::IncorrectArgumentCount(1, rest.len()).into()); } @@ -76,7 +115,7 @@ fn eval_allowance( let amount = amount.expect_u128()?; Ok(Allowance::Stx(StxAllowance { amount })) } - "with-ft" => { + NativeFunctions::AllowanceWithFt => { if rest.len() != 3 { return Err(CheckErrors::IncorrectArgumentCount(3, rest.len()).into()); } @@ -105,7 +144,7 @@ fn eval_allowance( Ok(Allowance::Ft(FtAllowance { asset, amount })) } - "with-nft" => { + NativeFunctions::AllowanceWithNft => { if rest.len() != 3 { return Err(CheckErrors::IncorrectArgumentCount(3, rest.len()).into()); } @@ -134,7 +173,7 @@ fn eval_allowance( Ok(Allowance::Nft(NftAllowance { asset, asset_ids })) } - "with-stacking" => { + NativeFunctions::AllowanceWithStacking => { if rest.len() != 1 { return Err(CheckErrors::IncorrectArgumentCount(1, rest.len()).into()); } @@ -142,7 +181,7 @@ fn eval_allowance( let amount = amount.expect_u128()?; Ok(Allowance::Stacking(StackingAllowance { amount })) } - "with-all-assets-unsafe" => { + NativeFunctions::AllowanceAll => { if !rest.is_empty() { return Err(CheckErrors::IncorrectArgumentCount(1, rest.len()).into()); } @@ -182,6 +221,10 @@ pub fn special_restrict_assets( allowance_list.len(), )?; + if allowance_list.len() > MAX_ALLOWANCES { + return Err(CheckErrors::TooManyAllowances(MAX_ALLOWANCES, allowance_list.len()).into()); + } + let mut allowances = Vec::with_capacity(allowance_list.len()); for allowance in allowance_list { allowances.push(eval_allowance(allowance, env, context)?); @@ -205,10 +248,17 @@ pub fn special_restrict_assets( // If the allowances are violated: // - Rollback the context - // - Emit an event - if let Some(violation_index) = check_allowances(&asset_owner, &allowances, asset_maps)? { - env.global_context.roll_back()?; - return Value::error(Value::UInt(violation_index)); + // - Return an error with the index of the violated allowance + match check_allowances(&asset_owner, &allowances, asset_maps) { + Ok(None) => {} + Ok(Some(violation_index)) => { + env.global_context.roll_back()?; + return Value::error(Value::UInt(violation_index)); + } + Err(e) => { + env.global_context.roll_back()?; + return Err(e); + } } env.global_context.commit()?; @@ -255,14 +305,19 @@ pub fn special_as_contract( allowance_list.len(), )?; - let mut allowances = Vec::with_capacity(allowance_list.len()); - for allowance in allowance_list { - allowances.push(eval_allowance(allowance, env, context)?); - } - - let mut memory_use = 0; + let mut memory_use = 0u64; finally_drop_memory!( env, memory_use; { + let mut allowances = Vec::with_capacity(allowance_list.len()); + for allowance_expr in allowance_list { + let allowance = eval_allowance(allowance_expr, env, context)?; + let allowance_memory = u64::try_from(allowance.size_in_bytes()?) + .map_err(|_| InterpreterError::Expect("Allowance size too large".into()))?; + env.add_memory(allowance_memory)?; + memory_use += allowance_memory; + allowances.push(allowance); + } + env.add_memory(cost_constants::AS_CONTRACT_MEMORY)?; memory_use += cost_constants::AS_CONTRACT_MEMORY; @@ -287,7 +342,7 @@ pub fn special_as_contract( // If the allowances are violated: // - Rollback the context - // - Emit an event + // - Return an error with the index of the violated allowance match check_allowances(&contract_principal, &allowances, asset_maps) { Ok(None) => {} Ok(Some(violation_index)) => { From 0a44836e39953065bf0f85ebc91f5a23da1bb17a Mon Sep 17 00:00:00 2001 From: Brice Dobry Date: Thu, 9 Oct 2025 14:25:14 -0400 Subject: [PATCH 31/35] test: `with-nft` with wrong identifier type --- clarity/src/vm/tests/post_conditions.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/clarity/src/vm/tests/post_conditions.rs b/clarity/src/vm/tests/post_conditions.rs index 7a966b56d7..9b18f8b062 100644 --- a/clarity/src/vm/tests/post_conditions.rs +++ b/clarity/src/vm/tests/post_conditions.rs @@ -577,6 +577,21 @@ fn test_as_contract_with_nft_empty_id_list() { assert_eq!(expected, execute(snippet).unwrap().unwrap()); } +#[test] +fn test_as_contract_with_nft_wrong_type() { + let snippet = r#" +(define-non-fungible-token stackaroo uint) +(nft-mint? stackaroo u122 current-contract) +(nft-mint? stackaroo u123 current-contract) +(let ((recipient tx-sender)) + (as-contract? ((with-nft current-contract "stackaroo" (list 123))) + (try! (nft-transfer? stackaroo u123 tx-sender recipient)) + ) +)"#; + let expected = Value::error(Value::UInt(0)).unwrap(); + assert_eq!(expected, execute(snippet).unwrap().unwrap()); +} + #[test] fn test_as_contract_with_nft_wildcard_ok() { let snippet = r#" From 9e95356050a7d95b238a654dabf7746c574ce455 Mon Sep 17 00:00:00 2001 From: Brice Dobry Date: Thu, 9 Oct 2025 14:29:01 -0400 Subject: [PATCH 32/35] doc: add more detail to `with-stacking` doc --- clarity/src/vm/docs/mod.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/clarity/src/vm/docs/mod.rs b/clarity/src/vm/docs/mod.rs index 9eb1190532..dec8b015d5 100644 --- a/clarity/src/vm/docs/mod.rs +++ b/clarity/src/vm/docs/mod.rs @@ -2714,7 +2714,11 @@ const ALLOWANCE_WITH_STACKING: SpecialAPI = SpecialAPI { expression. `with-stacking` is not allowed outside of `restrict-assets?` or `as-contract?` contexts. This restricts calls to the active PoX contract that either delegate funds for stacking or stack directly, ensuring that the -locked amount is limited by the amount of uSTX specified.", +locked amount is limited by the amount of uSTX specified. Note that the +amount specified here is the total amount allowed to be stacked, i.e. a call to +`stack-increase` will need an allowance for the new total, not just the +increase amount. +", example: r#" (restrict-assets? tx-sender ((with-stacking u1000000000000)) From 6b972250f7b1440c690cd947532fc84e689e0fd1 Mon Sep 17 00:00:00 2001 From: Brice Dobry Date: Thu, 9 Oct 2025 15:33:17 -0400 Subject: [PATCH 33/35] refactor: use `Vec` for allowances This replaces a `HashSet` which was using the serialized value for quick lookups, but added the extra overhead on setup and use of serializing the values. With this new design, the value is used directly, with no new allocations, and then a linear search is required through the `Vec`, but in the common case, the number of entries will be very low. --- clarity/src/vm/functions/post_conditions.rs | 49 +++++++++++---------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/clarity/src/vm/functions/post_conditions.rs b/clarity/src/vm/functions/post_conditions.rs index 22f16caad7..f3fe958630 100644 --- a/clarity/src/vm/functions/post_conditions.rs +++ b/clarity/src/vm/functions/post_conditions.rs @@ -13,7 +13,7 @@ // You should have received a copy of the GNU General Public License // along with this program. If not, see . -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use clarity_types::types::{AssetIdentifier, PrincipalData, StandardPrincipalData}; @@ -249,7 +249,7 @@ pub fn special_restrict_assets( // If the allowances are violated: // - Rollback the context // - Return an error with the index of the violated allowance - match check_allowances(&asset_owner, &allowances, asset_maps) { + match check_allowances(&asset_owner, allowances, asset_maps) { Ok(None) => {} Ok(Some(violation_index)) => { env.global_context.roll_back()?; @@ -343,7 +343,7 @@ pub fn special_as_contract( // If the allowances are violated: // - Rollback the context // - Return an error with the index of the violated allowance - match check_allowances(&contract_principal, &allowances, asset_maps) { + match check_allowances(&contract_principal, allowances, asset_maps) { Ok(None) => {} Ok(Some(violation_index)) => { nested_env.global_context.roll_back()?; @@ -381,20 +381,25 @@ pub fn special_as_contract( /// allowances are satisfied, return `Ok(None)`. fn check_allowances( owner: &PrincipalData, - allowances: &[Allowance], + allowances: Vec, assets: &AssetMap, ) -> InterpreterResult> { // Elements are (index in allowances, amount) let mut stx_allowances: Vec<(usize, u128)> = Vec::new(); // Map assets to a vector of (index in allowances, amount) - let mut ft_allowances: HashMap<&AssetIdentifier, Vec<(usize, u128)>> = HashMap::new(); - // Map assets to a tuple with the first allowance's index and a hashset of - // serialized asset identifiers - let mut nft_allowances: HashMap<&AssetIdentifier, (usize, HashSet)> = HashMap::new(); + let mut ft_allowances: HashMap> = HashMap::new(); + // Map assets to a tuple with the first allowance's index and a vector of + // asset identifiers. We use Vec instead of HashSet because: + // 1. Most NFT IDs are simple (`uint`s), making Value::eq() very fast + // 2. Linear search through ≤128 items is cache-friendly and fast + // 3. Avoids serialization cost during both setup and lookup phases + // 4. Simpler implementation with lower memory overhead (no cloning or + // space used for serialization) + let mut nft_allowances: HashMap)> = HashMap::new(); // Elements are (index in allowances, amount) let mut stacking_allowances: Vec<(usize, u128)> = Vec::new(); - for (i, allowance) in allowances.iter().enumerate() { + for (i, allowance) in allowances.into_iter().enumerate() { match allowance { Allowance::All => { // any asset movement is allowed @@ -405,17 +410,15 @@ fn check_allowances( } Allowance::Ft(ft) => { ft_allowances - .entry(&ft.asset) + .entry(ft.asset) .or_default() .push((i, ft.amount)); } Allowance::Nft(nft) => { - let (_, set) = nft_allowances - .entry(&nft.asset) - .or_insert_with(|| (i, HashSet::new())); - for id in &nft.asset_ids { - set.insert(id.serialize_to_hex()?); - } + let (_, vec) = nft_allowances + .entry(nft.asset) + .or_insert_with(|| (i, Vec::new())); + vec.extend(nft.asset_ids); } Allowance::Stacking(stacking) => { stacking_allowances.push((i, stacking.amount)); @@ -495,16 +498,16 @@ fn check_allowances( // Check NFT movements if let Some(nft_moved) = assets.get_all_nonfungible_tokens(owner) { for (asset, ids_moved) in nft_moved { - let mut merged: Vec<(usize, HashSet)> = Vec::new(); - if let Some((index, allowance_map)) = nft_allowances.get(asset) { - merged.push((*index, allowance_map.clone())); + let mut merged: Vec<(usize, &Vec)> = Vec::new(); + if let Some((index, allowance_vec)) = nft_allowances.get(asset) { + merged.push((*index, allowance_vec)); } - if let Some((index, allowance_map)) = nft_allowances.get(&AssetIdentifier { + if let Some((index, allowance_vec)) = nft_allowances.get(&AssetIdentifier { contract_identifier: asset.contract_identifier.clone(), asset_name: "*".into(), }) { - merged.push((*index, allowance_map.clone())); + merged.push((*index, allowance_vec)); } if merged.is_empty() { @@ -515,10 +518,10 @@ fn check_allowances( // Sort by allowance index so we check allowances in order merged.sort_by_key(|(idx, _)| *idx); - for (index, allowance_map) in merged { + for (index, allowance_vec) in merged { // Check against the NFT allowances for id_moved in ids_moved { - if !allowance_map.contains(&id_moved.serialize_to_hex()?) { + if !allowance_vec.contains(id_moved) { return Ok(Some(u128::try_from(index).map_err(|_| { InterpreterError::Expect("failed to convert index to u128".into()) })?)); From 01356c88ef67d7f1ae54c28061a12ce9d85cda1c Mon Sep 17 00:00:00 2001 From: Brice Dobry Date: Thu, 9 Oct 2025 17:24:20 -0400 Subject: [PATCH 34/35] fix: update consensus snapshots This is necessary because the costs-4 contract was updated. I verified that the consensus tests passed if I removed the new functions in that contract. --- ...sts__consensus__append_block_with_contract_call_success.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_block_with_contract_call_success.snap b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_block_with_contract_call_success.snap index 8aec4b5474..f2e7904d9c 100644 --- a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_block_with_contract_call_success.snap +++ b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_block_with_contract_call_success.snap @@ -118,7 +118,7 @@ expression: result ), )), Success(ExpectedBlockOutput( - marf_hash: "416e728daeec4de695c89d15eede8ddb7b85fb4af82daffb1e0d8166a3e93451", + marf_hash: "0e2829cdc4ea57ab95e61b1c17c604c51b501ab684b75c90286623253aceac31", transactions: [ ExpectedTransactionOutput( return_type: Response(ResponseData( From 8044f76bd19b22e665e9313a4b129d1d899c5a08 Mon Sep 17 00:00:00 2001 From: Brice Dobry Date: Thu, 9 Oct 2025 17:32:15 -0400 Subject: [PATCH 35/35] fix: update other consensus snapshots --- stackslib/src/chainstate/tests/consensus.rs | 2 +- ...append_chainstate_error_expression_stack_depth_too_deep.snap | 2 +- ..._lib__chainstate__tests__consensus__append_empty_blocks.snap | 2 +- ...instate__tests__consensus__append_stx_transfers_success.snap | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/stackslib/src/chainstate/tests/consensus.rs b/stackslib/src/chainstate/tests/consensus.rs index f224f72646..92130299d8 100644 --- a/stackslib/src/chainstate/tests/consensus.rs +++ b/stackslib/src/chainstate/tests/consensus.rs @@ -799,7 +799,7 @@ fn test_append_block_with_contract_upload_success() { ), )), Success(ExpectedBlockOutput( - marf_hash: "3520c2dd96f7d91e179c4dcd00f3c49c16d6ec21434fb16921922558282eab26", + marf_hash: "2b3131abe0cf6f5b762551c4ac1bca4ae52a0a999b0713228bf40a70916e388b", transactions: [ ExpectedTransactionOutput( return_type: Response(ResponseData( diff --git a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_chainstate_error_expression_stack_depth_too_deep.snap b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_chainstate_error_expression_stack_depth_too_deep.snap index 6089465bfa..a3ace14e23 100644 --- a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_chainstate_error_expression_stack_depth_too_deep.snap +++ b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_chainstate_error_expression_stack_depth_too_deep.snap @@ -6,5 +6,5 @@ expression: result Failure("Invalid Stacks block a60c62267d58f1ea29c64b2f86d62cf210ff5ab14796abfa947ca6d95007d440: ClarityError(Parse(ParseError { err: ExpressionStackDepthTooDeep, pre_expressions: None, diagnostic: Diagnostic { level: Error, message: \"AST has too deep of an expression nesting. The maximum stack depth is 64\", spans: [], suggestion: None } }))"), Failure("Invalid Stacks block 238f2ce280580228f19c8122a9bdd0c61299efabe59d8c22c315ee40a865cc7b: ClarityError(Parse(ParseError { err: ExpressionStackDepthTooDeep, pre_expressions: None, diagnostic: Diagnostic { level: Error, message: \"AST has too deep of an expression nesting. The maximum stack depth is 64\", spans: [], suggestion: None } }))"), Failure("Invalid Stacks block b5dd8cdc0f48b30d355a950077f7c9b20bf01062e9c96262c28f17fff55a2b0f: ClarityError(Parse(ParseError { err: ExpressionStackDepthTooDeep, pre_expressions: None, diagnostic: Diagnostic { level: Error, message: \"AST has too deep of an expression nesting. The maximum stack depth is 64\", spans: [], suggestion: None } }))"), - Failure("Invalid Stacks block cfbddc874c465753158a065eff61340e933d33671633843dde0fbd2bfaaac7a4: ClarityError(Parse(ParseError { err: ExpressionStackDepthTooDeep, pre_expressions: None, diagnostic: Diagnostic { level: Error, message: \"AST has too deep of an expression nesting. The maximum stack depth is 64\", spans: [], suggestion: None } }))"), + Failure("Invalid Stacks block cdfe1ac0e60b459fc417de8ce0678d360fa9f43df512e90fcefc5108f6e5bc17: ClarityError(Parse(ParseError { err: ExpressionStackDepthTooDeep, pre_expressions: None, diagnostic: Diagnostic { level: Error, message: \"AST has too deep of an expression nesting. The maximum stack depth is 64\", spans: [], suggestion: None } }))"), ] diff --git a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_empty_blocks.snap b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_empty_blocks.snap index a1f13d92b8..41078dea1b 100644 --- a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_empty_blocks.snap +++ b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_empty_blocks.snap @@ -37,7 +37,7 @@ expression: result ), )), Success(ExpectedBlockOutput( - marf_hash: "23ecbcb91cac914ba3994a15f3ea7189bcab4e9762530cd0e6c7d237fcd6dc78", + marf_hash: "dedba92e27a93c88c9631c2ecad3b191ada28f49b1e9f973b573af0bebf8445e", transactions: [], total_block_cost: ExecutionCost( write_length: 0, diff --git a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_stx_transfers_success.snap b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_stx_transfers_success.snap index c4be6d8a74..7c5e9db804 100644 --- a/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_stx_transfers_success.snap +++ b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_stx_transfers_success.snap @@ -157,7 +157,7 @@ expression: result ), )), Success(ExpectedBlockOutput( - marf_hash: "66eed8c0ab31db111a5adcc83d38a7004c6e464e3b9fb9f52ec589bc6d5f2d32", + marf_hash: "bdf60f076c966aeec77ecbfff4661fc13747cb768df03637a9bb740d2413bf48", transactions: [ ExpectedTransactionOutput( return_type: Response(ResponseData(