// Copyright (C) 2025-2026 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::ClarityName;
use clarity_types::types::{AssetIdentifier, PrincipalData, StandardPrincipalData};
use stacks_common::types::StacksEpochId;
use crate::vm::analysis::type_checker::v2_1::natives::post_conditions::MAX_ALLOWANCES;
use crate::vm::contexts::{AssetMap, ExecutionState, InvocationContext};
use crate::vm::costs::cost_functions::ClarityCostFunction;
use crate::vm::costs::{
CostErrors, CostTracker, MemoryConsumer, constants as cost_constants, runtime_cost,
};
use crate::vm::errors::{
RuntimeCheckErrorKind, RuntimeError, VmExecutionError, VmInternalError,
check_arguments_at_least,
};
use crate::vm::functions::NativeFunctions;
use crate::vm::representations::SymbolicExpression;
use crate::vm::types::Value;
use crate::vm::{LocalContext, eval};
#[derive(Debug)]
pub struct StxAllowance {
amount: u128,
}
#[derive(Debug)]
pub struct FtAllowance {
asset: AssetIdentifier,
amount: u128,
}
#[derive(Debug)]
pub struct NftAllowance {
asset: AssetIdentifier,
asset_ids: Vec,
}
#[derive(Debug)]
pub struct StackingAllowance {
amount: u128,
}
#[derive(Debug)]
pub enum Allowance {
/// Permits the asset owner to move or burn up to the specified amount of
/// STX within the protected scope.
Stx(StxAllowance),
/// Permits the asset owner to move or burn up to the specified amount of a
/// fungible token within the protected scope.
Ft(FtAllowance),
/// Permits the asset owner to transfer or burn specific non-fungible
/// tokens within the protected scope.
Nft(NftAllowance),
/// Permits the asset owner to stake up to the specified amount of STX
/// within the protected scope.
Stacking(StackingAllowance),
/// Permits the asset owner to perform a position-altering PoX action
/// (`unstake`, `unstake-sbtc`, `update-bond-registration`,
/// `announce-l1-early-exit`) within the protected scope.
Pox,
/// Permits the asset owner to access all assets within the protected
/// scope.
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| {
VmInternalError::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::Pox => Ok(0),
Allowance::All => Ok(0),
}
}
}
fn eval_allowance(
allowance_expr: &SymbolicExpression,
exec_state: &mut ExecutionState,
invoke_ctx: &InvocationContext,
context: &LocalContext,
) -> Result {
let list = allowance_expr
.match_list()
.ok_or(RuntimeCheckErrorKind::Unreachable(
"Non functional application".to_string(),
))?;
let (name_expr, rest) = list
.split_first()
.ok_or(RuntimeCheckErrorKind::Unreachable(
"Non functional application".to_string(),
))?;
let name = name_expr
.match_atom()
.ok_or(RuntimeCheckErrorKind::Unreachable(
"Bad function name".to_string(),
))?;
let Some(ref native_function) = NativeFunctions::lookup_by_name_at_version(
name,
invoke_ctx.contract_context.get_clarity_version(),
) else {
return Err(
RuntimeCheckErrorKind::Unreachable(format!("Expected allowance expr: {name}")).into(),
);
};
match native_function {
NativeFunctions::AllowanceWithStx => {
if rest.len() != 1 {
return Err(RuntimeCheckErrorKind::IncorrectArgumentCount(1, rest.len()).into());
}
let amount = eval(&rest[0], exec_state, invoke_ctx, context)?;
let amount = match amount.as_ref() {
Value::UInt(amount) => *amount,
_ => {
return Err(VmInternalError::Expect("Expected u128".into()).into());
}
};
Ok(Allowance::Stx(StxAllowance { amount }))
}
NativeFunctions::AllowanceWithFt => {
if rest.len() != 3 {
return Err(RuntimeCheckErrorKind::IncorrectArgumentCount(3, rest.len()).into());
}
let contract_value =
eval(&rest[0], exec_state, invoke_ctx, context)?.clone_with_cost(exec_state)?;
let contract = contract_value
.clone()
.expect_principal()
.map_err(|_| VmInternalError::Expect("Expected principal".into()))?;
let contract_identifier = match contract {
PrincipalData::Standard(_) => {
return Err(RuntimeCheckErrorKind::ExpectedContractPrincipalValue(
contract_value.to_error_string(),
)
.into());
}
PrincipalData::Contract(c) => c,
};
let asset_name =
eval(&rest[1], exec_state, invoke_ctx, context)?.clone_with_cost(exec_state)?;
let asset_name = asset_name
.expect_string_ascii()
.map_err(|_| VmInternalError::Expect("Expected ASCII String.".into()))?;
let asset_name = match ClarityName::try_from(asset_name) {
Ok(name) => name,
Err(_) => {
return Err(RuntimeError::BadTokenName(rest[1].to_string()).into());
}
};
let asset = AssetIdentifier {
contract_identifier,
asset_name,
};
let amount =
eval(&rest[2], exec_state, invoke_ctx, context)?.clone_with_cost(exec_state)?;
let amount = amount
.expect_u128()
.map_err(|_| VmInternalError::Expect("Expected u128".into()))?;
Ok(Allowance::Ft(FtAllowance { asset, amount }))
}
NativeFunctions::AllowanceWithNft => {
if rest.len() != 3 {
return Err(RuntimeCheckErrorKind::IncorrectArgumentCount(3, rest.len()).into());
}
let contract_value =
eval(&rest[0], exec_state, invoke_ctx, context)?.clone_with_cost(exec_state)?;
let contract = contract_value
.clone()
.expect_principal()
.map_err(|_| VmInternalError::Expect("Expected principal".into()))?;
let contract_identifier = match contract {
PrincipalData::Standard(_) => {
return Err(RuntimeCheckErrorKind::ExpectedContractPrincipalValue(
contract_value.to_error_string(),
)
.into());
}
PrincipalData::Contract(c) => c,
};
let asset_name =
eval(&rest[1], exec_state, invoke_ctx, context)?.clone_with_cost(exec_state)?;
let asset_name = asset_name
.expect_string_ascii()
.map_err(|_| VmInternalError::Expect("Expected ASCII String.".into()))?;
let asset_name = match ClarityName::try_from(asset_name) {
Ok(name) => name,
Err(_) => {
return Err(RuntimeError::BadTokenName(rest[1].to_string()).into());
}
};
let asset = AssetIdentifier {
contract_identifier,
asset_name,
};
let asset_id_list =
eval(&rest[2], exec_state, invoke_ctx, context)?.clone_with_cost(exec_state)?;
let asset_ids = asset_id_list
.expect_list()
.map_err(|_| VmInternalError::Expect("Expected list".into()))?;
Ok(Allowance::Nft(NftAllowance { asset, asset_ids }))
}
// `with-stacking` (Clarity 4-5) and `with-staking` (Clarity 6+) are the
// same allowance under two spellings; the version gating is handled by
// `lookup_by_name_at_version`.
NativeFunctions::AllowanceWithStacking | NativeFunctions::AllowanceWithStaking => {
if rest.len() != 1 {
return Err(RuntimeCheckErrorKind::IncorrectArgumentCount(1, rest.len()).into());
}
let amount =
eval(&rest[0], exec_state, invoke_ctx, context)?.clone_with_cost(exec_state)?;
let amount = amount
.expect_u128()
.map_err(|_| VmInternalError::Expect("Expected u128".into()))?;
Ok(Allowance::Stacking(StackingAllowance { amount }))
}
NativeFunctions::AllowanceWithPox => {
if !rest.is_empty() {
return Err(RuntimeCheckErrorKind::IncorrectArgumentCount(0, rest.len()).into());
}
Ok(Allowance::Pox)
}
NativeFunctions::AllowanceAll => {
if !rest.is_empty() {
return Err(RuntimeCheckErrorKind::IncorrectArgumentCount(1, rest.len()).into());
}
Ok(Allowance::All)
}
_ => Err(
RuntimeCheckErrorKind::Unreachable(format!("Expected allowance expr: {name}")).into(),
),
}
}
/// Handles the function `restrict-assets?`
pub fn special_restrict_assets(
args: &[SymbolicExpression],
exec_state: &mut ExecutionState,
invoke_ctx: &InvocationContext,
context: &LocalContext,
) -> Result {
// (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(RuntimeCheckErrorKind::Unreachable(
"Expected list of allowances: for restrict-assets? as argument 2".to_string(),
))?;
let body_exprs = &args[2..];
let asset_owner =
eval(asset_owner_expr, exec_state, invoke_ctx, context)?.clone_with_cost(exec_state)?;
let asset_owner = asset_owner
.expect_principal()
.map_err(|_| VmInternalError::Expect("Expected principal".into()))?;
let allowance_len = allowance_list.len();
runtime_cost(
ClarityCostFunction::RestrictAssets,
exec_state,
allowance_len,
)?;
if allowance_len > MAX_ALLOWANCES {
return Err(RuntimeCheckErrorKind::Unreachable(format!(
"Too many allowances: got {allowance_len}, allowed {MAX_ALLOWANCES}"
))
.into());
}
let starting_memory = exec_state.global_context.cost_track.get_memory();
let mut memory_use: u64 = 0;
finally_drop_memory!( exec_state, memory_use; {
let mut allowances = Vec::with_capacity(allowance_len);
for allowance in allowance_list {
let allowance = eval_allowance(allowance, exec_state, invoke_ctx, context)?;
let allowance_memory = u64::try_from(allowance.size_in_bytes()?)
.map_err(|_| VmInternalError::Expect("Allowance size too large".into()))?;
match exec_state.add_memory(allowance_memory) {
Ok(()) => {
memory_use = memory_use.checked_add(allowance_memory).ok_or_else(|| {
VmInternalError::Expect(
"restrict-assets allowance memory overflowed".into(),
)
})?;
}
Err(CostErrors::MemoryBalanceExceeded(used, limit)) => {
memory_use = used.checked_sub(starting_memory).ok_or_else(|| {
VmInternalError::Expect(
"restrict-assets allowance memory cleanup underflowed".into(),
)
})?;
return Err(
RuntimeCheckErrorKind::RestrictAssetsMemoryExceeded(used, limit).into(),
);
}
Err(e) => return Err(e.into()),
}
allowances.push(allowance);
}
evaluate_body_with_allowance_check(
&asset_owner,
allowances,
body_exprs,
invoke_ctx,
exec_state,
context,
)
})
}
/// Evaluate the body of a post-condition scope inside a rollback sub-context, then enforce
/// the accumulated `allowances` against the resulting asset map.
///
/// On allowance violation the sub-context is rolled back and the caller gets back
/// `(err )`. On an error from `check_allowances` the sub-context is rolled
/// back and the error is propagated. Otherwise the sub-context is committed and the body
/// result is wrapped in `(ok ...)`.
fn evaluate_body_with_allowance_check(
asset_owner: &PrincipalData,
allowances: Vec,
body_exprs: &[SymbolicExpression],
invoke_ctx: &InvocationContext,
exec_state: &mut ExecutionState,
context: &LocalContext,
) -> Result {
let epoch = *exec_state.epoch();
exec_state.global_context.begin();
// Evaluate the body expressions inside a closure so `?` only exits the closure
let eval_result: Result