Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
35534ab
feat: initial implementation for `restric-assets?`
obycode Sep 15, 2025
9672363
feat: add syntax and type-checking for `as-contract?` and allowances
obycode Sep 18, 2025
e4fc430
chore: fix formatting
obycode Sep 19, 2025
e29ec16
fix: add Clarity4 version of test contract
obycode Sep 19, 2025
196e71e
feat: initial implementation of allowances
obycode Sep 20, 2025
0a141ae
fix: remove ASTRules
obycode Sep 21, 2025
7e8698f
fix: error in docs example
obycode Sep 21, 2025
204e848
fix: copy/paste error
obycode Sep 22, 2025
771262c
feat: update `with-nft` to support list of identifiers
obycode Sep 22, 2025
b450cd1
test: add tests for ft and nft post-conditions
obycode Sep 22, 2025
11c4b14
feat: add checks for max number of allowances and max NFT identifiers
obycode Sep 22, 2025
1afa168
fix: update test and fix clippy issues
obycode Sep 22, 2025
156724d
test: ignore `with-stacking` in doc examples tests
obycode Sep 22, 2025
df2b581
fix: typo in tests
obycode Sep 22, 2025
b20511e
feat: implement tracking of stacking for post-conditions
obycode Sep 22, 2025
b396d86
feat: add support for "*" wildcard in allowances
obycode Sep 23, 2025
dd13789
fix: add stacking allowance check
obycode Sep 24, 2025
103027c
test: add integration test for `with-stacking` allowances
obycode Sep 25, 2025
c4fb73d
refactor: return `uint` error from post-conditions
obycode Sep 25, 2025
22449dd
fix: handle errors in `restrict-assets?` and `as-contract?` body
obycode Sep 25, 2025
0060129
chore: fix clippy and formatting
obycode Sep 25, 2025
489793b
test: begin adding rollback integration tests
obycode Sep 27, 2025
ce562a3
test: additional rollback tests
obycode Sep 27, 2025
7806db3
docs: update doc example
obycode Sep 30, 2025
610c421
refactor: use non-panicking array access
obycode Sep 30, 2025
43b17c5
test: add `cheeck_as_contract_rollback` integration test
obycode Sep 30, 2025
a65dda4
test: add integration tests for `stack-stx` allowances
obycode Oct 1, 2025
4799797
Merge branch 'develop' into feat/restrict-assets
obycode Oct 1, 2025
c824cde
chore: remove todo for event on allowance violation
obycode Oct 1, 2025
a6afc03
chore: formatting
obycode Oct 1, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ and this project adheres to the versioning scheme outlined in the [README.md](RE
- `current-contract`
- `block-time`
- `to-ascii?`
- `restrict-assets?`
- `as-contract?`
- Special allowance expressions:
- `with-stx`
- `with-ft`
- `with-nft`
- `with-stacking`
- `with-all-assets-unsafe`
- Added `contract_cost_limit_percentage` to the miner config file — sets the percentage of a block’s execution cost at which, if a large non-boot contract call would cause a BlockTooBigError, the miner will stop adding further non-boot contract calls and only include STX transfers and boot contract calls for the remainder of the block.

### Changed
Expand Down
18 changes: 18 additions & 0 deletions clarity-types/src/errors/analysis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down Expand Up @@ -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}"),
}
}

Expand Down
1 change: 1 addition & 0 deletions clarity-types/src/errors/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ pub enum RuntimeErrorType {
PoxAlreadyLocked,

BlockTimeNotAvailable,
Unreachable,
}

#[derive(Debug, PartialEq)]
Expand Down
10 changes: 10 additions & 0 deletions clarity-types/src/types/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1235,6 +1235,16 @@ impl Value {
Err(InterpreterError::Expect("Expected response".into()).into())
}
}

pub fn expect_string_ascii(self) -> Result<String> {
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 {
Expand Down
4 changes: 3 additions & 1 deletion clarity-types/src/types/signatures.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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)
}
Expand Down
28 changes: 25 additions & 3 deletions clarity/src/vm/analysis/arithmetic_checker/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand Down
148 changes: 134 additions & 14 deletions clarity/src/vm/analysis/read_only_checker/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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)
}
}
}

Expand Down
43 changes: 37 additions & 6 deletions clarity/src/vm/analysis/type_checker/v2_05/natives/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
));
Expand Down
6 changes: 4 additions & 2 deletions clarity/src/vm/analysis/type_checker/v2_1/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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],
Expand Down
10 changes: 10 additions & 0 deletions clarity/src/vm/analysis/type_checker/v2_1/natives/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ mod assets;
mod conversions;
mod maps;
mod options;
pub(crate) mod post_conditions;
mod sequences;

#[allow(clippy::large_enum_variant)]
Expand Down Expand Up @@ -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)
Expand Down
Loading
Loading