diff --git a/Documents/Task Bounty/src/lib.rs b/Documents/Task Bounty/src/lib.rs index 1bec81c..e79dd56 100644 --- a/Documents/Task Bounty/src/lib.rs +++ b/Documents/Task Bounty/src/lib.rs @@ -12,18 +12,18 @@ //! - Dispute resolution //! - Multi-token support (XLM and SAC tokens) -mod types; -mod storage; -mod task; -mod submission; mod dispute; mod events; +mod storage; +mod submission; +mod task; +mod types; #[cfg(test)] mod test; use soroban_sdk::{contract, contractimpl, Address, Env, String, Vec}; -use types::{Task, Submission, TaskStatus, SubmissionStatus}; +use types::{Submission, SubmissionStatus, Task, TaskStatus}; #[contract] pub struct TaskBountyContract; @@ -101,18 +101,13 @@ impl TaskBountyContract { submission::submit_work(&env, task_id, contributor, work_url, description) } - /// Approve a submission and release payment + /// Approve a submission and release a proportional reward share /// /// # Arguments /// * `task_id` - ID of the task /// * `submission_id` - ID of the submission to approve /// * `poster` - Address of the task poster (for auth) - pub fn approve_submission( - env: Env, - task_id: u64, - submission_id: u64, - poster: Address, - ) { + pub fn approve_submission(env: Env, task_id: u64, submission_id: u64, poster: Address) { poster.require_auth(); submission::approve_submission(&env, task_id, submission_id, poster) diff --git a/Documents/Task Bounty/src/submission.rs b/Documents/Task Bounty/src/submission.rs index 57f6561..8749f1d 100644 --- a/Documents/Task Bounty/src/submission.rs +++ b/Documents/Task Bounty/src/submission.rs @@ -1,7 +1,7 @@ -use soroban_sdk::{Address, Env, String, token}; -use crate::types::{Submission, SubmissionStatus, TaskStatus, Error}; -use crate::storage; use crate::events; +use crate::storage; +use crate::types::{Error, Submission, SubmissionStatus, TaskStatus}; +use soroban_sdk::{token, Address, Env, String}; /// Submit work for a task pub fn submit_work( @@ -68,12 +68,7 @@ pub fn submit_work( } /// Approve a submission and release payment -pub fn approve_submission( - env: &Env, - task_id: u64, - submission_id: u64, - poster: Address, -) { +pub fn approve_submission(env: &Env, task_id: u64, submission_id: u64, poster: Address) { // Get task if !storage::task_exists(env, task_id) { panic_with_error!(env, Error::TaskNotFound); @@ -115,7 +110,13 @@ pub fn approve_submission( // Update statuses submission.status = SubmissionStatus::Approved; - task.status = TaskStatus::Completed; + task.approved_count += 1; + + if task.approved_count >= task.max_submissions { + task.status = TaskStatus::Completed; + } else { + task.status = TaskStatus::InProgress; + } storage::set_submission(env, &submission); storage::set_task(env, &task); @@ -123,16 +124,17 @@ pub fn approve_submission( // Transfer reward to contributor let contract_address = env.current_contract_address(); let token_client = token::Client::new(env, &task.token); - token_client.transfer(&contract_address, &submission.contributor, &task.reward); + let approved_slots = task.max_submissions as i128; + let base_payout = task.reward / approved_slots; + let payout = if task.approved_count >= task.max_submissions { + task.reward - (base_payout * (approved_slots - 1)) + } else { + base_payout + }; + token_client.transfer(&contract_address, &submission.contributor, &payout); // Emit event - events::emit_submission_approved( - env, - task_id, - submission_id, - &submission.contributor, - task.reward, - ); + events::emit_submission_approved(env, task_id, submission_id, &submission.contributor, payout); } /// Reject a submission diff --git a/Documents/Task Bounty/src/task.rs b/Documents/Task Bounty/src/task.rs index b37b85e..1c9fd83 100644 --- a/Documents/Task Bounty/src/task.rs +++ b/Documents/Task Bounty/src/task.rs @@ -1,7 +1,7 @@ -use soroban_sdk::{Address, Env, String, token}; -use crate::types::{Task, TaskStatus, Error}; -use crate::storage; use crate::events; +use crate::storage; +use crate::types::{Error, Task, TaskStatus}; +use soroban_sdk::{token, Address, Env, String}; const MIN_REWARD: i128 = 1_000_000; // 0.1 XLM (7 decimals) const MAX_DEADLINE: u64 = 31_536_000; // 365 days in seconds @@ -52,6 +52,7 @@ pub fn create_task( deadline, max_submissions, submission_count: 0, + approved_count: 0, status: TaskStatus::Open, created_at: current_time, }; diff --git a/Documents/Task Bounty/src/test.rs b/Documents/Task Bounty/src/test.rs index 38dc888..41d2456 100644 --- a/Documents/Task Bounty/src/test.rs +++ b/Documents/Task Bounty/src/test.rs @@ -5,14 +5,21 @@ use soroban_sdk::{ testutils::{Address as _, Ledger, LedgerInfo}, token, Address, Env, String, }; -use types::{TaskStatus, SubmissionStatus}; +use types::{SubmissionStatus, TaskStatus}; fn create_token_contract<'a>(env: &Env, admin: &Address) -> token::Client<'a> { let token_address = env.register_stellar_asset_contract(admin.clone()); token::Client::new(env, &token_address) } -fn setup_test() -> (Env, Address, Address, Address, token::Client<'static>, Address) { +fn setup_test() -> ( + Env, + Address, + Address, + Address, + token::Client<'static>, + Address, +) { let env = Env::default(); env.mock_all_auths(); @@ -63,6 +70,7 @@ fn test_create_task() { assert_eq!(task.reward, reward); assert_eq!(task.deadline, deadline); assert_eq!(task.max_submissions, 3); + assert_eq!(task.approved_count, 0); assert_eq!(task.status, TaskStatus::Open); } @@ -231,7 +239,10 @@ fn test_approve_submission() { // Check payment let contributor_balance_after = token_client.balance(&contributor); - assert_eq!(contributor_balance_after, contributor_balance_before + reward); + assert_eq!( + contributor_balance_after, + contributor_balance_before + reward + ); // Check statuses let task = client.get_task(&task_id); @@ -241,6 +252,69 @@ fn test_approve_submission() { assert_eq!(submission.status, SubmissionStatus::Approved); } +#[test] +fn test_multiple_winners_share_reward() { + let (env, poster, contributor, _, token_client, contract_id) = setup_test(); + let client = TaskBountyContractClient::new(&env, &contract_id); + + let reward = 9_000_000; + + let task_id = client.create_task( + &poster, + &String::from_str(&env, "Task"), + &String::from_str(&env, "Description"), + &token_client.address, + &reward, + &(env.ledger().timestamp() + 86400), + &3, + ); + + let contributor2 = Address::generate(&env); + let contributor3 = Address::generate(&env); + + let sub1 = client.submit_work( + &task_id, + &contributor, + &String::from_str(&env, "ipfs://1"), + &String::from_str(&env, "Work 1"), + ); + let sub2 = client.submit_work( + &task_id, + &contributor2, + &String::from_str(&env, "ipfs://2"), + &String::from_str(&env, "Work 2"), + ); + let sub3 = client.submit_work( + &task_id, + &contributor3, + &String::from_str(&env, "ipfs://3"), + &String::from_str(&env, "Work 3"), + ); + + let share = reward / 3; + let first_before = token_client.balance(&contributor); + let second_before = token_client.balance(&contributor2); + let third_before = token_client.balance(&contributor3); + + client.approve_submission(&task_id, &sub1, &poster); + let task_after_first = client.get_task(&task_id); + assert_eq!(task_after_first.status, TaskStatus::InProgress); + assert_eq!(task_after_first.approved_count, 1); + assert_eq!(token_client.balance(&contributor), first_before + share); + + client.approve_submission(&task_id, &sub2, &poster); + let task_after_second = client.get_task(&task_id); + assert_eq!(task_after_second.status, TaskStatus::InProgress); + assert_eq!(task_after_second.approved_count, 2); + assert_eq!(token_client.balance(&contributor2), second_before + share); + + client.approve_submission(&task_id, &sub3, &poster); + let task_after_third = client.get_task(&task_id); + assert_eq!(task_after_third.status, TaskStatus::Completed); + assert_eq!(task_after_third.approved_count, 3); + assert_eq!(token_client.balance(&contributor3), third_before + share); +} + #[test] fn test_reject_submission() { let (env, poster, contributor, _, token_client, contract_id) = setup_test(); diff --git a/Documents/Task Bounty/src/types.rs b/Documents/Task Bounty/src/types.rs index 05e40d6..6cb3284 100644 --- a/Documents/Task Bounty/src/types.rs +++ b/Documents/Task Bounty/src/types.rs @@ -28,13 +28,14 @@ pub struct Task { pub poster: Address, pub title: String, pub description: String, - pub token: Address, // Token address for reward - pub reward: i128, // Reward amount - pub deadline: u64, // Unix timestamp + pub token: Address, // Token address for reward + pub reward: i128, // Reward amount + pub deadline: u64, // Unix timestamp pub max_submissions: u32, pub submission_count: u32, + pub approved_count: u32, pub status: TaskStatus, - pub created_at: u64, // Unix timestamp + pub created_at: u64, // Unix timestamp } /// Submission structure @@ -44,9 +45,9 @@ pub struct Submission { pub id: u64, pub task_id: u64, pub contributor: Address, - pub work_url: String, // IPFS, Arweave, GitHub, etc. + pub work_url: String, // IPFS, Arweave, GitHub, etc. pub description: String, - pub submitted_at: u64, // Unix timestamp + pub submitted_at: u64, // Unix timestamp pub status: SubmissionStatus, }