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
27 changes: 26 additions & 1 deletion contracts/escrow/IMPLEMENTATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,9 @@ pub fn deposit(

### create_milestone

Creates a new milestone within an escrow.
Creates a new milestone within an escrow. This legacy entry-point continues to
support the traditional, manual validation workflow in which project validators
vote on submitted proofs.

```rust
pub fn create_milestone(
Expand All @@ -147,6 +149,29 @@ pub fn create_milestone(
) -> Result<(), Error>
```

### create_oracle_milestone

An enhanced constructor for milestones that require objective, external
verification. The caller must specify the oracle service address, an expected
hash that the oracle will return, and a deadline timestamp. Prior to the
deadline the configured oracle is the only actor authorised to mark the
milestone as approved. If the oracle provides a matching hash the funds are
released immediately; a mismatch or lack of response leaves the milestone in the
`Submitted` state so that validators can fall back to manual voting after the
deadline.

```rust
pub fn create_oracle_milestone(
env: Env,
project_id: u64,
description: String,
amount: Amount,
oracle: Address,
expected_hash: Bytes,
deadline: u64,
) -> Result<(), Error>
```

**Parameters:**
- `project_id`: Project identifier
- `description`: Milestone description
Expand Down
155 changes: 153 additions & 2 deletions contracts/escrow/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,7 @@ use shared::{
constants::{MIN_VALIDATORS, RESUME_TIME_DELAY, UPGRADE_TIME_LOCK_SECS},
errors::Error,
events::*,
types::{Amount, EscrowInfo, Hash, Milestone, MilestoneStatus, PauseState, PendingUpgrade},
MAX_APPROVAL_THRESHOLD, MIN_APPROVAL_THRESHOLD,
types::{Amount, EscrowInfo, Hash, Milestone, MilestoneStatus, ValidationType},
};
use soroban_sdk::{contract, contractimpl, token::TokenClient, Address, BytesN, Env, Vec};

Expand Down Expand Up @@ -103,6 +102,7 @@ impl EscrowContract {
description_hash: Hash,
amount: Amount,
) -> Result<(), Error> {
// backward-compatible manual milestone creation
let escrow = get_escrow(&env, project_id)?;
escrow.creator.require_auth();

Expand Down Expand Up @@ -137,6 +137,10 @@ impl EscrowContract {
approval_count: 0,
rejection_count: 0,
created_at: env.ledger().timestamp(),
validation_type: ValidationType::Manual,
oracle_address: None,
oracle_expected_hash: None,
oracle_deadline: None,
};

set_milestone(&env, project_id, milestone_id, &milestone);
Expand Down Expand Up @@ -198,6 +202,16 @@ impl EscrowContract {

let mut milestone = get_milestone(&env, project_id, milestone_id)?;

// Oracle milestones cannot be voted on until deadline has passed
if milestone.validation_type == ValidationType::Oracle {
if let Some(deadline) = milestone.oracle_deadline {
if env.ledger().timestamp() < deadline {
return Err(Error::OracleDeadlineNotReached);
}
}
}

// Validate milestone status
if milestone.status != MilestoneStatus::Submitted {
return Err(Error::InvalidMilestoneStatus);
}
Expand Down Expand Up @@ -282,6 +296,143 @@ impl EscrowContract {
Ok(escrow.total_deposited - escrow.released_amount)
}

/// Create an oracle‑backed milestone. The oracle address will be the only
/// actor authorized to deliver the objective validation result before the
/// deadline. After the deadline (or on oracle failure) standard validator
/// voting is allowed as a fallback.
///
/// # Arguments
/// * `project_id` - Project identifier
/// * `description_hash` - Hash of the milestone description
/// * `amount` - Amount to be released when milestone is approved
/// * `oracle` - Address of the oracle service
/// * `expected_hash` - Expected data hash that the oracle will return
/// * `deadline` - UNIX timestamp after which validators may vote as a fallback
pub fn create_oracle_milestone(
env: Env,
project_id: u64,
description_hash: Hash,
amount: Amount,
oracle: Address,
expected_hash: Bytes,
deadline: u64,
) -> Result<(), Error> {
// basic sanity checks are same as manual milestone
let escrow = get_escrow(&env, project_id)?;
escrow.creator.require_auth();

if amount <= 0 {
return Err(Error::InvalidInput);
}
let total_milestones = get_total_milestone_amount(&env, project_id)?;
let new_total = total_milestones
.checked_add(amount)
.ok_or(Error::InvalidInput)?;
if new_total > escrow.total_deposited {
return Err(Error::InsufficientEscrowBalance);
}

let milestone_id = get_milestone_counter(&env, project_id)?;
let next_id = milestone_id.checked_add(1).ok_or(Error::InvalidInput)?;

let empty_hash = BytesN::from_array(&env, &[0u8; 32]);
let milestone = Milestone {
id: milestone_id,
project_id,
description_hash: description_hash.clone(),
amount,
status: MilestoneStatus::Pending,
proof_hash: empty_hash,
approval_count: 0,
rejection_count: 0,
created_at: env.ledger().timestamp(),
validation_type: ValidationType::Oracle,
oracle_address: Some(oracle.clone()),
oracle_expected_hash: Some(expected_hash.clone()),
oracle_deadline: Some(deadline),
};

set_milestone(&env, project_id, milestone_id, &milestone);
set_milestone_counter(&env, project_id, next_id);

env.events().publish(
(MILESTONE_CREATED,),
(project_id, milestone_id, amount, description_hash),
);

Ok(())
}

/// Oracle callback to validate an objective milestone.
/// Only the specified oracle address may call this before the deadline.
/// If the returned hash matches the expected value the milestone is
/// immediately approved and funds released. A mismatch emits a failure
/// event and leaves the milestone open for validator voting after the
/// deadline.
pub fn oracle_validate(
env: Env,
project_id: u64,
milestone_id: u64,
result_hash: Bytes,
) -> Result<(), Error> {
// retrieve milestone
let mut milestone = get_milestone(&env, project_id, milestone_id)?;
if milestone.validation_type != ValidationType::Oracle {
return Err(Error::InvalidInput);
}
// only configured oracle may call
let oracle_addr = milestone
.oracle_address
.clone()
.ok_or(Error::InvalidInput)?;
oracle_addr.require_auth();

// must be in submitted state
if milestone.status != MilestoneStatus::Submitted {
return Err(Error::InvalidMilestoneStatus);
}

// compare result
if let Some(expected) = milestone.oracle_expected_hash.clone() {
if result_hash == expected {
// automatic approval path
milestone.status = MilestoneStatus::Approved;
// release funds
let mut escrow = get_escrow(&env, project_id)?;
release_milestone_funds(&env, &mut escrow, &milestone)?;
let token_client = TokenClient::new(&env, &escrow.token);
token_client.transfer(
&env.current_contract_address(),
&escrow.creator,
&milestone.amount,
);
set_escrow(&env, project_id, &escrow);
set_milestone(&env, project_id, milestone_id, &milestone);
env.events().publish(
(MILESTONE_ORACLE_APPROVED,),
(project_id, milestone_id),
);
env.events().publish(
(FUNDS_RELEASED,),
(project_id, milestone_id, milestone.amount),
);
} else {
// mismatch: emit failure but keep submitted so validators can vote
env.events().publish(
(MILESTONE_ORACLE_FAILED,),
(project_id, milestone_id),
);
set_milestone(&env, project_id, milestone_id, &milestone);
}
} else {
return Err(Error::InvalidInput);
}
Ok(())
}
///
/// # Arguments
/// * `project_id` - Project identifier
/// * `new_validators` - New list of validator addresses
pub fn update_validators(
env: Env,
project_id: u64,
Expand Down
Loading
Loading