@@ -286,37 +286,52 @@ impl AssetMap {
286286 }
287287 }
288288
289- // This will get the next amount for a (principal, stx) entry in the stx table.
289+ /// This will get the next amount for a (principal, stx) entry in the stx table.
290290 fn get_next_stx_amount (
291291 & self ,
292292 principal : & PrincipalData ,
293293 amount : u128 ,
294294 ) -> Result < u128 , VmExecutionError > {
295+ // `ArithmeticOverflow` in this function is **unreachable** in normal Clarity execution because:
296+ // - Every `stx-transfer?` or `stx-burn?` is validated against the sender’s
297+ // **unlocked balance** before being queued in `AssetMap`.
298+ // - The unlocked balance is a subset of `stx-liquid-supply`.
299+ // - All balance updates in Clarity use the `+` operator **before** logging to `AssetMap`.
300+ // - `+` performs `checked_add` and returns `RuntimeError::ArithmeticOverflow` **first**.
295301 let current_amount = self . stx_map . get ( principal) . unwrap_or ( & 0 ) ;
296302 current_amount
297303 . checked_add ( amount)
298304 . ok_or ( RuntimeError :: ArithmeticOverflow . into ( ) )
299305 }
300306
301- // This will get the next amount for a (principal, stx) entry in the burn table.
307+ /// This will get the next amount for a (principal, stx) entry in the burn table.
302308 fn get_next_stx_burn_amount (
303309 & self ,
304310 principal : & PrincipalData ,
305311 amount : u128 ,
306312 ) -> Result < u128 , VmExecutionError > {
313+ // `ArithmeticOverflow` in this function is **unreachable** in normal Clarity execution because:
314+ // - Every `stx-burn?` is validated against the sender’s **unlocked balance** first.
315+ // - Unlocked balance is a subset of `stx-liquid-supply`, which is <= `u128::MAX`.
316+ // - All balance updates in Clarity use the `+` operator **before** logging to `AssetMap`.
317+ // - `+` performs `checked_add` and returns `RuntimeError::ArithmeticOverflow` **first**.
307318 let current_amount = self . burn_map . get ( principal) . unwrap_or ( & 0 ) ;
308319 current_amount
309320 . checked_add ( amount)
310321 . ok_or ( RuntimeError :: ArithmeticOverflow . into ( ) )
311322 }
312323
313- // This will get the next amount for a (principal, asset) entry in the asset table.
324+ /// This will get the next amount for a (principal, asset) entry in the asset table.
314325 fn get_next_amount (
315326 & self ,
316327 principal : & PrincipalData ,
317328 asset : & AssetIdentifier ,
318329 amount : u128 ,
319330 ) -> Result < u128 , VmExecutionError > {
331+ // `ArithmeticOverflow` in this function is **unreachable** in normal Clarity execution because:
332+ // - The inner transaction must have **partially succeeded** to log any assets.
333+ // - All balance updates in Clarity use the `+` operator **before** logging to `AssetMap`.
334+ // - `+` performs `checked_add` and returns `RuntimeError::ArithmeticOverflow` **first**.
320335 let current_amount = self
321336 . token_map
322337 . get ( principal)
@@ -1011,6 +1026,13 @@ impl<'a, 'b, 'hooks> Environment<'a, 'b, 'hooks> {
10111026 . expressions ;
10121027
10131028 if parsed. is_empty ( ) {
1029+ // `TypeParseFailure` is **unreachable** in standard Clarity VM execution.
1030+ // - `eval_read_only` parses a raw program string into an AST.
1031+ // - Any empty or invalid program would be rejected at publish/deploy time or earlier parsing stages.
1032+ // - Therefore, `parsed.is_empty()` cannot occur for programs originating from a valid contract
1033+ // or transaction.
1034+ // - Only malformed input fed directly to this internal method (e.g., in unit tests or
1035+ // artificial VM invocations) can trigger this error.
10141036 return Err ( RuntimeError :: TypeParseFailure (
10151037 "Expected a program of at least length 1" . to_string ( ) ,
10161038 )
@@ -1060,6 +1082,12 @@ impl<'a, 'b, 'hooks> Environment<'a, 'b, 'hooks> {
10601082 . expressions ;
10611083
10621084 if parsed. is_empty ( ) {
1085+ // `TypeParseFailure` is **unreachable** in standard Clarity VM execution.
1086+ // - `eval_raw` parses a raw program string into an AST.
1087+ // - All programs deployed or called via the standard VM go through static parsing and validation first.
1088+ // - Any empty or invalid program would be rejected at publish/deploy time or earlier parsing stages.
1089+ // - Therefore, `parsed.is_empty()` cannot occur for a program that originates from a valid Clarity contract or transaction.
1090+ // Only malformed input directly fed to this internal method (e.g., in unit tests) can trigger this error.
10631091 return Err ( RuntimeError :: TypeParseFailure (
10641092 "Expected a program of at least length 1" . to_string ( ) ,
10651093 )
@@ -1940,6 +1968,14 @@ impl<'a> LocalContext<'a> {
19401968
19411969 pub fn extend ( & ' a self ) -> Result < LocalContext < ' a > , VmExecutionError > {
19421970 if self . depth >= MAX_CONTEXT_DEPTH {
1971+ // `MaxContextDepthReached` in this function is **unreachable** in normal Clarity execution because:
1972+ // - Every function call in Clarity increments both the call stack depth and the local context depth.
1973+ // - The VM enforces `MAX_CALL_STACK_DEPTH` (currently 64) **before** `MAX_CONTEXT_DEPTH` (256).
1974+ // - This means no contract can create more than 64 nested function calls, preventing context depth from reaching 256.
1975+ // - Nested expressions (`let`, `begin`, `if`, etc.) increment context depth, but the Clarity parser enforces
1976+ // `ExpressionStackDepthTooDeep` long before MAX_CONTEXT_DEPTH nested contexts can be written.
1977+ // - As a result, `MaxContextDepthReached` can only occur in artificial Rust-level tests calling `LocalContext::extend()`,
1978+ // not in deployed contract execution.
19431979 Err ( RuntimeError :: MaxContextDepthReached . into ( ) )
19441980 } else {
19451981 Ok ( LocalContext {
@@ -2292,4 +2328,108 @@ mod test {
22922328 TypeSignature :: CallableType ( CallableSubtype :: Trait ( trait_id) )
22932329 ) ;
22942330 }
2331+
2332+ #[ test]
2333+ fn asset_map_arithmetic_overflows ( ) {
2334+ let a_contract_id = QualifiedContractIdentifier :: local ( "a" ) . unwrap ( ) ;
2335+ let b_contract_id = QualifiedContractIdentifier :: local ( "b" ) . unwrap ( ) ;
2336+ let p1 = PrincipalData :: Contract ( a_contract_id. clone ( ) ) ;
2337+ let p2 = PrincipalData :: Contract ( b_contract_id. clone ( ) ) ;
2338+ let t1 = AssetIdentifier {
2339+ contract_identifier : a_contract_id,
2340+ asset_name : "a" . into ( ) ,
2341+ } ;
2342+
2343+ let mut am1 = AssetMap :: new ( ) ;
2344+ let mut am2 = AssetMap :: new ( ) ;
2345+
2346+ // Token transfer: add u128::MAX followed by 1 to overflow
2347+ am1. add_token_transfer ( & p1, t1. clone ( ) , u128:: MAX ) . unwrap ( ) ;
2348+ assert ! ( matches!(
2349+ am1. add_token_transfer( & p1, t1. clone( ) , 1 ) . unwrap_err( ) ,
2350+ VmExecutionError :: Runtime ( RuntimeError :: ArithmeticOverflow , _)
2351+ ) ) ;
2352+
2353+ // STX burn: add u128::MAX followed by 1 to overflow
2354+ am1. add_stx_burn ( & p1, u128:: MAX ) . unwrap ( ) ;
2355+ assert ! ( matches!(
2356+ am1. add_stx_burn( & p1, 1 ) . unwrap_err( ) ,
2357+ VmExecutionError :: Runtime ( RuntimeError :: ArithmeticOverflow , _)
2358+ ) ) ;
2359+
2360+ // STX transfer: add u128::MAX followed by 1 to overflow
2361+ am1. add_stx_transfer ( & p1, u128:: MAX ) . unwrap ( ) ;
2362+ assert ! ( matches!(
2363+ am1. add_stx_transfer( & p1, 1 ) . unwrap_err( ) ,
2364+ VmExecutionError :: Runtime ( RuntimeError :: ArithmeticOverflow , _)
2365+ ) ) ;
2366+
2367+ // commit_other: merge two maps where sum exceeds u128::MAX
2368+ am2. add_token_transfer ( & p1, t1. clone ( ) , u128:: MAX ) . unwrap ( ) ;
2369+ assert ! ( matches!(
2370+ am1. commit_other( am2) . unwrap_err( ) ,
2371+ VmExecutionError :: Runtime ( RuntimeError :: ArithmeticOverflow , _)
2372+ ) ) ;
2373+ }
2374+
2375+ #[ test]
2376+ fn eval_raw_empty_program ( ) {
2377+ // Setup environment
2378+ let mut tl_env_factory = tl_env_factory ( ) ;
2379+ let mut env = tl_env_factory. get_env ( StacksEpochId :: latest ( ) ) ;
2380+
2381+ // Call eval_read_only with an empty program
2382+ let program = "" ; // empty program triggers parsed.is_empty()
2383+ let err = env. eval_raw ( program) . unwrap_err ( ) ;
2384+
2385+ assert ! (
2386+ matches!(
2387+ err,
2388+ VmExecutionError :: Runtime ( RuntimeError :: TypeParseFailure ( msg) , _) if msg. contains( "Expected a program of at least length 1" ) ) ,
2389+ "Expected a type parse failure"
2390+ ) ;
2391+ }
2392+
2393+ #[ test]
2394+ fn eval_read_only_empty_program ( ) {
2395+ // Setup environment
2396+ let mut tl_env_factory = tl_env_factory ( ) ;
2397+ let mut env = tl_env_factory. get_env ( StacksEpochId :: latest ( ) ) ;
2398+
2399+ // Construct a dummy contract context
2400+ let contract_id = QualifiedContractIdentifier :: local ( "dummy-contract" ) . unwrap ( ) ;
2401+
2402+ // Call eval_read_only with an empty program
2403+ let program = "" ; // empty program triggers parsed.is_empty()
2404+ let err = env. eval_read_only ( & contract_id, program) . unwrap_err ( ) ;
2405+
2406+ assert ! (
2407+ matches!(
2408+ err,
2409+ VmExecutionError :: Runtime ( RuntimeError :: TypeParseFailure ( msg) , _) if msg. contains( "Expected a program of at least length 1" ) ) ,
2410+ "Expected a type parse failure"
2411+ ) ;
2412+ }
2413+
2414+ #[ test]
2415+ fn max_context_depth_exceeded ( ) {
2416+ let root = LocalContext {
2417+ function_context : None ,
2418+ parent : None ,
2419+ callable_contracts : HashMap :: new ( ) ,
2420+ variables : HashMap :: new ( ) ,
2421+ depth : MAX_CONTEXT_DEPTH - 1 ,
2422+ } ;
2423+ // We should be able to extend once successfully.
2424+ let result = root. extend ( ) . unwrap ( ) ;
2425+ // We are now at the MAX_CONTEXT_DEPTH and should fail.
2426+ let result_2 = result. extend ( ) ;
2427+ assert ! ( matches!(
2428+ result_2,
2429+ Err ( VmExecutionError :: Runtime (
2430+ RuntimeError :: MaxContextDepthReached ,
2431+ _
2432+ ) )
2433+ ) ) ;
2434+ }
22952435}
0 commit comments