Skip to content

Commit 1c7bf30

Browse files
authored
Merge pull request #6602 from obycode/fix/post-condition-check-order
feat: enforce ordering of allowance check
2 parents 7dc8b45 + 4fb73a0 commit 1c7bf30

File tree

2 files changed

+74
-48
lines changed

2 files changed

+74
-48
lines changed

clarity/src/vm/functions/post_conditions.rs

Lines changed: 38 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,13 @@ fn check_allowances(
384384
allowances: Vec<Allowance>,
385385
assets: &AssetMap,
386386
) -> InterpreterResult<Option<u128>> {
387+
let mut earliest_violation: Option<u128> = None;
388+
let mut record_violation = |candidate: u128| {
389+
if earliest_violation.is_none_or(|current| candidate < current) {
390+
earliest_violation = Some(candidate);
391+
}
392+
};
393+
387394
// Elements are (index in allowances, amount)
388395
let mut stx_allowances: Vec<(usize, u128)> = Vec::new();
389396
// Map assets to a vector of (index in allowances, amount)
@@ -428,34 +435,30 @@ fn check_allowances(
428435

429436
// Check STX movements
430437
if let Some(stx_moved) = assets.get_stx(owner) {
431-
// If there are no allowances for STX, any movement is a violation
432438
if stx_allowances.is_empty() {
433-
return Ok(Some(MAX_ALLOWANCES as u128));
434-
}
435-
436-
// Check against the STX allowances
437-
for (index, allowance) in &stx_allowances {
438-
if stx_moved > *allowance {
439-
return Ok(Some(u128::try_from(*index).map_err(|_| {
440-
InterpreterError::Expect("failed to convert index to u128".into())
441-
})?));
439+
// If there are no allowances for STX, any movement is a violation
440+
record_violation(MAX_ALLOWANCES as u128);
441+
} else {
442+
for (index, allowance) in &stx_allowances {
443+
if stx_moved > *allowance {
444+
record_violation(*index as u128);
445+
break;
446+
}
442447
}
443448
}
444449
}
445450

446451
// Check STX burns
447452
if let Some(stx_burned) = assets.get_stx_burned(owner) {
448-
// If there are no allowances for STX, any burn is a violation
449453
if stx_allowances.is_empty() {
450-
return Ok(Some(MAX_ALLOWANCES as u128));
451-
}
452-
453-
// Check against the STX allowances
454-
for (index, allowance) in &stx_allowances {
455-
if stx_burned > *allowance {
456-
return Ok(Some(u128::try_from(*index).map_err(|_| {
457-
InterpreterError::Expect("failed to convert index to u128".into())
458-
})?));
454+
// If there are no allowances for STX, any burn is a violation
455+
record_violation(MAX_ALLOWANCES as u128);
456+
} else {
457+
for (index, allowance) in &stx_allowances {
458+
if stx_burned > *allowance {
459+
record_violation(*index as u128);
460+
break;
461+
}
459462
}
460463
}
461464
}
@@ -479,17 +482,13 @@ fn check_allowances(
479482

480483
if merged.is_empty() {
481484
// No allowance for this asset, any movement is a violation
482-
return Ok(Some(MAX_ALLOWANCES as u128));
485+
record_violation(MAX_ALLOWANCES as u128);
486+
continue;
483487
}
484488

485-
// Sort by allowance index so we check allowances in order
486-
merged.sort_by_key(|(idx, _)| *idx);
487-
488489
for (index, allowance) in merged {
489490
if *amount_moved > allowance {
490-
return Ok(Some(u128::try_from(index).map_err(|_| {
491-
InterpreterError::Expect("failed to convert index to u128".into())
492-
})?));
491+
record_violation(index as u128);
493492
}
494493
}
495494
}
@@ -512,20 +511,13 @@ fn check_allowances(
512511

513512
if merged.is_empty() {
514513
// No allowance for this asset, any movement is a violation
515-
return Ok(Some(MAX_ALLOWANCES as u128));
514+
record_violation(MAX_ALLOWANCES as u128);
515+
continue;
516516
}
517517

518-
// Sort by allowance index so we check allowances in order
519-
merged.sort_by_key(|(idx, _)| *idx);
520-
521518
for (index, allowance_vec) in merged {
522-
// Check against the NFT allowances
523-
for id_moved in ids_moved {
524-
if !allowance_vec.contains(id_moved) {
525-
return Ok(Some(u128::try_from(index).map_err(|_| {
526-
InterpreterError::Expect("failed to convert index to u128".into())
527-
})?));
528-
}
519+
if ids_moved.iter().any(|id| !allowance_vec.contains(id)) {
520+
record_violation(index as u128);
529521
}
530522
}
531523
}
@@ -535,20 +527,18 @@ fn check_allowances(
535527
if let Some(stx_stacked) = assets.get_stacking(owner) {
536528
// If there are no allowances for stacking, any stacking is a violation
537529
if stacking_allowances.is_empty() {
538-
return Ok(Some(MAX_ALLOWANCES as u128));
539-
}
540-
541-
// Check against the stacking allowances
542-
for (index, allowance) in &stacking_allowances {
543-
if stx_stacked > *allowance {
544-
return Ok(Some(u128::try_from(*index).map_err(|_| {
545-
InterpreterError::Expect("failed to convert index to u128".into())
546-
})?));
530+
record_violation(MAX_ALLOWANCES as u128);
531+
} else {
532+
for (index, allowance) in &stacking_allowances {
533+
if stx_stacked > *allowance {
534+
record_violation(*index as u128);
535+
break;
536+
}
547537
}
548538
}
549539
}
550540

551-
Ok(None)
541+
Ok(earliest_violation)
552542
}
553543

554544
/// Handles all allowance functions, always returning an error, since these are

clarity/src/vm/tests/post_conditions.rs

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1404,3 +1404,39 @@ fn test_restrict_assets_with_error_in_body() {
14041404
ClarityError::ShortReturn(ShortReturnType::ExpectedValue(expected_err.into()));
14051405
assert_eq!(short_return, execute(snippet).unwrap_err());
14061406
}
1407+
1408+
#[test]
1409+
fn test_restrict_assets_with_multiple_violations_different_kinds() {
1410+
let snippet = r#"
1411+
(define-non-fungible-token stackaroo uint)
1412+
(nft-mint? stackaroo u122 tx-sender)
1413+
(nft-mint? stackaroo u123 tx-sender)
1414+
(let ((recipient 'SP000000000000000000002Q6VF78))
1415+
(restrict-assets? tx-sender ((with-nft current-contract "stackaroo" (list u122)) (with-stx u10))
1416+
(begin
1417+
(try! (stx-transfer? u50 tx-sender recipient))
1418+
(try! (nft-transfer? stackaroo u123 tx-sender recipient))
1419+
)
1420+
)
1421+
)"#;
1422+
let expected = Value::error(Value::UInt(0)).unwrap();
1423+
assert_eq!(expected, execute(snippet).unwrap().unwrap());
1424+
}
1425+
1426+
#[test]
1427+
fn test_restrict_assets_with_multiple_violations_different_kinds_order_2() {
1428+
let snippet = r#"
1429+
(define-non-fungible-token stackaroo uint)
1430+
(nft-mint? stackaroo u122 tx-sender)
1431+
(nft-mint? stackaroo u123 tx-sender)
1432+
(let ((recipient 'SP000000000000000000002Q6VF78))
1433+
(restrict-assets? tx-sender ((with-stx u10) (with-nft current-contract "stackaroo" (list u122)))
1434+
(begin
1435+
(try! (nft-transfer? stackaroo u123 tx-sender recipient))
1436+
(try! (stx-transfer? u50 tx-sender recipient))
1437+
)
1438+
)
1439+
)"#;
1440+
let expected = Value::error(Value::UInt(0)).unwrap();
1441+
assert_eq!(expected, execute(snippet).unwrap().unwrap());
1442+
}

0 commit comments

Comments
 (0)