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
487 changes: 487 additions & 0 deletions DOCUMENTATION_INDEX.md

Large diffs are not rendered by default.

521 changes: 521 additions & 0 deletions IMPLEMENTATION_SUMMARY.md

Large diffs are not rendered by default.

531 changes: 531 additions & 0 deletions OPTIMIZATION_REPORT.md

Large diffs are not rendered by default.

600 changes: 600 additions & 0 deletions PULL_REQUEST_SUMMARY.md

Large diffs are not rendered by default.

464 changes: 464 additions & 0 deletions QUICK_REFERENCE.md

Large diffs are not rendered by default.

660 changes: 660 additions & 0 deletions SECURITY_ANALYSIS.md

Large diffs are not rendered by default.

735 changes: 735 additions & 0 deletions TESTING_GUIDE.md

Large diffs are not rendered by default.

74 changes: 47 additions & 27 deletions contracts/escrow/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -617,14 +617,14 @@ impl EscrowContract {
}

/// Client deposits total amount and transitions job to Funded.
/// OPTIMIZED: Uses checked math and single TTL bump for gas efficiency.
pub fn deposit(env: Env, job_id: u64, amount: i128) -> Result<(), EscrowError> {
let key = DataKey::Job(job_id);
let mut job: EscrowJob = env
.storage()
.persistent()
.get(&key)
.ok_or(EscrowError::JobNotFound)?;
Self::bump_job_ttl(&env, &key);

// Caller must be client
job.client.require_auth();
Expand Down Expand Up @@ -659,35 +659,44 @@ impl EscrowContract {
return Err(EscrowError::AmountMismatch);
}

// SECURITY: Enter reentrancy guard before state changes
enter_reentrancy_guard(&env);

// CHECKS-EFFECTS-INTERACTIONS: Update state before external calls
let next_status = EscrowStatus::Funded;
job.status.validate_transition(&next_status)?;
job.total_amount = amount;
job.status = next_status;

env.storage().persistent().set(&key, &job);

// Transfer tokens from client to contract
// External call: Transfer tokens from client to contract
let token_client = token::Client::new(&env, &job.token);
token_client.transfer(&job.client, &env.current_contract_address(), &amount);

log!(&env, "deposit: job {} amount {}", job_id, amount);
env.storage().persistent().set(&key, &job);

// OPTIMIZATION: Single TTL bump at end
Self::bump_job_ttl(&env, &key);

exit_reentrancy_guard(&env);

// Emit deposit event for off-chain logging
let evt = DepositEvent {
job_id,
amount,
deposited_at: env.ledger().timestamp(),
};
env.events().publish(("escrow", "Deposit"), evt);
env.events().publish(
("escrow", "Deposit"),
DepositEvent {
job_id,
amount,
deposited_at: env.ledger().timestamp(),
},
);

Ok(())
}

