Skip to content

Commit 887182a

Browse files
armandocodecrclaude
andcommitted
fix: freeze milestone status changes once milestone is released or resolved
Status and evidence stay editable while a milestone's dispute is open (fresh evidence can help resolve it) but can no longer be rewritten after the milestone is released or resolved, preserving the on-chain evidence trail. Reuses existing MilestoneAlreadyReleased / MilestoneAlreadyResolved errors. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 6c2885a commit 887182a

2 files changed

Lines changed: 91 additions & 1 deletion

File tree

‎contracts/escrow/src/core/validators/milestone.rs‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,20 @@ pub fn validate_milestone_status_change_conditions(
3030

3131
let idx = validate_and_convert_milestone_index(update.index, escrow.milestones.len())?;
3232

33-
let _milestone = escrow
33+
let milestone = escrow
3434
.milestones
3535
.get(idx)
3636
.ok_or(ContractError::MilestoneToUpdateDoesNotExist)?;
37+
38+
// Status/evidence stay editable while a milestone's dispute is open
39+
// (fresh evidence can help resolve it), but freeze once the milestone
40+
// is released or resolved.
41+
if milestone.flags.released {
42+
return Err(ContractError::MilestoneAlreadyReleased);
43+
}
44+
if milestone.flags.resolved {
45+
return Err(ContractError::MilestoneAlreadyResolved);
46+
}
3747
}
3848

3949
Ok(())

‎contracts/escrow/src/tests/milestone.rs‎

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -524,3 +524,83 @@ fn test_update_after_milestone_released_append_new() {
524524
"New milestone flags must all be false"
525525
);
526526
}
527+
528+
#[test]
529+
fn test_status_change_rejected_when_milestone_released_or_resolved_allowed_while_disputed() {
530+
use crate::core::validators::milestone::validate_milestone_status_change_conditions;
531+
use crate::error::ContractError;
532+
533+
let env = Env::default();
534+
let service_provider = Address::generate(&env);
535+
let other = Address::generate(&env);
536+
537+
let milestone = Milestone {
538+
description: String::from_str(&env, "M0"),
539+
status: String::from_str(&env, "in-progress"),
540+
evidence: String::from_str(&env, ""),
541+
amount: 100,
542+
flags: Flags {
543+
approved: false,
544+
disputed: false,
545+
released: false,
546+
resolved: false,
547+
},
548+
receiver: other.clone(),
549+
};
550+
551+
let mut escrow = Escrow {
552+
engagement_id: String::from_str(&env, "t3_guard"),
553+
title: String::from_str(&env, "T3"),
554+
description: String::from_str(&env, "T3 guard test"),
555+
roles: Roles {
556+
approver: other.clone(),
557+
service_provider: service_provider.clone(),
558+
platform: other.clone(),
559+
release_signer: other.clone(),
560+
dispute_resolver: Address::generate(&env),
561+
},
562+
platform_fee: 300,
563+
milestones: vec![&env, milestone.clone()],
564+
trustline: Trustline {
565+
address: Address::generate(&env),
566+
},
567+
receiver_memo: 0,
568+
};
569+
570+
let updates = vec![
571+
&env,
572+
MilestoneUpdate {
573+
index: 0,
574+
status: String::from_str(&env, "completed"),
575+
evidence: None,
576+
},
577+
];
578+
579+
// Open dispute on the milestone: still editable.
580+
let mut m = escrow.milestones.get(0).unwrap();
581+
m.flags.disputed = true;
582+
escrow.milestones.set(0, m);
583+
assert!(
584+
validate_milestone_status_change_conditions(&escrow, &updates, &service_provider).is_ok()
585+
);
586+
587+
// Released milestone: frozen.
588+
let mut m = escrow.milestones.get(0).unwrap();
589+
m.flags.disputed = false;
590+
m.flags.released = true;
591+
escrow.milestones.set(0, m);
592+
assert_eq!(
593+
validate_milestone_status_change_conditions(&escrow, &updates, &service_provider),
594+
Err(ContractError::MilestoneAlreadyReleased)
595+
);
596+
597+
// Resolved milestone: frozen.
598+
let mut m = escrow.milestones.get(0).unwrap();
599+
m.flags.released = false;
600+
m.flags.resolved = true;
601+
escrow.milestones.set(0, m);
602+
assert_eq!(
603+
validate_milestone_status_change_conditions(&escrow, &updates, &service_provider),
604+
Err(ContractError::MilestoneAlreadyResolved)
605+
);
606+
}

0 commit comments

Comments
 (0)