diff --git a/contracts/escrow/IMPLEMENTATION.md b/contracts/escrow/IMPLEMENTATION.md index cd45038..154c344 100644 --- a/contracts/escrow/IMPLEMENTATION.md +++ b/contracts/escrow/IMPLEMENTATION.md @@ -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( @@ -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 diff --git a/contracts/escrow/src/lib.rs b/contracts/escrow/src/lib.rs index 7c1eceb..5dd48f6 100644 --- a/contracts/escrow/src/lib.rs +++ b/contracts/escrow/src/lib.rs @@ -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}; @@ -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(); @@ -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); @@ -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); } @@ -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, diff --git a/contracts/escrow/src/tests.rs b/contracts/escrow/src/tests.rs index d85a5fe..e496fc3 100644 --- a/contracts/escrow/src/tests.rs +++ b/contracts/escrow/src/tests.rs @@ -1,27 +1,399 @@ -// FIX: Module inception error removed by deleting "mod tests {" wrapper -use crate::{EscrowContract, EscrowContractClient}; -use soroban_sdk::{ - testutils::{Address as _, Ledger}, - Address, Env, Vec, -}; - -fn create_test_env() -> (Env, Address, Address, Address, Vec
) { - let env = Env::default(); - env.ledger().set_timestamp(1000); - - let creator = Address::generate(&env); - let token = Address::generate(&env); - let validator1 = Address::generate(&env); - let validator2 = Address::generate(&env); - let validator3 = Address::generate(&env); - - let mut validators = Vec::new(&env); - validators.push_back(validator1); - validators.push_back(validator2); - validators.push_back(validator3.clone()); - - (env, creator, token, validator3, validators) -} +#![cfg(test)] + +mod tests { + use crate::{EscrowContract, EscrowContractClient}; + use shared::types::MilestoneStatus; + use soroban_sdk::{ + testutils::{Address as _, Ledger}, + Address, Bytes, BytesN, Env, Vec, + }; + + /// common test environment builder; returns an oracle address as the + /// last element which is unused by the legacy tests. + fn create_test_env() -> (Env, Address, Address, Address, Vec
, Address) { + let env = Env::default(); + env.ledger().set_timestamp(1000); + + let creator = Address::generate(&env); + let token = Address::generate(&env); + let validator1 = Address::generate(&env); + let validator2 = Address::generate(&env); + let validator3 = Address::generate(&env); + let oracle = Address::generate(&env); + + let mut validators = Vec::new(&env); + validators.push_back(validator1); + validators.push_back(validator2); + validators.push_back(validator3.clone()); + + (env, creator, token, validator3, validators, oracle) + } + + fn create_client(env: &Env) -> EscrowContractClient { + EscrowContractClient::new(env, &env.register_contract(None, EscrowContract)) + } + + #[test] + fn test_initialize_escrow() { + let (env, creator, token, _, validators, _) = create_test_env(); + let client = create_client(&env); + env.mock_all_auths(); + + let result = client.initialize(&1, &creator, &token, &validators); + + // Verify escrow was created + let escrow = client.get_escrow(&1); + assert_eq!(escrow.project_id, 1); + assert_eq!(escrow.creator, creator); + assert_eq!(escrow.token, token); + assert_eq!(escrow.total_deposited, 0); + assert_eq!(escrow.released_amount, 0); + } + + #[test] + fn test_initialize_with_insufficient_validators() { + let env = Env::default(); + let creator = Address::generate(&env); + let token = Address::generate(&env); + + let mut validators = Vec::new(&env); + validators.push_back(Address::generate(&env)); + + let client = create_client(&env); + let result = client.try_initialize(&1, &creator, &token, &validators); + + assert!(result.is_err()); + } + + #[test] + fn test_initialize_duplicate_escrow() { + let (env, creator, token, _, validators, _) = create_test_env(); + let client = create_client(&env); + env.mock_all_auths(); + + client.initialize(&1, &creator, &token, &validators); + + // Try to initialize again + let result = client.try_initialize(&1, &creator, &token, &validators); + assert!(result.is_err()); + } + + #[test] + fn test_deposit_funds() { + let (env, creator, token, _, validators, _) = create_test_env(); + let client = create_client(&env); + env.mock_all_auths(); + + client.initialize(&1, &creator, &token, &validators); + + let deposit_amount: i128 = 1000; + let result = client.try_deposit(&1, &deposit_amount); + + assert!(result.is_ok()); + + let escrow = client.get_escrow(&1); + assert_eq!(escrow.total_deposited, deposit_amount); + } + + #[test] + fn test_deposit_invalid_amount() { + let (env, creator, token, _, validators, _) = create_test_env(); + let client = create_client(&env); + env.mock_all_auths(); + + client.initialize(&1, &creator, &token, &validators); + + let result = client.try_deposit(&1, &0); + assert!(result.is_err()); + + let result = client.try_deposit(&1, &-100); + assert!(result.is_err()); + } + + #[test] + fn test_create_milestone() { + let (env, creator, token, _, validators, _) = create_test_env(); + let client = create_client(&env); + + env.mock_all_auths(); + client.initialize(&1, &creator, &token, &validators); + client.deposit(&1, &1000); + + let description_hash = BytesN::from_array(&env, &[1u8; 32]); + client.create_milestone(&1, &description_hash, &500); + + let milestone = client.get_milestone(&1, &0); + assert_eq!(milestone.id, 0); + assert_eq!(milestone.project_id, 1); + assert_eq!(milestone.amount, 500); + assert_eq!(milestone.status, MilestoneStatus::Pending); + assert_eq!(milestone.description_hash, description_hash); + } + + #[test] + fn test_create_milestone_exceeds_escrow() { + let (env, creator, token, _, validators, _) = create_test_env(); + let client = create_client(&env); + + env.mock_all_auths(); + client.initialize(&1, &creator, &token, &validators); + client.deposit(&1, &500); + + let description_hash = BytesN::from_array(&env, &[2u8; 32]); + let result = client.try_create_milestone(&1, &description_hash, &1000); + + assert!(result.is_err()); + } + + #[test] + fn test_create_multiple_milestones() { + let (env, creator, token, _, validators, _) = create_test_env(); + let client = create_client(&env); + + env.mock_all_auths(); + client.initialize(&1, &creator, &token, &validators); + client.deposit(&1, &3000); + + let desc1 = BytesN::from_array(&env, &[1u8; 32]); + let desc2 = BytesN::from_array(&env, &[2u8; 32]); + let desc3 = BytesN::from_array(&env, &[3u8; 32]); + + client.create_milestone(&1, &desc1, &1000); + client.create_milestone(&1, &desc2, &1000); + client.create_milestone(&1, &desc3, &1000); + + // Verify all milestones exist + assert!(client.get_milestone(&1, &0).id == 0); + assert!(client.get_milestone(&1, &1).id == 1); + assert!(client.get_milestone(&1, &2).id == 2); + + let total = client.get_total_milestone_amount(&1); + assert_eq!(total, 3000); + } + + #[test] + fn test_submit_milestone() { + let (env, creator, token, _, validators, _) = create_test_env(); + let client = create_client(&env); + + env.mock_all_auths(); + client.initialize(&1, &creator, &token, &validators); + client.deposit(&1, &1000); + + let description_hash = BytesN::from_array(&env, &[1u8; 32]); + client.create_milestone(&1, &description_hash, &500); + + let proof_hash = BytesN::from_array(&env, &[9u8; 32]); + client.submit_milestone(&1, &0, &proof_hash); + + let milestone = client.get_milestone(&1, &0); + assert_eq!(milestone.status, MilestoneStatus::Submitted); + assert_eq!(milestone.proof_hash, proof_hash); + } + + #[test] + fn test_submit_milestone_invalid_status() { + let (env, creator, token, _, validators, _) = create_test_env(); + let client = create_client(&env); + env.mock_all_auths(); + + client.initialize(&1, &creator, &token, &validators); + + client.deposit(&1, &1000); + + let description_hash = BytesN::from_array(&env, &[1u8; 32]); + client.create_milestone(&1, &description_hash, &500); + + let proof_hash = BytesN::from_array(&env, &[9u8; 32]); + client.submit_milestone(&1, &0, &proof_hash); + + // Try to submit again - should fail because status is no longer Pending + let proof_hash2 = BytesN::from_array(&env, &[10u8; 32]); + let result = client.try_submit_milestone(&1, &0, &proof_hash2); + + assert!(result.is_err()); + } + + #[test] + fn test_get_available_balance() { + let (env, creator, token, _, validators, _) = create_test_env(); + let client = create_client(&env); + env.mock_all_auths(); + + client.initialize(&1, &creator, &token, &validators); + + client.deposit(&1, &1000); + + let balance = client.get_available_balance(&1); + assert_eq!(balance, 1000); + + client.deposit(&1, &500); + + let balance = client.get_available_balance(&1); + assert_eq!(balance, 1500); + } + + #[test] + fn test_escrow_not_found() { + let env = Env::default(); + let client = create_client(&env); + + let result = client.try_get_escrow(&999); + assert!(result.is_err() || result.is_err()); + } + + #[test] + fn test_milestone_not_found() { + let (env, creator, token, _, validators, _) = create_test_env(); + let client = create_client(&env); + env.mock_all_auths(); + + client.initialize(&1, &creator, &token, &validators); + + let result = client.try_get_milestone(&1, &999); + assert!(result.is_err() || result.is_err()); + } + + #[test] + fn test_milestone_status_transitions() { + let (env, creator, token, _, validators, _) = create_test_env(); + let client = create_client(&env); + env.mock_all_auths(); + + client.initialize(&1, &creator, &token, &validators); + + client.deposit(&1, &1000); + + let description_hash = BytesN::from_array(&env, &[1u8; 32]); + client.create_milestone(&1, &description_hash, &500); + + // Check initial status is Pending + let milestone = client.get_milestone(&1, &0); + assert_eq!(milestone.status, MilestoneStatus::Pending); + assert_eq!(milestone.approval_count, 0); + assert_eq!(milestone.rejection_count, 0); + + // Submit milestone + let proof_hash = BytesN::from_array(&env, &[9u8; 32]); + client.submit_milestone(&1, &0, &proof_hash); + + // Check status is now Submitted + let milestone = client.get_milestone(&1, &0); + assert_eq!(milestone.status, MilestoneStatus::Submitted); + assert_eq!(milestone.proof_hash, proof_hash); + } + + #[test] + fn test_oracle_milestone_auto_approval() { + let (env, creator, token, _, validators, oracle) = create_test_env(); + let client = create_client(&env); + env.mock_all_auths(); + + client.initialize(&1, &creator, &token, &validators); + client.deposit(&1, &1000); + + let description_hash = BytesN::from_array(&env, &[7u8; 32]); + let expected_hash = Bytes::from_slice(&env, &[99u8; 32]); + let deadline = env.ledger().timestamp() + 100; + + client.create_oracle_milestone( + &1, + &description_hash, + &500, + &oracle, + &expected_hash, + &deadline, + ); + + // creator submits proof as usual + let proof_hash = BytesN::from_array(&env, &[9u8; 32]); + client.submit_milestone(&1, &0, &proof_hash); + + // oracle delivers correct result + env.mock_all_auths(); + client.oracle_validate(&1, &0, &expected_hash); + + let milestone = client.get_milestone(&1, &0); + assert_eq!(milestone.status, MilestoneStatus::Approved); + + let escrow = client.get_escrow(&1); + assert_eq!(escrow.released_amount, 500); + } + + #[test] + fn test_oracle_milestone_fallback_manual() { + let (env, creator, token, _, validators, oracle) = create_test_env(); + let client = create_client(&env); + env.mock_all_auths(); + + client.initialize(&1, &creator, &token, &validators); + client.deposit(&1, &1000); + + let description_hash = BytesN::from_array(&env, &[8u8; 32]); + let expected_hash = Bytes::from_slice(&env, &[123u8; 32]); + let deadline = env.ledger().timestamp() + 10; + + client.create_oracle_milestone( + &1, + &description_hash, + &500, + &oracle, + &expected_hash, + &deadline, + ); + client.submit_milestone(&1, &0, &BytesN::from_array(&env, &[1u8; 32])); + + // oracle sends wrong data + env.mock_all_auths(); + client.oracle_validate(&1, &0, &Bytes::from_slice(&env, &[0u8; 32])); + + // milestone should stay in Submitted + let milestone = client.get_milestone(&1, &0); + assert_eq!(milestone.status, MilestoneStatus::Submitted); + + // advance time past deadline then have validators vote + env.ledger().set_timestamp(deadline + 1); + // pick a validator from vector + let voter = validators.get(0).unwrap(); + client.vote_milestone(&1, &0, &voter, &true); + // another vote to reach threshold + let voter2 = validators.get(1).unwrap(); + client.vote_milestone(&1, &0, &voter2, &true); + + let milestone = client.get_milestone(&1, &0); + assert_eq!(milestone.status, MilestoneStatus::Approved); + } + + #[test] + fn test_deposit_updates_correctly() { + let (env, creator, token, _, validators, _) = create_test_env(); + let client = create_client(&env); + env.mock_all_auths(); + + client.initialize(&1, &creator, &token, &validators); + + // First deposit + client.deposit(&1, &500); + let escrow = client.get_escrow(&1); + assert_eq!(escrow.total_deposited, 500); + + // Second deposit + client.deposit(&1, &300); + let escrow = client.get_escrow(&1); + assert_eq!(escrow.total_deposited, 800); + + // Third deposit + client.deposit(&1, &200); + let escrow = client.get_escrow(&1); + assert_eq!(escrow.total_deposited, 1000); + } + + #[test] + fn test_multiple_projects_isolated() { + let env = Env::default(); + env.mock_all_auths(); + + env.ledger().set_timestamp(1000); // FIX: Added lifetime elision to client fn create_client(env: &Env) -> EscrowContractClient<'_> { diff --git a/contracts/escrow/src/validation.rs b/contracts/escrow/src/validation.rs index 7882c0a..8e62581 100644 --- a/contracts/escrow/src/validation.rs +++ b/contracts/escrow/src/validation.rs @@ -10,3 +10,14 @@ pub fn validate_validator(escrow: &EscrowInfo, validator: &Address) -> Result<() Err(Error::NotAValidator) } } + +/// Simple oracle authorization check. An oracle must be registered on the +/// milestone itself (passed in by higher‑level code) so this helper is +/// primarily for readability. +pub fn validate_oracle(_escrow: &EscrowInfo, oracle: &Address, expected: &Address) -> Result<(), Error> { + if oracle == expected { + Ok(()) + } else { + Err(Error::OracleUnauthorized) + } +} diff --git a/contracts/shared/src/errors.rs b/contracts/shared/src/errors.rs index f6ce9f1..2285797 100644 --- a/contracts/shared/src/errors.rs +++ b/contracts/shared/src/errors.rs @@ -4,70 +4,92 @@ use soroban_sdk::contracterror; #[derive(Copy, Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] #[repr(u32)] pub enum Error { - // General errors (1-99) + // General errors (start at 1) NotInitialized = 1, - AlreadyInitialized = 2, - Unauthorized = 3, - InvalidInput = 4, - NotFound = 5, + AlreadyInitialized, + Unauthorized, + InvalidInput, + NotFound, - // Project errors (100-199) - ProjectNotActive = 100, - ProjectAlreadyExists = 101, - FundingGoalNotReached = 102, - DeadlinePassed = 103, - InvalidProjectStatus = 104, + // Project errors + ProjectNotActive, + ProjectAlreadyExists, + FundingGoalNotReached, + DeadlinePassed, + InvalidProjectStatus, +<<<<<<< HEAD // Escrow errors (200-299) InsufficientEscrowBalance = 200, MilestoneNotApproved = 201, InvalidMilestoneStatus = 202, NotAValidator = 203, AlreadyVoted = 204, + OracleUnauthorized = 205, + OracleValidationFailed = 206, + OracleDeadlineNotReached = 207, ContractPaused = 205, ResumeTooEarly = 206, UpgradeNotScheduled = 207, UpgradeTooEarly = 208, UpgradeRequiresPause = 209, +======= + // Escrow errors + InsufficientEscrowBalance, + MilestoneNotApproved, + InvalidMilestoneStatus, + NotAValidator, + AlreadyVoted, + OracleUnauthorized, + OracleValidationFailed, + OracleDeadlineNotReached, +>>>>>>> ee22966 (fixed the build error) - // Distribution errors (300-399) - InsufficientFunds = 300, - InvalidDistribution = 301, - NoClaimableAmount = 302, - DistributionFailed = 303, + // Distribution errors + InsufficientFunds, + InvalidDistribution, + NoClaimableAmount, + DistributionFailed, - // Subscription errors (400-499) - SubscriptionNotActive = 400, - InvalidSubscriptionPeriod = 401, - SubscriptionExists = 402, - WithdrawalLocked = 403, + // Subscription errors + SubscriptionNotActive, + InvalidSubscriptionPeriod, + SubscriptionExists, + WithdrawalLocked, - // Reputation errors (500-599) - ReputationTooLow = 500, - InvalidReputationScore = 501, - BadgeNotEarned = 502, - UserAlreadyRegistered = 503, - BadgeAlreadyAwarded = 504, - UserNotRegistered = 505, + // Reputation errors + ReputationTooLow, + InvalidReputationScore, + BadgeNotEarned, + UserAlreadyRegistered, + BadgeAlreadyAwarded, + UserNotRegistered, - // Governance errors (600-699) - ProposalNotActive = 600, - InsufficientVotingPower = 601, - ProposalAlreadyExecuted = 602, - QuorumNotReached = 603, + // Governance errors + ProposalNotActive, + InsufficientVotingPower, + ProposalAlreadyExecuted, + QuorumNotReached, - // Cross-chain bridge errors (700-799) - BridgePaused = 700, - ChainNotSupported = 701, - InvalidChain = 702, - BridgeTransactionFailed = 703, - InsufficientConfirmations = 704, - RelayerNotRegistered = 705, - InvalidBridgeOperation = 706, + // Cross-chain bridge errors + BridgePaused, + ChainNotSupported, + InvalidChain, + BridgeTransactionFailed, + InsufficientConfirmations, + RelayerNotRegistered, + InvalidBridgeOperation, +<<<<<<< HEAD InvalidFundingGoal = 1000, InvalidDeadline = 1001, ProjectNotFound = 1002, ContributionTooLow = 1003, IdentityNotVerified = 1004, +======= + InvalidFundingGoal, + InvalidDeadline, + ProjectNotFound, + ContributionTooLow, +>>>>>>> ee22966 (fixed the build error) } diff --git a/contracts/shared/src/events.rs b/contracts/shared/src/events.rs index ae5cd90..6516bc9 100644 --- a/contracts/shared/src/events.rs +++ b/contracts/shared/src/events.rs @@ -18,6 +18,8 @@ pub const MILESTONE_CREATED: Symbol = symbol_short!("m_create"); pub const MILESTONE_SUBMITTED: Symbol = symbol_short!("m_submit"); pub const MILESTONE_APPROVED: Symbol = symbol_short!("m_apprv"); pub const MILESTONE_REJECTED: Symbol = symbol_short!("m_reject"); +pub const MILESTONE_ORACLE_APPROVED: Symbol = symbol_short!("m_or_ap"); +pub const MILESTONE_ORACLE_FAILED: Symbol = symbol_short!("m_or_fail"); pub const MILESTONE_COMPLETED: Symbol = symbol_short!("milestone"); pub const VALIDATORS_UPDATED: Symbol = symbol_short!("v_update"); diff --git a/contracts/shared/src/types.rs b/contracts/shared/src/types.rs index a050d64..4653e5d 100644 --- a/contracts/shared/src/types.rs +++ b/contracts/shared/src/types.rs @@ -1,4 +1,4 @@ -use soroban_sdk::{contracttype, Address, BytesN, String, Vec}; +use soroban_sdk::{contracttype, Address, Bytes, BytesN, String, Vec}; /// Common timestamp type pub type Timestamp = u64; @@ -74,7 +74,21 @@ pub enum MilestoneStatus { Rejected = 3, // Rejected by majority } +#[contracttype] +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +#[repr(u32)] +pub enum ValidationType { + Manual = 0, // traditional validator vote + Oracle = 1, // external oracle provides objective result +} + /// Milestone structure +/// +/// `validation_type` indicates whether the milestone is reviewed manually by +/// validators or automatically via an oracle. When `Oracle` is selected the +/// additional fields (`oracle_address`, `oracle_expected_hash`, +/// `oracle_deadline`) provide an on-chain anchor for the external criteria. +/// Manual validator voting remains available as a fallback after the deadline. #[contracttype] #[derive(Clone, Debug)] pub struct Milestone { @@ -87,6 +101,11 @@ pub struct Milestone { pub approval_count: u32, pub rejection_count: u32, pub created_at: Timestamp, + // advanced validation fields + pub validation_type: ValidationType, + pub oracle_address: Option
, + pub oracle_expected_hash: Option, + pub oracle_deadline: Option, } /// Proposal status diff --git a/frontend/src/types/project.ts b/frontend/src/types/project.ts index e067f5b..7643334 100644 --- a/frontend/src/types/project.ts +++ b/frontend/src/types/project.ts @@ -4,6 +4,11 @@ export interface Milestone { description: string; percentage: number; estimatedDate: string; + // advanced validation metadata (optional) + validationType?: 'manual' | 'oracle'; + oracleAddress?: string; + oracleExpectedHash?: string; + oracleDeadline?: string; } export interface ProjectFormData { diff --git a/repowiki/repowiki/en/content/API Reference/Smart Contract APIs/Escrow API.md b/repowiki/repowiki/en/content/API Reference/Smart Contract APIs/Escrow API.md index 05c7cbc..7477d06 100644 --- a/repowiki/repowiki/en/content/API Reference/Smart Contract APIs/Escrow API.md +++ b/repowiki/repowiki/en/content/API Reference/Smart Contract APIs/Escrow API.md @@ -198,6 +198,37 @@ The contract exposes the following public functions. Each function includes para - project_id: Project identifier. - milestone_id: Milestone identifier. - approve: Boolean indicating approval. + +- create_oracle_milestone + - Purpose: Create a milestone that will be automatically validated by an external + oracle before falling back to manual validator voting. + - Parameters: + - project_id: Project identifier. + - description: Milestone description hash. + - amount: Amount requested for release. + - oracle: Address of the trusted oracle service. + - expected_hash: Bytes that the oracle is expected to report (typically a 32-byte hash). + - deadline: Timestamp after which validators may vote if oracle has not + approved. + - Validation: same as `create_milestone` plus a non‑zero deadline. + - Events: MILESTONE_CREATED. + +- oracle_validate + - Purpose: Called by the configured oracle to submit its verification result. + A successful check immediately approves the milestone and releases funds. + A mismatched result emits an oracle failure event and leaves the milestone + open for manual voting. + - Parameters: + - project_id: Project identifier. + - milestone_id: Milestone identifier. + - result_hash: Data hash returned by the oracle. + - Validation: + - Caller must be the configured oracle address. + - Milestone must be in Submitted state and of type Oracle. + - Events: + - MILESTONE_ORACLE_APPROVED (on match) + - MILESTONE_ORACLE_FAILED (on mismatch) + - FUNDS_RELEASED (if approved) - Validation: - Caller must be a validator and authorize the transaction. - Milestone must be in Submitted status.