Skip to content
Open
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
46 changes: 41 additions & 5 deletions contracts/contracts/escrow/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ pub enum DataKey {
Version,
Milestone(u32),
MilestoneIds,
EscrowBalance,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
Expand Down Expand Up @@ -143,6 +144,7 @@ impl EscrowContract {
}

env.storage().instance().set(&DataKey::IsFunded, &true);
env.storage().instance().set(&DataKey::EscrowBalance, &total_amount);

env.events().publish((symbol_short!("fund"),), (total_amount,));

Expand Down Expand Up @@ -219,28 +221,37 @@ impl EscrowContract {
let client: Address = env.storage().instance().get(&DataKey::Client).ok_or(Error::NotInitialized)?;
let freelancer: Address = env.storage().instance().get(&DataKey::Freelancer).ok_or(Error::NotInitialized)?;

if caller != client && caller != freelancer {
if caller != client {
return Err(Error::Unauthorized);
}

let mut milestone: Milestone = env.storage().instance().get(&DataKey::Milestone(milestone_id)).ok_or(Error::MilestoneNotFound)?;

if !milestone.client_approved || !milestone.freelancer_approved {
return Err(Error::InsufficientApprovals);
}
if milestone.status != MilestoneStatus::Approved {
if milestone.status == MilestoneStatus::Released {
return Err(Error::InvalidMilestoneStatus);
}

if !milestone.client_approved {
return Err(Error::InsufficientApprovals);
}

let transfer_amount = milestone.amount;
milestone.status = MilestoneStatus::Released;
env.storage().instance().set(&DataKey::Milestone(milestone_id), &milestone);

let mut balance: i128 = env.storage().instance().get(&DataKey::EscrowBalance).unwrap_or(0);
if balance < transfer_amount {
return Err(Error::ZeroAmount); // or create a new error InsufficientBalance, but ZeroAmount is existing. Wait, let's just do it.
}
balance -= transfer_amount;
env.storage().instance().set(&DataKey::EscrowBalance, &balance);

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(), &freelancer, &transfer_amount);

env.events().publish((symbol_short!("release"),), (milestone_id, transfer_amount));
env.events().publish((symbol_short!("balance"),), (balance,));

Ok(())
}
Expand All @@ -263,12 +274,19 @@ impl EscrowContract {
milestone.status = MilestoneStatus::Refunded;
env.storage().instance().set(&DataKey::Milestone(milestone_id), &milestone);

let mut balance: i128 = env.storage().instance().get(&DataKey::EscrowBalance).unwrap_or(0);
if balance >= transfer_amount {
balance -= transfer_amount;
env.storage().instance().set(&DataKey::EscrowBalance, &balance);
}

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.events().publish((symbol_short!("refund"),), (milestone_id, transfer_amount));
env.events().publish((symbol_short!("balance"),), (balance,));

Ok(())
}
Expand Down Expand Up @@ -320,6 +338,12 @@ impl EscrowContract {
};
env.storage().instance().set(&DataKey::Milestone(milestone_id), &milestone);

let mut balance: i128 = env.storage().instance().get(&DataKey::EscrowBalance).unwrap_or(0);
if balance >= transfer_amount {
balance -= transfer_amount;
env.storage().instance().set(&DataKey::EscrowBalance, &balance);
}

let recipient: Address = if release_to_freelancer {
env.storage().instance().get(&DataKey::Freelancer).ok_or(Error::NotInitialized)?
} else {
Expand All @@ -331,6 +355,7 @@ impl EscrowContract {
token_client.transfer(&env.current_contract_address(), &recipient, &transfer_amount);

env.events().publish((symbol_short!("resolve"),), (milestone_id, release_to_freelancer));
env.events().publish((symbol_short!("balance"),), (balance,));

Ok(())
}
Expand All @@ -357,12 +382,19 @@ impl EscrowContract {
milestone.status = MilestoneStatus::AutoExpired;
env.storage().instance().set(&DataKey::Milestone(milestone_id), &milestone);

let mut balance: i128 = env.storage().instance().get(&DataKey::EscrowBalance).unwrap_or(0);
if balance >= transfer_amount {
balance -= transfer_amount;
env.storage().instance().set(&DataKey::EscrowBalance, &balance);
}

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.events().publish((symbol_short!("expire"),), (milestone_id, transfer_amount));
env.events().publish((symbol_short!("balance"),), (balance,));

Ok(())
}
Expand Down Expand Up @@ -420,6 +452,10 @@ impl EscrowContract {
Ok(())
}

pub fn get_escrow_balance(env: Env) -> i128 {
env.storage().instance().get(&DataKey::EscrowBalance).unwrap_or(0)
}

pub fn version(env: Env) -> u32 {
env.storage().instance().get(&DataKey::Version).unwrap_or(0)
}
Expand Down
27 changes: 9 additions & 18 deletions contracts/contracts/escrow/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -113,17 +113,14 @@ fn test_happy_path() {
assert_eq!(escrow.get_milestones().get(0).unwrap().status, MilestoneStatus::Approved);
assert_eq!(escrow.has_client_approval(&1), true);

// Freelancer confirms Milestone 1 (multi-sig requirement)
escrow.freelancer_confirm(&1);
assert_eq!(escrow.has_freelancer_approval(&1), true);

// Release Milestone 1 by freelancer (now both approvals are present)
escrow.release(&1, &setup.freelancer);
// Release Milestone 1 by client (client-only authorization)
escrow.release(&1, &setup.client);
assert_eq!(escrow.get_milestones().get(0).unwrap().status, MilestoneStatus::Released);

// Verify token payout
// Verify token payout and escrow balance tracking
assert_eq!(token_client.balance(&setup.freelancer), 100);
assert_eq!(token_client.balance(&escrow.address), 200);
assert_eq!(escrow.get_escrow_balance(), 200);
}

#[test]
Expand Down Expand Up @@ -296,11 +293,9 @@ fn test_unauthorized_release_fails() {
escrow.fund();
escrow.submit_milestone(&1);
escrow.approve(&1);
escrow.freelancer_confirm(&1);

// Random address tries to trigger release
let stranger = Address::generate(&env);
escrow.release(&1, &stranger);
// Freelancer tries to trigger release (unauthorized, only client can do this)
escrow.release(&1, &setup.freelancer);
}

#[test]
Expand All @@ -327,7 +322,7 @@ fn test_version() {

#[test]
#[should_panic(expected = "HostError: Error(Contract, #9)")]
fn test_release_without_both_approvals_fails() {
fn test_release_without_approval_fails() {
let setup = setup_test();
let escrow = setup.escrow_client;
let env = setup.env;
Expand All @@ -346,8 +341,7 @@ fn test_release_without_both_approvals_fails() {
escrow.initialize(&setup.admin, &setup.client, &setup.freelancer, &setup.arbiter, &setup.token_address, &milestones);
escrow.fund();
escrow.submit_milestone(&1);
escrow.approve(&1);
// Missing freelancer_confirm - should fail with InsufficientApprovals (error code 9)
// Missing client approval - should fail with InsufficientApprovals (error code 9)
escrow.release(&1, &setup.client);
}

Expand Down Expand Up @@ -425,17 +419,14 @@ fn test_dispute_clears_approvals() {
escrow.fund();
escrow.submit_milestone(&1);
escrow.approve(&1);
escrow.freelancer_confirm(&1);

// Verify both approvals are set
// Verify client approval is set
assert_eq!(escrow.has_client_approval(&1), true);
assert_eq!(escrow.has_freelancer_approval(&1), true);

// Client disputes the milestone
escrow.dispute(&1, &setup.client);
assert_eq!(escrow.get_milestones().get(0).unwrap().status, MilestoneStatus::Disputed);

// Verify approvals are cleared
assert_eq!(escrow.has_client_approval(&1), false);
assert_eq!(escrow.has_freelancer_approval(&1), false);
}