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
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions contracts/pifp_protocol/src/bench_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -233,10 +233,10 @@ fn bench_update_metadata() {
}

// ---------------------------------------------------------------------------
// verify_and_release (legacy stub)
// verify_proof (legacy stub)
// ---------------------------------------------------------------------------
#[test]
fn bench_verify_and_release() {
fn bench_verify_proof() {
let env = Env::default();
env.mock_all_auths();

Expand All @@ -247,8 +247,8 @@ fn bench_verify_and_release() {
let proof_hash = BytesN::from_array(&env, &[12u8; 32]);

env.cost_estimate().budget().reset_default();
client.verify_and_release(&project_id, &proof_hash);
client.verify_proof(&project_id, &proof_hash);

let b = env.cost_estimate().budget();
report("verify_and_release", b.cpu_instruction_cost(), b.memory_bytes_cost());
report("verify_proof", b.cpu_instruction_cost(), b.memory_bytes_cost());
}
9 changes: 8 additions & 1 deletion contracts/pifp_protocol/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
//! |------|--------------------------|-------------------------------------------------------------|
//! | 1 | `ProjectNotFound` | Querying or operating on a project ID that does not exist |
//! | 2 | `MilestoneNotFound` | Reserved for future milestone-level operations |
//! | 3 | `MilestoneAlreadyReleased` | Calling `verify_and_release` on an already-completed project |
//! | 3 | `MilestoneAlreadyReleased` | Calling `verify_proof` on an already-verified/completed project |
//! | 4 | `InsufficientBalance` | Refund requested but donator has zero balance for that token |
//! | 5 | `InvalidMilestones` | Reserved for future milestone validation |
//! | 6 | `NotAuthorized` | Caller lacks the RBAC role required for the operation |
Expand Down Expand Up @@ -41,6 +41,7 @@
//! | 31 | `MetadataCidInvalid` | IPFS CID byte string was empty or exceeded max length |
//! | 32 | `FeeBpsExceedsMaximum` | Configured fee in basis points exceeds the 10_000 hard cap |
//! | 33 | `ProjectPaused` | Mutating project action attempted while the project is paused |
//! | 34 | `GracePeriodActive` | `claim_funds` called before the 24-hour grace period has elapsed |

use soroban_sdk::contracterror;

Expand Down Expand Up @@ -147,4 +148,10 @@ pub enum Error {

/// The proposed fee in basis points exceeds the hard cap of 10,000.
FeeBpsExceedsMaximum = 32,

/// The target project is paused; deposits and releases are temporarily blocked.
ProjectPaused = 33,

/// The 24-hour grace period after proof verification has not yet elapsed.
GracePeriodActive = 34,
}
71 changes: 52 additions & 19 deletions contracts/pifp_protocol/src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -141,10 +141,6 @@ pub struct FundsReleased {
pub project_id: u64,
pub token: Address,
pub amount: i128,
pub oracle: Option<Address>,
pub oracle_index: Option<u32>,
pub voter_count: Option<u32>,
pub threshold: Option<u32>,
}

#[contracttype]
Expand All @@ -161,6 +157,23 @@ pub struct OracleRemoved {
pub oracle: Address,
}

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct FundsClaimed {
pub project_id: u64,
pub creator: Address,
}

#[contracttype]
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct OracleVoted {
pub project_id: u64,
pub oracle: Address,
pub index: u32,
pub voter_count: u32,
pub threshold: u32,
}

// ── Emission helpers ──────────────────────────────────────────────────────────

pub fn emit_project_created(
Expand Down Expand Up @@ -241,20 +254,12 @@ pub fn emit_funds_released(
project_id: u64,
token: Address,
amount: i128,
oracle: Option<Address>,
oracle_index: Option<u32>,
voter_count: Option<u32>,
threshold: Option<u32>,
) {
let topics = (symbol_short!("fund_rel"), project_id);
let topics = (symbol_short!("fnd_rel"), project_id);
let data = FundsReleased {
project_id,
token,
amount,
oracle,
oracle_index,
voter_count,
threshold,
};
env.events().publish(topics, data);
}
Expand Down Expand Up @@ -349,14 +354,32 @@ pub fn emit_protocol_unpaused(env: &Env, admin: Address) {
env.events().publish(topics, data);
}

pub fn emit_milestone_verified(
pub fn emit_funds_claimed(env: &Env, project_id: u64, creator: Address) {
let topics = (symbol_short!("fnd_clm"), project_id);
let data = FundsClaimed {
project_id,
creator,
};
env.events().publish(topics, data);
}

pub fn emit_oracle_voted(
env: &Env,
project_id: u64,
milestone_index: u32,
bps: u32,
oracle: Address,
index: u32,
voter_count: u32,
threshold: u32,
) {
let topics = (MILESTONE_VERIFIED, project_id, milestone_index);
env.events().publish(topics, bps);
let topics = (symbol_short!("ora_voted"), project_id);
let data = OracleVoted {
project_id,
oracle,
index,
voter_count,
threshold,
};
env.events().publish(topics, data);
}

pub fn emit_oracle_added(env: &Env, project_id: u64, oracle: Address) {
Expand All @@ -369,4 +392,14 @@ pub fn emit_oracle_removed(env: &Env, project_id: u64, oracle: Address) {
let topics = (symbol_short!("ora_rem"), project_id);
let data = OracleRemoved { project_id, oracle };
env.events().publish(topics, data);
}
}

pub fn emit_milestone_verified(
env: &Env,
project_id: u64,
milestone_index: u32,
bps: u32,
) {
let topics = (MILESTONE_VERIFIED, project_id, milestone_index);
env.events().publish(topics, bps);
}
52 changes: 37 additions & 15 deletions contracts/pifp_protocol/src/fuzz_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ extern crate std;
use std::vec::Vec;

use proptest::prelude::*;
use soroban_sdk::{testutils::Address as _, token, Address, Bytes, BytesN, Env, Vec as SorobanVec};
use soroban_sdk::{testutils::{Address as _, Ledger}, token, Address, Bytes, BytesN, Env, Vec as SorobanVec};

use crate::invariants_checker::*;
pub use crate::types::ProjectStatus;
Expand Down Expand Up @@ -83,6 +83,7 @@ proptest! {
&dummy_metadata_uri(&env),
&deadline,
&false,
&0u32,
);

check_all_project_invariants(&env, &project);
Expand Down Expand Up @@ -112,6 +113,7 @@ proptest! {
&dummy_metadata_uri(&env),
&deadline,
&false,
&0u32,
);

check_all_project_invariants(&env, &project);
Expand Down Expand Up @@ -140,6 +142,7 @@ proptest! {
&dummy_metadata_uri(&env),
&deadline,
&false,
&0u32,
);

check_all_project_invariants(&env, &project);
Expand Down Expand Up @@ -174,6 +177,7 @@ proptest! {
&dummy_metadata_uri(&env),
&deadline,
&false,
&0u32,
);

let donator = Address::generate(&env);
Expand Down Expand Up @@ -213,6 +217,7 @@ proptest! {
&dummy_metadata_uri(&env),
&deadline,
&false,
&0u32,
);

let sac = token::StellarAssetClient::new(&env, &token_client.address);
Expand Down Expand Up @@ -271,14 +276,15 @@ proptest! {
&dummy_metadata_uri(&env),
&deadline,
&false,
&0u32,
);

let oracle = Address::generate(&env);
client.set_oracle(&admin, &oracle);

let wrong_hash = BytesN::from_array(&env, &submitted_bytes);
let result = client.try_verify_and_release(&oracle, &project.id, &wrong_hash);
prop_assert!(result.is_err(), "verify_and_release should fail with wrong hash");
let result = client.try_verify_proof(&oracle, &project.id, &wrong_hash);
prop_assert!(result.is_err(), "verify_proof should fail with wrong hash");
}

#[test]
Expand All @@ -305,16 +311,17 @@ proptest! {
&dummy_metadata_uri(&env),
&deadline,
&false,
&0u32,
);

let oracle = Address::generate(&env);
client.set_oracle(&admin, &oracle);

client.verify_and_release(&oracle, &project.id, &proof_hash);
client.verify_proof(&oracle, &project.id, &proof_hash);

let updated = client.get_project(&project.id);
check_inv7_status_transition(&ProjectStatus::Funding, &updated.status);
assert_eq!(updated.status, ProjectStatus::Completed);
assert_eq!(updated.status, ProjectStatus::Verified);
}
}

Expand Down Expand Up @@ -347,6 +354,7 @@ proptest! {
&dummy_metadata_uri(&env),
&deadline,
&false,
&0u32,
);
projects.push(p);
}
Expand Down Expand Up @@ -386,6 +394,7 @@ proptest! {
&dummy_metadata_uri(&env),
&deadline,
&false,
&0u32,
);