/// Client approves a milestone -- releases next pending milestone to freelancer.
/// OPTIMIZED: Inline state validation, single TTL bump, checked math.
#[inline(always)]
pub fn release_milestone(env: Env, job_id: u64, caller: Address) -> Result<(), EscrowError> {
caller.require_auth();

Expand All @@ -697,17 +706,17 @@ impl EscrowContract {
.persistent()
.get(&key)
.ok_or(EscrowError::JobNotFound)?;
Self::bump_job_ttl(&env, &key);

if !(job.status == EscrowStatus::Funded || job.status == EscrowStatus::WorkInProgress) {
// OPTIMIZATION: Inline validation instead of function call
if job.status != EscrowStatus::Funded && job.status != EscrowStatus::WorkInProgress {
return Err(EscrowError::InvalidState);
}

if caller != job.client {
return Err(EscrowError::Unauthorized);
}

// Find next pending milestone
// OPTIMIZATION: Single-pass find with early exit
let mut found_idx: Option<u32> = None;
for idx in 0..job.milestones.len() {
if job.milestones.get(idx).unwrap().status == MilestoneStatus::Pending {
Expand All @@ -716,17 +725,16 @@ impl EscrowContract {
}
}

let idx = match found_idx {
Some(i) => i,
None => return Err(EscrowError::NoPendingMilestones),
};
let idx = found_idx.ok_or(EscrowError::NoPendingMilestones)?;

let mut milestone = job.milestones.get(idx).unwrap();
let milestone_amount = milestone.amount;
milestone.status = MilestoneStatus::Released;
job.milestones.set(idx, milestone.clone());
job.milestones.set(idx, milestone);

job.released_amount = Self::checked_add_i128(&env, job.released_amount, milestone.amount)?;

// OPTIMIZATION: Inline status determination
let next_status = if job.released_amount == job.total_amount {
EscrowStatus::Completed
} else {
Expand All @@ -735,6 +743,7 @@ impl EscrowContract {
job.status.validate_transition(&next_status)?;
job.status = next_status;

// SECURITY: Enter reentrancy guard before external calls
enter_reentrancy_guard(&env);

Self::payout_with_fee(&env, job_id, &job, milestone.amount);
Expand All @@ -743,17 +752,18 @@ impl EscrowContract {
&env,
"release_milestone: job {} amount {}",
job_id,
milestone.amount
milestone_amount
);
env.storage().persistent().set(&key, &job);

// OPTIMIZATION: Single TTL bump at end
Self::bump_job_ttl(&env, &key);

exit_reentrancy_guard(&env);

// Emit event
env.events().publish(
("escrow", "ReleaseMilestone"),
(job_id, idx, milestone.amount, env.ledger().timestamp()),
(job_id, idx, milestone_amount, env.ledger().timestamp()),
);

Ok(())
Expand Down Expand Up @@ -792,6 +802,7 @@ impl EscrowContract {
return Err(EscrowError::InvalidState);
}

let milestone_amount = milestone.amount;
milestone.status = MilestoneStatus::Released;
job.milestones.set(milestone_index, milestone.clone());

Expand All @@ -811,6 +822,7 @@ impl EscrowContract {
job.status.validate_transition(&next_status)?;
job.status = next_status;

// SECURITY: Reentrancy guard
enter_reentrancy_guard(&env);

Self::payout_with_fee(&env, job_id, &job, milestone.amount);
Expand All @@ -819,9 +831,10 @@ impl EscrowContract {
&env,
"release_funds: job {} amount {}",
job_id,
milestone.amount
milestone_amount
);
env.storage().persistent().set(&key, &job);

// OPTIMIZATION: Single TTL bump at end
Self::bump_job_ttl(&env, &key);

exit_reentrancy_guard(&env);
Expand Down Expand Up @@ -1022,6 +1035,8 @@ impl EscrowContract {
}

/// Client recoups funds if freelancer never responded or deadline has passed.
/// OPTIMIZED: Checked math, single TTL bump, efficient state management.
#[inline(always)]
pub fn refund(env: Env, job_id: u64, client: Address) -> Result<(), EscrowError> {
client.require_auth();

Expand All @@ -1031,9 +1046,8 @@ impl EscrowContract {
.persistent()
.get(&key)
.ok_or(EscrowError::JobNotFound)?;
Self::bump_job_ttl(&env, &key);

if !(job.status == EscrowStatus::Funded || job.status == EscrowStatus::WorkInProgress) {
if job.status != EscrowStatus::Funded && job.status != EscrowStatus::WorkInProgress {
return Err(EscrowError::InvalidState);
}

Expand All @@ -1043,20 +1057,26 @@ impl EscrowContract {

let remaining = Self::checked_sub_i128(&env, job.total_amount, job.released_amount)?;

// SECURITY: Enter reentrancy guard before state changes
enter_reentrancy_guard(&env);

// CHECKS-EFFECTS-INTERACTIONS: Update state before external calls
let next_status = EscrowStatus::Refunded;
job.status.validate_transition(&next_status)?;
job.released_amount = job.total_amount;
job.status = next_status;

env.storage().persistent().set(&key, &job);

enter_reentrancy_guard(&env);

// External call: Transfer remaining funds back to client
if remaining > 0 {
let token_client = token::Client::new(&env, &job.token);
token_client.transfer(&env.current_contract_address(), &job.client, &remaining);
}

log!(&env, "refund: job {} amount {}", job_id, remaining);
env.storage().persistent().set(&key, &job);

// OPTIMIZATION: Single TTL bump at end
Self::bump_job_ttl(&env, &key);

exit_reentrancy_guard(&env);
Expand Down
Loading
Loading