diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a49caf668..47cefc18af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,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. - Fixed a bug caused by a miner winning a sortition with a block commit that pointed to a previous tip, which would cause the miner to try and reorg itself. [#6481](https://github.com/stacks-network/stacks-core/issues/6481) diff --git a/clarity-types/src/errors/analysis.rs b/clarity-types/src/errors/analysis.rs index 09e4e5f056..5cb3176c02 100644 --- a/clarity-types/src/errors/analysis.rs +++ b/clarity-types/src/errors/analysis.rs @@ -307,6 +307,16 @@ pub enum CheckErrors { // time checker errors ExecutionTimeExpired, + + // contract post-conditions + ExpectedListOfAllowances(String, i32), + AllowanceExprNotAllowed, + ExpectedAllowanceExpr(String), + WithAllAllowanceNotAllowed, + WithAllAllowanceNotAlone, + WithNftExpectedListOfIdentifiers, + MaxIdentifierLengthExceeded(u32, u32), + TooManyAllowances(usize, usize), } #[derive(Debug, PartialEq)] @@ -605,6 +615,14 @@ 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::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(), + 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, 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-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-types/src/types/mod.rs b/clarity-types/src/types/mod.rs index 5b8dedd864..72dad1ae7c 100644 --- a/clarity-types/src/types/mod.rs +++ b/clarity-types/src/types/mod.rs @@ -1235,6 +1235,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-types/src/types/signatures.rs b/clarity-types/src/types/signatures.rs index 8ab6f96fc6..9115aeae92 100644 --- a/clarity-types/src/types/signatures.rs +++ b/clarity-types/src/types/signatures.rs @@ -855,6 +855,8 @@ impl TypeSignature { pub const STRING_ASCII_MAX: TypeSignature = Self::type_ascii_const(MAX_VALUE_SIZE); /// String ASCII type with length 40. pub const STRING_ASCII_40: TypeSignature = Self::type_ascii_const(40); + /// String ASCII type with length 128. + pub const STRING_ASCII_128: TypeSignature = Self::type_ascii_const(128); /// String UTF8 type with minimum length (`1`). pub const STRING_UTF8_MIN: TypeSignature = Self::type_string_utf8(1); @@ -908,7 +910,7 @@ impl TypeSignature { /// Creates a string ASCII type with the specified length. /// It may panic if the provided length is invalid. - #[cfg(test)] + #[cfg(any(test, feature = "testing"))] pub const fn new_ascii_type_checked(len: u32) -> Self { Self::type_ascii_const(len) } diff --git a/clarity/src/vm/analysis/arithmetic_checker/mod.rs b/clarity/src/vm/analysis/arithmetic_checker/mod.rs index cfacd5b9ee..fac859bace 100644 --- a/clarity/src/vm/analysis/arithmetic_checker/mod.rs +++ b/clarity/src/vm/analysis/arithmetic_checker/mod.rs @@ -173,9 +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 => 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 21b2bbd8f7..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) } @@ -427,6 +508,45 @@ 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::ExpectedListOfAllowances( + "restrict-assets?".into(), + 2, + ))?; + 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) + } + 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 4cd38adb1c..827678dba4 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 @@ -777,12 +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 => { + 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/mod.rs b/clarity/src/vm/analysis/type_checker/v2_1/mod.rs index 29f0a8e715..8a2ab5c6fc 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 397d4eb323..fd769a22d3 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 @@ -39,6 +39,7 @@ mod assets; mod conversions; mod maps; mod options; +pub(crate) mod post_conditions; mod sequences; #[allow(clippy::large_enum_variant)] @@ -1210,6 +1211,15 @@ impl TypedNativeFunction { CheckErrors::Expects("FATAL: Legal Clarity response type marked invalid".into()) })?, ))), + 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..6ac1104ef6 --- /dev/null +++ b/clarity/src/vm/analysis/type_checker/v2_1/natives/post_conditions.rs @@ -0,0 +1,309 @@ +// 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::{SequenceSubtype, 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; + +/// 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; + +pub fn check_restrict_assets( + checker: &mut TypeChecker, + args: &[SymbolicExpression], + context: &TypingContext, +) -> Result { + check_arguments_at_least(3, args)?; + + 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 + .get(2..) + .ok_or(CheckErrors::CheckerImplementationFailure)?; + + if allowance_list.len() > MAX_ALLOWANCES { + return Err(CheckErrors::TooManyAllowances(MAX_ALLOWANCES, allowance_list.len()).into()); + } + + 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 { + if check_allowance(checker, allowance, context)? { + return Err(CheckErrors::WithAllAllowanceNotAllowed.into()); + } + } + + // 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::UIntType, + )?) +} + +pub fn check_as_contract( + checker: &mut TypeChecker, + args: &[SymbolicExpression], + context: &TypingContext, +) -> Result { + check_arguments_at_least(2, args)?; + + let allowance_list = args + .first() + .ok_or(CheckErrors::CheckerImplementationFailure)? + .match_list() + .ok_or(CheckErrors::ExpectedListOfAllowances( + "as-contract?".into(), + 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()); + } + + runtime_cost( + ClarityCostFunction::AnalysisListItemsCheck, + checker, + allowance_list.len() + body_exprs.len(), + )?; + + for allowance in allowance_list { + 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 + 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::UIntType, + )?) +} + +/// 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()) +} + +/// 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, +) -> Result { + 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), + 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) + } + NativeFunctions::AllowanceAll => check_allowance_all(checker, args, context), + _ => 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, +) -> Result { + check_argument_count(1, args)?; + + checker.type_check_expects( + args.first() + .ok_or(CheckErrors::CheckerImplementationFailure)?, + context, + &TypeSignature::UIntType, + )?; + + Ok(false) +} + +/// 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, +) -> Result { + check_argument_count(3, args)?; + + 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, + &TypeSignature::STRING_ASCII_128, + )?; + checker.type_check_expects( + args.get(2) + .ok_or(CheckErrors::CheckerImplementationFailure)?, + context, + &TypeSignature::UIntType, + )?; + + Ok(false) +} + +/// 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, +) -> Result { + check_argument_count(3, args)?; + + 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, + &TypeSignature::STRING_ASCII_128, + )?; + + // Asset identifiers must be a Clarity list with any type of elements + 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()); + }; + if list_data.get_max_len() > MAX_NFT_IDENTIFIERS { + return Err(CheckErrors::MaxIdentifierLengthExceeded( + MAX_NFT_IDENTIFIERS, + list_data.get_max_len(), + ) + .into()); + } + + Ok(false) +} + +/// Type check a `with-stacking` allowance expression. +/// `(with-stacking amount:uint)` +fn check_allowance_with_stacking( + checker: &mut TypeChecker, + args: &[SymbolicExpression], + context: &TypingContext, +) -> Result { + check_argument_count(1, args)?; + + checker.type_check_expects( + args.first() + .ok_or(CheckErrors::CheckerImplementationFailure)?, + context, + &TypeSignature::UIntType, + )?; + + Ok(false) +} + +/// Type check an `with-all-assets-unsafe` allowance expression. +/// `(with-all-assets-unsafe)` +fn check_allowance_all( + _checker: &mut TypeChecker, + args: &[SymbolicExpression], + _context: &TypingContext, +) -> Result { + check_argument_count(0, args)?; + + Ok(true) +} 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 9a694ada7b..31b2459f0d 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 { 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/mod.rs b/clarity/src/vm/analysis/type_checker/v2_1/tests/mod.rs index 413ae80f29..bd30b6b30c 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 @@ -40,6 +40,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..eae392c198 --- /dev/null +++ b/clarity/src/vm/analysis/type_checker/v2_1/tests/post_conditions.rs @@ -0,0 +1,903 @@ +// 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::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; + +/// 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::UIntType).unwrap() + ), + // literal asset owner + ( + "(restrict-assets? 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4 ((with-stx u1000)) true)", + 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::UIntType).unwrap() + ), + // variable asset owner + ( + "(let ((p tx-sender)) + (restrict-assets? p ((with-stx u1000)) true))", + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::UIntType).unwrap() + ), + // no allowances + ( + "(restrict-assets? tx-sender () true)", + 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::UIntType).unwrap() + ), + // multiple body expressions + ( + "(restrict-assets? tx-sender ((with-stx u1000)) (+ u1 u2) true)", + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::UIntType).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, + ), + // too many allowances + ( + &format!( + "(restrict-assets? tx-sender ({} ) true)", + std::iter::repeat_n("(with-stx u1)", 130) + .collect::>() + .join(" ") + ), + 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, TypeSignature::UIntType) + .unwrap() + .into(), + TypeSignature::new_response(TypeSignature::NoType, TypeSignature::IntType) + .unwrap() + .into(), + ), + ), + ]; + + 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(code, version).unwrap_err().err, + "{code}", + ); + } else { + assert_eq!( + expected_type, + &type_check_helper_version(code, version).unwrap(), + "{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(code, version).unwrap_err().err, + "{code}", + ); + } else { + assert_eq!( + expected_err, + type_check_helper_version(code, version) + .unwrap_err() + .err + .as_ref(), + "{code}", + ); + } + } +} + +/// 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::UIntType).unwrap() + ), + // no allowances + ( + "(as-contract? () true)", + 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::UIntType).unwrap() + ), + // multiple body expressions + ( + "(as-contract? ((with-stx u1000)) (+ u1 u2) true)", + 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::UIntType).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, + ), + // 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, + ), + // too many allowances + ( + &format!( + "(as-contract? ({} ) true)", + std::iter::repeat_n("(with-stx u1)", 130) + .collect::>() + .join(" ") + ), + 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, TypeSignature::UIntType) + .unwrap() + .into(), + TypeSignature::new_response(TypeSignature::NoType, TypeSignature::IntType) + .unwrap() + .into(), + ), + ), + ]; + + for (code, expected_type) in &good { + 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, + "{code}" + ); + } else { + assert_eq!( + expected_type, + &type_check_helper_version(code, version).unwrap(), + "{code}" + ); + } + } + + for (code, expected_err) in &bad { + 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, + "{code}" + ); + } else { + assert_eq!( + expected_err, + type_check_helper_version(code, version) + .unwrap_err() + .err + .as_ref(), + "{code}" + ); + } + } +} + +/// 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::UIntType).unwrap() + ), + // zero amount + ( + "(restrict-assets? tx-sender ((with-stx u0)) true)", + TypeSignature::new_response(TypeSignature::BoolType, TypeSignature::UIntType).unwrap() + ), + // large amount + ( + "(restrict-assets? tx-sender ((with-stx u340282366920938463463374607431768211455)) true)", + 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::UIntType).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_ascii_type_checked(4).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 { + if version < ClarityVersion::Clarity4 { + assert_eq!( + CheckErrors::UnknownFunction("restrict-assets?".to_string()), + *type_check_helper_version(code, version).unwrap_err().err, + "{code}" + ); + } else { + assert_eq!( + expected_type, + &type_check_helper_version(code, version).unwrap(), + "{code}" + ); + } + } + + for (code, expected_err) in &bad { + if version < ClarityVersion::Clarity4 { + assert_eq!( + CheckErrors::UnknownFunction("restrict-assets?".to_string()), + *type_check_helper_version(code, version).unwrap_err().err, + "{code}" + ); + } else { + assert_eq!( + expected_err, + type_check_helper_version(code, version) + .unwrap_err() + .err + .as_ref(), + "{code}" + ); + } + } +} + +/// 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::UIntType).unwrap(), + ), + // full literal principal + ( + r#"(restrict-assets? tx-sender ((with-ft 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.token "token-name" u1000)) true)"#, + 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::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::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::UIntType).unwrap(), + ), + // "*" token name + ( + r#"(restrict-assets? tx-sender ((with-ft .token "*" u1000)) true)"#, + 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::UIntType).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_ascii_type_checked(MAX_STRING_LEN as u32).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_ascii_type_checked(4).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_ascii_type_checked(MAX_STRING_LEN as u32).into(), + TypeSignature::new_ascii_type_checked(146u32).into(), + ), + ), + ]; + + for (code, expected_type) in &good { + if version < ClarityVersion::Clarity4 { + assert_eq!( + CheckErrors::UnknownFunction("restrict-assets?".to_string()), + *type_check_helper_version(code, version).unwrap_err().err, + "{code}", + ); + } else { + assert_eq!( + expected_type, + &type_check_helper_version(code, version).unwrap(), + "{code}", + ); + } + } + + for (code, expected_err) in &bad { + if version < ClarityVersion::Clarity4 { + assert_eq!( + CheckErrors::UnknownFunction("restrict-assets?".to_string()), + *type_check_helper_version(code, version).unwrap_err().err, + "{code}", + ); + } else { + assert_eq!( + expected_err, + type_check_helper_version(code, version) + .unwrap_err() + .err + .as_ref(), + "{code}", + ); + } + } +} + +/// 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" (list u1000))) true)"#, + 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::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::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::UIntType).unwrap(), + ), + // "*" token name + ( + r#"(restrict-assets? tx-sender ((with-nft .token "*" (list u1000))) true)"#, + 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::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::UIntType).unwrap(), + ), + // buffer asset-id + ( + r#"(restrict-assets? tx-sender ((with-nft .token "token-name" (list 0x0123456789))) true)"#, + 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::UIntType).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" (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" (list 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 (list u456))) true)", + CheckErrors::TypeError( + TypeSignature::new_ascii_type_checked(MAX_STRING_LEN as u32).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_ascii_type_checked(MAX_STRING_LEN as u32).into(), + TypeSignature::new_ascii_type_checked(146u32).into(), + ), + ), + // too many identifiers (more than 128) + ( + &format!( + "(restrict-assets? tx-sender ((with-nft .token \"token-name\" (list {}))) true)", + std::iter::repeat_n("u1", 130) + .collect::>() + .join(" ") + ), + CheckErrors::MaxIdentifierLengthExceeded(MAX_NFT_IDENTIFIERS, 130), + ), + ]; + + for (code, expected_type) in &good { + if version < ClarityVersion::Clarity4 { + assert_eq!( + CheckErrors::UnknownFunction("restrict-assets?".to_string()), + *type_check_helper_version(code, version).unwrap_err().err, + "{code}", + ); + } else { + assert_eq!( + expected_type, + &type_check_helper_version(code, version).unwrap(), + "{code}", + ); + } + } + + for (code, expected_err) in &bad { + if version < ClarityVersion::Clarity4 { + assert_eq!( + CheckErrors::UnknownFunction("restrict-assets?".to_string()), + *type_check_helper_version(code, version).unwrap_err().err, + "{code}", + ); + } else { + assert_eq!( + expected_err, + type_check_helper_version(code, version) + .unwrap_err() + .err + .as_ref(), + "{code}", + ); + } + } +} + +/// 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::UIntType).unwrap(), + ), + // zero amount + ( + "(restrict-assets? tx-sender ((with-stacking u0)) true)", + 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::UIntType).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_ascii_type_checked(4).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 { + if version < ClarityVersion::Clarity4 { + assert_eq!( + CheckErrors::UnknownFunction("restrict-assets?".to_string()), + *type_check_helper_version(code, version).unwrap_err().err, + "{code}", + ); + } else { + assert_eq!( + expected_type, + &type_check_helper_version(code, version).unwrap(), + "{code}", + ); + } + } + + for (code, expected_err) in &bad { + if version < ClarityVersion::Clarity4 { + assert_eq!( + CheckErrors::UnknownFunction("restrict-assets?".to_string()), + *type_check_helper_version(code, version).unwrap_err().err, + "{code}", + ); + } else { + assert_eq!( + expected_err, + type_check_helper_version(code, version) + .unwrap_err() + .err + .as_ref(), + "{code}", + ); + } + } +} + +/// 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::UIntType).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 { + if version < ClarityVersion::Clarity4 { + assert_eq!( + CheckErrors::UnknownFunction("as-contract?".to_string()), + *type_check_helper_version(code, version).unwrap_err().err, + "{code}", + ); + } else { + assert_eq!( + expected_type, + &type_check_helper_version(code, version).unwrap(), + "{code}", + ); + } + } + + for (code, expected_err) in &bad { + if version < ClarityVersion::Clarity4 { + assert_eq!( + CheckErrors::UnknownFunction("restrict-assets?".to_string()), + *type_check_helper_version(code, version).unwrap_err().err, + "{code}", + ); + } else { + assert_eq!( + expected_err, + type_check_helper_version(code, version) + .unwrap_err() + .err + .as_ref(), + "{code}", + ); + } + } +} diff --git a/clarity/src/vm/contexts.rs b/clarity/src/vm/contexts.rs index 116c631387..2120b508b3 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<()> { @@ -392,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(()) } @@ -455,6 +486,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 +502,18 @@ 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) + } + + pub fn get_stacking(&self, principal: &PrincipalData) -> Option { + self.stacking_map.get(principal).copied() + } } impl fmt::Display for AssetMap { @@ -1551,6 +1602,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, @@ -1590,6 +1647,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/clarity/src/vm/costs/cost_functions.rs b/clarity/src/vm/costs/cost_functions.rs index 6abbaec555..055232ff4e 100644 --- a/clarity/src/vm/costs/cost_functions.rs +++ b/clarity/src/vm/costs/cost_functions.rs @@ -159,6 +159,8 @@ define_named_enum!(ClarityCostFunction { BitwiseRShift("cost_bitwise_right_shift"), ContractHash("cost_contract_hash"), ToAscii("cost_to_ascii"), + RestrictAssets("cost_restrict_assets"), + AsContractSafe("cost_as_contract_safe"), Unimplemented("cost_unimplemented"), }); @@ -330,6 +332,8 @@ 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; + fn cost_as_contract_safe(n: u64) -> InterpreterResult; } impl ClarityCostFunction { @@ -484,6 +488,8 @@ 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::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 1e400f56bd..7afbe365a9 100644 --- a/clarity/src/vm/costs/costs_1.rs +++ b/clarity/src/vm/costs/costs_1.rs @@ -753,4 +753,12 @@ 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()) + } + + 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 451008bd1b..bdb4fa3e81 100644 --- a/clarity/src/vm/costs/costs_2.rs +++ b/clarity/src/vm/costs/costs_2.rs @@ -753,4 +753,12 @@ 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()) + } + + 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 647bafedb9..d942d83019 100644 --- a/clarity/src/vm/costs/costs_2_testnet.rs +++ b/clarity/src/vm/costs/costs_2_testnet.rs @@ -753,4 +753,12 @@ 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()) + } + + 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 b195303510..46ae6796ed 100644 --- a/clarity/src/vm/costs/costs_3.rs +++ b/clarity/src/vm/costs/costs_3.rs @@ -771,4 +771,12 @@ 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()) + } + + 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 d1c92732d9..aca4731eb2 100644 --- a/clarity/src/vm/costs/costs_4.rs +++ b/clarity/src/vm/costs/costs_4.rs @@ -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,14 @@ 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))) + } + + 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 8ab926834a..c258bc199b 100644 --- a/clarity/src/vm/docs/mod.rs +++ b/clarity/src/vm/docs/mod.rs @@ -139,8 +139,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 { @@ -255,7 +254,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 +262,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 +463,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 +473,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 +484,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 +495,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 +506,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 { @@ -833,7 +832,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 +1361,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 { @@ -1411,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 { @@ -1433,7 +1434,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 +1446,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 { @@ -2372,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 ", }; @@ -2388,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)) "#, }; @@ -2410,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) "# }; @@ -2429,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) "# }; @@ -2450,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) " }; @@ -2566,6 +2562,204 @@ characters.", "#, }; +const RESTRICT_ASSETS: SpecialAPI = SpecialAPI { + 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)", + 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 `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 u128) +"#, +}; + +const AS_CONTRACT_SAFE: SpecialAPI = SpecialAPI { + input_type: "((Allowance){0,128}), AnyType, ... A", + snippet: "as-contract? (${1:allowance-1} ${2:allowance-2}) ${3:expr-1}", + 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 +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 `u128` if an asset with no allowance caused the violation.", + example: r#" +(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? u50 tx-sender recipient)) + ) +) ;; Returns (err u128) +"#, +}; + +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 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 u0) +"#, +}; + +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#" +(define-fungible-token stackaroo) +(ft-mint? stackaroo u200 tx-sender) +(restrict-assets? tx-sender + ((with-ft current-contract "stackaroo" u100)) + (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 u0) +"#, +}; + +const ALLOWANCE_WITH_NFT: SpecialAPI = SpecialAPI { + 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 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 +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`. +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) +(nft-mint? stackaroo u124 tx-sender) +(nft-mint? stackaroo u125 tx-sender) +(restrict-assets? tx-sender + ((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" (list u125))) + (try! (nft-transfer? stackaroo u124 tx-sender 'SPAXYA5XS51713FDTQ8H94EJ4V579CXMTRNBZKSF)) +) ;; Returns (err u0) +"#, +}; + +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 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. 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)) + (try! (contract-call? 'SP000000000000000000002Q6VF78.pox-4 delegate-stx + u1100000000000 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM none none + )) +) ;; Returns (err u0) +(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#" +(let ((recipient tx-sender)) + (as-contract? ((with-all-assets-unsafe)) + (try! (stx-transfer? u100 tx-sender recipient)) + ) +) ;; Returns (ok true) +"#, +}; + pub fn make_api_reference(function: &NativeFunctions) -> FunctionAPI { use crate::vm::functions::NativeFunctions::*; let name = function.get_name(); @@ -2680,6 +2874,13 @@ 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), + 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), } } @@ -2790,6 +2991,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, @@ -3033,7 +3235,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![]; @@ -3060,7 +3262,7 @@ mod test { &contract_id, &whole_contract, &mut (), - ClarityVersion::latest(), + version, StacksEpochId::latest(), ) .unwrap() @@ -3072,7 +3274,7 @@ mod test { &mut analysis_db, false, &StacksEpochId::latest(), - &ClarityVersion::latest(), + &version, ) .expect("Failed to type check"); } @@ -3085,7 +3287,7 @@ mod test { &contract_id, &total_example, &mut (), - ClarityVersion::latest(), + version, StacksEpochId::latest(), ) .unwrap() @@ -3098,7 +3300,7 @@ mod test { &mut analysis_db, false, &StacksEpochId::latest(), - &ClarityVersion::latest(), + &version, ) .expect("Failed to type check"); type_results.push( @@ -3112,8 +3314,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, @@ -3186,10 +3387,14 @@ 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 - // 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( @@ -3244,6 +3449,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()); @@ -3258,6 +3464,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 @@ -3283,7 +3496,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(); @@ -3365,7 +3582,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..df58bb845c 100644 --- a/clarity/src/vm/functions/mod.rs +++ b/clarity/src/vm/functions/mod.rs @@ -76,6 +76,7 @@ mod crypto; mod database; pub mod define; mod options; +mod post_conditions; pub mod principals; mod sequences; pub mod tuples; @@ -143,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), @@ -193,6 +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), + 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), }); /// @@ -565,6 +573,20 @@ 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, + ), + 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 new file mode 100644 index 0000000000..f3fe958630 --- /dev/null +++ b/clarity/src/vm/functions/post_conditions.rs @@ -0,0 +1,564 @@ +// 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 std::collections::HashMap; + +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, 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}; + +pub struct StxAllowance { + amount: u128, +} + +pub struct FtAllowance { + asset: AssetIdentifier, + amount: u128, +} + +pub struct NftAllowance { + asset: AssetIdentifier, + asset_ids: Vec, +} + +pub struct StackingAllowance { + amount: u128, +} + +pub enum Allowance { + Stx(StxAllowance), + Ft(FtAllowance), + Nft(NftAllowance), + Stacking(StackingAllowance), + 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, + context: &LocalContext, +) -> InterpreterResult { + 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)?; + 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()); + } + let amount = eval(&rest[0], env, context)?; + let amount = amount.expect_u128()?; + Ok(Allowance::Stx(StxAllowance { amount })) + } + NativeFunctions::AllowanceWithFt => { + 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 })) + } + NativeFunctions::AllowanceWithNft => { + 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_list = eval(&rest[2], env, context)?; + let asset_ids = asset_id_list.expect_list()?; + + Ok(Allowance::Nft(NftAllowance { asset, asset_ids })) + } + NativeFunctions::AllowanceWithStacking => { + 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 })) + } + NativeFunctions::AllowanceAll => { + 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?` +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::ExpectedListOfAllowances( + "restrict-assets?".into(), + 2, + ))?; + let body_exprs = &args[2..]; + + let asset_owner = eval(asset_owner_expr, env, context)?; + let asset_owner = asset_owner.expect_principal()?; + + runtime_cost( + ClarityCostFunction::RestrictAssets, + env, + 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)?); + } + + // Create a new evaluation context, so that we can rollback if the + // post-conditions are violated + env.global_context.begin(); + + // 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()?; + + // 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) { + 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()?; + + // 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?` +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 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; + + 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 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()?; + + // 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) { + Ok(None) => {} + Ok(Some(violation_index)) => { + nested_env.global_context.roll_back()?; + return Value::error(Value::UInt(violation_index)); + } + Err(e) => { + nested_env.global_context.roll_back()?; + return Err(e); + } + } + + nested_env.global_context.commit()?; + + // 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) + } + } + }) +} + +/// 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 128 if an asset with no allowance caused the violation. If all +/// allowances are satisfied, return `Ok(None)`. +fn check_allowances( + owner: &PrincipalData, + 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> = 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.into_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 (_, 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)); + } + } + } + + // 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(MAX_ALLOWANCES as u128)); + } + + // Check against the STX allowances + for (index, allowance) in &stx_allowances { + if stx_moved > *allowance { + return Ok(Some(u128::try_from(*index).map_err(|_| { + InterpreterError::Expect("failed to convert index to u128".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(MAX_ALLOWANCES as u128)); + } + + // Check against the STX allowances + for (index, allowance) in &stx_allowances { + if stx_burned > *allowance { + return Ok(Some(u128::try_from(*index).map_err(|_| { + InterpreterError::Expect("failed to convert index to u128".into()) + })?)); + } + } + } + + // Check FT movements + 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) { + 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(MAX_ALLOWANCES as u128)); + } + + // 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(u128::try_from(index).map_err(|_| { + InterpreterError::Expect("failed to convert index to u128".into()) + })?)); + } + } + } + } + + // 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, &Vec)> = Vec::new(); + if let Some((index, allowance_vec)) = nft_allowances.get(asset) { + merged.push((*index, allowance_vec)); + } + + if let Some((index, allowance_vec)) = nft_allowances.get(&AssetIdentifier { + contract_identifier: asset.contract_identifier.clone(), + asset_name: "*".into(), + }) { + merged.push((*index, allowance_vec)); + } + + if merged.is_empty() { + // No allowance for this asset, any movement is a violation + return Ok(Some(MAX_ALLOWANCES as u128)); + } + + // Sort by allowance index so we check allowances in order + merged.sort_by_key(|(idx, _)| *idx); + + for (index, allowance_vec) in merged { + // Check against the NFT allowances + for id_moved in ids_moved { + if !allowance_vec.contains(id_moved) { + return Ok(Some(u128::try_from(index).map_err(|_| { + InterpreterError::Expect("failed to convert index to u128".into()) + })?)); + } + } + } + } + } + + // 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(MAX_ALLOWANCES as u128)); + } + + // Check against the stacking allowances + for (index, allowance) in &stacking_allowances { + if stx_stacked > *allowance { + return Ok(Some(u128::try_from(*index).map_err(|_| { + InterpreterError::Expect("failed to convert index to u128".into()) + })?)); + } + } + } + + Ok(None) +} + +/// 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/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..9b18f8b062 --- /dev/null +++ b/clarity/src/vm/tests/post_conditions.rs @@ -0,0 +1,1406 @@ +// 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 . + +//! 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::{Error as ClarityError, InterpreterResult, ShortReturnType}; +use clarity_types::types::{PrincipalData, QualifiedContractIdentifier, StandardPrincipalData}; +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}; + +fn execute(snippet: &str) -> InterpreterResult> { + execute_with_parameters_and_call_in_global_context( + snippet, + ClarityVersion::Clarity4, + StacksEpochId::Epoch33, + 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::UInt(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::UInt(MAX_ALLOWANCES as u128)).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" (list 123))) + (try! (stx-transfer? u50 tx-sender recipient)) + ) +)"#; + let expected = Value::error(Value::UInt(MAX_ALLOWANCES as u128)).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::UInt(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::UInt(MAX_ALLOWANCES as u128)).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" (list 123))) + (try! (stx-burn? u50 tx-sender)) +)"#; + let expected = Value::error(Value::UInt(MAX_ALLOWANCES as u128)).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::UInt(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::UInt(1)).unwrap(); + 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::UInt(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::UInt(MAX_ALLOWANCES as u128)).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::UInt(MAX_ALLOWANCES as u128)).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::UInt(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::UInt(1)).unwrap(); + 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::UInt(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::UInt(MAX_ALLOWANCES as u128)).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::UInt(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::UInt(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::UInt(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::UInt(0)).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::UInt(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::UInt(MAX_ALLOWANCES as u128)).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::UInt(MAX_ALLOWANCES as u128)).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::UInt(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::UInt(0)).unwrap(); + 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#" +(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::UInt(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::UInt(MAX_ALLOWANCES as u128)).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::UInt(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::UInt(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::UInt(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::UInt(0)).unwrap(); + 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] +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::UInt(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::UInt(MAX_ALLOWANCES as u128)).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" (list 123))) + (try! (stx-transfer? u50 tx-sender 'SP000000000000000000002Q6VF78)) +)"#; + let expected = Value::error(Value::UInt(MAX_ALLOWANCES as u128)).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::UInt(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::UInt(MAX_ALLOWANCES as u128)).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" (list 123))) + (try! (stx-burn? u50 tx-sender)) +)"#; + let expected = Value::error(Value::UInt(MAX_ALLOWANCES as u128)).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::UInt(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::UInt(1)).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::UInt(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::UInt(MAX_ALLOWANCES as u128)).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::UInt(MAX_ALLOWANCES as u128)).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::UInt(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::UInt(1)).unwrap(); + 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::UInt(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::UInt(MAX_ALLOWANCES as u128)).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::UInt(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::UInt(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::UInt(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::UInt(0)).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::UInt(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::UInt(MAX_ALLOWANCES as u128)).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::UInt(MAX_ALLOWANCES as u128)).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::UInt(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::UInt(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::UInt(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::UInt(MAX_ALLOWANCES as u128)).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::UInt(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::UInt(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::UInt(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::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/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 { diff --git a/stacks-node/src/tests/nakamoto_integrations.rs b/stacks-node/src/tests/nakamoto_integrations.rs index 6450bead84..c903098da6 100644 --- a/stacks-node/src/tests/nakamoto_integrations.rs +++ b/stacks-node/src/tests/nakamoto_integrations.rs @@ -15331,3 +15331,2460 @@ fn check_block_time_keyword() { run_loop_thread.join().unwrap(); } + +#[test] +#[ignore] +/// 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; + } + + 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)) + (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)) + (unwrap! (contract-call? 'ST000000000000000000002AMW42H.pox-4 delegate-stx + amount 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM none none + ) (err u1)) + ) +) +(define-public (delegate-stx-no-allowance (amount uint)) + (as-contract? () + (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)) + (unwrap! (contract-call? 'ST000000000000000000002AMW42H.pox-4 delegate-stx + amount 'ST1PQHQKV0RJXZFY1DGX8MNSNYVE3VGZJSRTPGZGM none none + ) (err u1)) + ) +) +(define-public (revoke-delegate-stx) + (as-contract? () + (unwrap! (contract-call? 'ST000000000000000000002AMW42H.pox-4 revoke-delegate-stx) (err u1)) + 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::UInt(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::UInt(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::UInt(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::UInt(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::UInt(128)).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(); +} + +#[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 +/// `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 = 4000; + 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 u300) + )) + ) +) +(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 (+ 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)) + )) + (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-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-catch-err + (recipient principal) + (amount-1 uint) + (amount-2 uint) + (allowed uint) + ) + (begin + (unwrap-err! (restrict-assets? tx-sender ((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 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" + ); + + 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, + &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 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: 2 transfers within allowance"); + sender_balance = new_sender_balance; + recipient_balance = new_recipient_balance; + 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" + ); + + info!("Test: 2 transfers that exceed allowance"); + sender_balance = new_sender_balance; + recipient_balance = new_recipient_balance; + 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" + ); + + 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() + .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 +/// `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/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 715bee4966..f83afdcdf4 100644 --- a/stackslib/src/chainstate/stacks/boot/costs-4.clar +++ b/stackslib/src/chainstate/stacks/boot/costs-4.clar @@ -663,3 +663,9 @@ (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 + +(define-read-only (cost_as_contract_safe (n uint)) + (runtime (linear n u1 u100))) ;; TODO: needs criterion benchmark diff --git a/stackslib/src/chainstate/tests/consensus.rs b/stackslib/src/chainstate/tests/consensus.rs index 2526ceaf1a..3ef7b3c415 100644 --- a/stackslib/src/chainstate/tests/consensus.rs +++ b/stackslib/src/chainstate/tests/consensus.rs @@ -733,7 +733,7 @@ fn test_append_block_with_contract_upload_success() { ), )), Success(ExpectedBlockOutput( - marf_hash: "25eff57753c490824fc0205b4493d7073e378f0d4648810454cc7e06276fe7da", + marf_hash: "c72ff94259d531c853a2b3a5ae3d8a8d5a87014337451a09cbce09fa6c43e228", transactions: [ ExpectedTransactionOutput( return_type: Response(ResponseData( 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 2c231d7026..ee4c1950d5 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: "eabaa1042075ab7afd7721584a590ee8f8542ad4743adc41ed3b1dbe9078a5b4", + marf_hash: "c2e7ab1e81196330a7c375ee676efd05e1c162b5667ec2d5fd2e072ebebc8fd5", 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_stx_transfers_success.snap b/stackslib/src/chainstate/tests/snapshots/blockstack_lib__chainstate__tests__consensus__append_stx_transfers_success.snap index ef8280179a..b97b796a0d 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: "39a1ec92bc388262902593e82da7af6e0cc12412bd566974cebb7f7e9f4e67ce", + marf_hash: "88bbf16581aeb1b8d38b0cff4ca1f852ce264744b1e62eb7c1a88167ce97f87b", transactions: [ ExpectedTransactionOutput( return_type: Response(ResponseData( 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 75be448599..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)", @@ -165,7 +165,13 @@ 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))", + 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( @@ -1035,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)); + } } } }) @@ -1077,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)); + } } } }) @@ -1114,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)); + } } } }) @@ -1148,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)); + } } } }) diff --git a/stackslib/src/config/mod.rs b/stackslib/src/config/mod.rs index b7d1919752..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_standard_principal(&address) - .unwrap() - .into(), + address: PrincipalData::parse(&address).unwrap().into(), amount, }; self.initial_balances.push(new_balance);