let donator = Address::generate(&env);
Expand Down Expand Up @@ -421,11 +430,12 @@ proptest! {
&dummy_metadata_uri(&env),
&deadline,
&false,
&0u32,
);

let oracle = Address::generate(&env);
client.set_oracle(&admin, &oracle);
client.verify_and_release(&oracle, &original.id, &proof_hash);
client.verify_proof(&oracle, &original.id, &proof_hash);

let after = client.get_project(&original.id);
check_inv10_config_immutable(&original, &after);
Expand Down Expand Up @@ -465,6 +475,7 @@ proptest! {
&dummy_metadata_uri(&env),
&deadline,
&false,
&0u32,
);
check_all_project_invariants(&env, &project);
assert_eq!(project.status, ProjectStatus::Funding);
Expand Down Expand Up @@ -496,16 +507,23 @@ proptest! {
// Phase 3: Oracle verification.
let oracle = Address::generate(&env);
client.set_oracle(&admin, &oracle);
client.verify_and_release(&oracle, &project.id, &proof_hash);
client.verify_proof(&oracle, &project.id, &proof_hash);

// Advance time past 24h grace period
let mut ledger = env.ledger().get();
ledger.timestamp += 86_400;
env.ledger().set(ledger);

client.claim_funds(&project.id);

let final_project = client.get_project(&project.id);
check_inv7_status_transition(&ProjectStatus::Funding, &final_project.status);
check_inv10_config_immutable(&project, &final_project);
assert_eq!(final_project.status, ProjectStatus::Completed);

// Phase 4: Balance verification after verification.
// Balance should be zero after verification — funds are drained and
// transferred to the creator during verify_and_release.
// Phase 4: Balance verification after claiming.
// Balance should be zero after claiming — funds are drained and
// transferred to the creator during claim_funds.
let post_verify_balance = client.get_balance(&project.id, &token_client.address);
assert_eq!(post_verify_balance, 0i128);

Expand All @@ -514,7 +532,7 @@ proptest! {
assert_eq!(creator_actual_balance, total_deposited);

// Phase 5: Double-verify should fail.
let result = client.try_verify_and_release(&oracle, &project.id, &proof_hash);
let result = client.try_verify_proof(&oracle, &project.id, &proof_hash);
prop_assert!(result.is_err(), "double verification should fail");
}
}
Expand Down Expand Up @@ -545,6 +563,8 @@ proptest! {
&proof_hash,
&dummy_metadata_uri(&env),
&deadline,
&false,
&0u32,
);

