Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
84 changes: 82 additions & 2 deletions contracts/contracts/escrow/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ pub enum MilestoneStatus {
Approved = 3, // Approved by client, ready for release
Released = 4, // Funds successfully transferred to freelancer
Refunded = 5, // Funds returned to client
Disputed = 6, // Under dispute, waiting for arbiter resolution
Disputed = 6,
AutoExpired = 7, // Under dispute, waiting for arbiter resolution
}

#[derive(Clone, Debug, PartialEq, Eq)]
#[contracttype]
pub struct Milestone {
pub deadline: u64,
pub id: u32,
pub amount: i128,
pub status: MilestoneStatus,
Expand All @@ -35,6 +37,7 @@ pub enum DataKey {
IsFunded,
ClientApproval(u32),
FreelancerApproval(u32),
MilestoneDeadline(u32),
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
Expand All @@ -50,6 +53,8 @@ pub enum Error {
ZeroAmount = 8,
InsufficientApprovals = 9,
AlreadyApproved = 10,
DeadlineExceeded = 11,
AlreadyExpired = 12,
}

#[contract]
Expand Down Expand Up @@ -87,6 +92,14 @@ impl EscrowContract {
env.storage().instance().set(&DataKey::Arbiter, &arbiter);
env.storage().instance().set(&DataKey::Token, &token);
env.storage().instance().set(&DataKey::Milestones, &milestones);

// Store milestone deadlines for time-based validation
for i in 0..milestones.len() {
let m = milestones.get(i).unwrap();
if m.deadline > 0 {
env.storage().instance().set(&DataKey::MilestoneDeadline(m.id), &m.deadline);
}
}
env.storage().instance().set(&DataKey::IsFunded, &false);

Ok(())
Expand Down Expand Up @@ -151,7 +164,10 @@ impl EscrowContract {
if milestone.status != MilestoneStatus::Funded {
return Err(Error::InvalidMilestoneStatus);
}
milestone.status = MilestoneStatus::Submitted;
if milestone.deadline > 0 && env.ledger().timestamp() > milestone.deadline {
return Err(Error::DeadlineExceeded);
}
milestone.status = MilestoneStatus::Submitted;
}
updated_milestones.push_back(milestone);
}
Expand Down Expand Up @@ -416,6 +432,70 @@ impl EscrowContract {
Ok(())
}


/// Auto-expire a milestone that has exceeded its deadline without being completed.
/// Refunds the milestone amount back to the client.
/// Can be called by anyone once the deadline has passed.
pub fn auto_expire(env: Env, milestone_id: u32) -> Result<(), Error> {
let milestones: Vec<Milestone> = env.storage().instance().get(&DataKey::Milestones).ok_or(Error::NotInitialized)?;
let mut found = false;
let mut transfer_amount: i128 = 0;
let mut updated_milestones = Vec::new(&env);

for i in 0..milestones.len() {
let mut milestone = milestones.get(i).unwrap();
if milestone.id == milestone_id {
found = true;

// Check deadline exists and has passed
if milestone.deadline == 0 {
return Err(Error::InvalidMilestoneStatus);
}
if env.ledger().timestamp() <= milestone.deadline {
return Err(Error::InvalidMilestoneStatus);
}

// Only expire if not yet released, refunded, or disputed
if milestone.status == MilestoneStatus::Released ||
milestone.status == MilestoneStatus::Refunded ||
milestone.status == MilestoneStatus::Disputed ||
milestone.status == MilestoneStatus::AutoExpired {
return Err(Error::AlreadyExpired);
}

transfer_amount = milestone.amount;
milestone.status = MilestoneStatus::AutoExpired;
}
updated_milestones.push_back(milestone);
}

if !found {
return Err(Error::MilestoneNotFound);
}

// Refund the expired milestone amount to the client
let client: Address = env.storage().instance().get(&DataKey::Client).ok_or(Error::NotInitialized)?;
let token_address: Address = env.storage().instance().get(&DataKey::Token).ok_or(Error::NotInitialized)?;
let token_client = token::Client::new(&env, &token_address);
token_client.transfer(&env.current_contract_address(), &client, &transfer_amount);

env.storage().instance().set(&DataKey::Milestones, &updated_milestones);
Ok(())
}

/// Check if a milestone has passed its deadline.
pub fn is_milestone_expired(env: Env, milestone_id: u32) -> Result<bool, Error> {
let deadline: u64 = env.storage().instance().get(&DataKey::MilestoneDeadline(milestone_id)).unwrap_or(0);
if deadline == 0 {
return Ok(false);
}
Ok(env.ledger().timestamp() > deadline)
}

/// Get the deadline for a specific milestone.
pub fn get_milestone_deadline(env: Env, milestone_id: u32) -> Result<u64, Error> {
env.storage().instance().get(&DataKey::MilestoneDeadline(milestone_id)).ok_or(Error::MilestoneNotFound)
}
// --- State Getters ---

pub fn get_client(env: Env) -> Result<Address, Error> {
Expand Down
3 changes: 3 additions & 0 deletions lib/escrow/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export type EscrowStatus =
| 'refunded'

export type MilestoneStatus =
| 'auto_expired'
| 'pending'
| 'in_progress'
| 'submitted'
Expand Down Expand Up @@ -131,6 +132,8 @@ export interface MilestoneInput {
description?: string
amount: string
dueDate?: string
/** Unix timestamp (seconds) for milestone deadline. After this, the milestone can be auto-expired. */
deadline?: number
sortOrder?: number
}

Expand Down
Loading