let donator = Address::generate(&env);
Expand All @@ -568,7 +588,7 @@ proptest! {
client.set_oracle(&admin, &oracle);

let wrong_hash = BytesN::from_array(&env, &hash_bytes);
let result = client.try_verify_and_release(&oracle, &fake_id, &wrong_hash);
let result = client.try_verify_proof(&oracle, &fake_id, &wrong_hash);
prop_assert!(result.is_err(), "verify on non-existent project {} should fail", fake_id);
}

Expand Down Expand Up @@ -623,18 +643,20 @@ proptest! {
&proof_hash,
&dummy_metadata_uri(&env),
&deadline,
&false,
&0u32,
);

let oracle = Address::generate(&env);
client.set_oracle(&admin, &oracle);

let wrong = BytesN::from_array(&env, &submitted);
let result = client.try_verify_and_release(&oracle, &project.id, &wrong);
let result = client.try_verify_proof(&oracle, &project.id, &wrong);
prop_assert!(result.is_err(), "wrong hash should never bypass verification");

// Correct hash must still succeed.
client.verify_and_release(&oracle, &project.id, &proof_hash);
client.verify_proof(&oracle, &project.id, &proof_hash);
let updated = client.get_project(&project.id);
assert_eq!(updated.status, ProjectStatus::Completed);
assert_eq!(updated.status, ProjectStatus::Verified);
}
}
4 changes: 4 additions & 0 deletions contracts/pifp_protocol/src/invariants_checker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
//! as defined in ARCHITECTURE.md. These checkers are used both in fuzz tests
//! and can be triggered as post-execution assertions in debug builds.

use crate::rbac::get_super_admin;
use crate::types::{Project, ProjectStatus};
use soroban_sdk::{Address, Env, Vec};

Expand Down Expand Up @@ -74,11 +75,14 @@ pub fn check_inv7_status_transition(from: &ProjectStatus, to: &ProjectStatus) {
let valid = matches!(
(from, to),
(ProjectStatus::Funding, ProjectStatus::Active)
| (ProjectStatus::Funding, ProjectStatus::Verified)
| (ProjectStatus::Funding, ProjectStatus::Completed)
| (ProjectStatus::Funding, ProjectStatus::Expired)
| (ProjectStatus::Active, ProjectStatus::Verified)
| (ProjectStatus::Active, ProjectStatus::Completed)
| (ProjectStatus::Active, ProjectStatus::Expired)
| (ProjectStatus::Active, ProjectStatus::Cancelled)
| (ProjectStatus::Verified, ProjectStatus::Completed)
);

assert!(
Expand Down
Loading
Loading