diff --git a/contracts/grant_stream/src/lib.rs b/contracts/grant_stream/src/lib.rs index 9eba55f7..a3a1329f 100644 --- a/contracts/grant_stream/src/lib.rs +++ b/contracts/grant_stream/src/lib.rs @@ -1856,7 +1856,21 @@ impl GrantStreamContract { return Err(Error::InvalidNonce); } + let native_token_addr: Address = env.storage().instance().get(&StorageKey::NativeToken).ok_or(Error::NotInitialized)?; + let native_client = token::Client::new(&env, &native_token_addr); + native_client.transfer( + &grant.recipient, + &env.current_contract_address(), + &MILESTONE_SUBMISSION_DEPOSIT_XLM, + ); + env.storage().persistent().set(&milestone_key, &proof); + set_milestone_submission_deposit( + &env, + grant_id, + milestone_index, + MILESTONE_SUBMISSION_DEPOSIT_XLM, + ); let next_nonce = nonce.checked_add(1).ok_or(Error::MathOverflow)?; write_expected_milestone_nonce(&env, grant_id, next_nonce); @@ -1869,6 +1883,52 @@ impl GrantStreamContract { Ok(()) } + /// Admin-only: approve a milestone submission and refund its anti-spam deposit. + pub fn approve_milestone_submission( + env: Env, + grant_id: u64, + milestone_index: u32, + ) -> Result<(), Error> { + require_admin_auth(&env)?; + let grant = read_grant(&env, grant_id)?; + let deposit = get_milestone_submission_deposit(&env, grant_id, milestone_index) + .ok_or(Error::SubmissionDepositNotFound)?; + let native_token_addr: Address = env.storage().instance().get(&StorageKey::NativeToken).ok_or(Error::NotInitialized)?; + let native_client = token::Client::new(&env, &native_token_addr); + native_client.transfer(&env.current_contract_address(), &grant.recipient, &deposit); + env.storage() + .instance() + .remove(&DataKey::MilestoneSubmissionDeposit(grant_id, milestone_index)); + env.events().publish( + (symbol_short!("mil_depr"),), + (grant_id, milestone_index, deposit), + ); + Ok(()) + } + + /// Admin-only: slash a fraudulent milestone submission deposit to treasury. + pub fn slash_milestone_submission_deposit( + env: Env, + grant_id: u64, + milestone_index: u32, + ) -> Result<(), Error> { + require_admin_auth(&env)?; + let deposit = get_milestone_submission_deposit(&env, grant_id, milestone_index) + .ok_or(Error::SubmissionDepositNotFound)?; + let treasury = read_treasury(&env)?; + let native_token_addr: Address = env.storage().instance().get(&StorageKey::NativeToken).ok_or(Error::NotInitialized)?; + let native_client = token::Client::new(&env, &native_token_addr); + native_client.transfer(&env.current_contract_address(), &treasury, &deposit); + env.storage() + .instance() + .remove(&DataKey::MilestoneSubmissionDeposit(grant_id, milestone_index)); + env.events().publish( + (symbol_short!("mil_deps"),), + (grant_id, milestone_index, deposit), + ); + Ok(()) + } + pub fn emit_grant_status(env: Env, grant_id: u64) -> Result { let mut grant = read_grant(&env, grant_id)?; settle_grant(&mut grant, env.ledger().timestamp())?; diff --git a/contracts/grant_stream/src/test.rs b/contracts/grant_stream/src/test.rs index 9d1b2c3a..9c0f58ed 100644 --- a/contracts/grant_stream/src/test.rs +++ b/contracts/grant_stream/src/test.rs @@ -72,6 +72,62 @@ fn test_pipeline() { client.create_grant(&grant_id, &recipient, &total_amount, &flow_rate, &warmup_duration, &None, &None); } +#[test] +fn test_milestone_submission_deposit_refunded_on_approval() { + let env = Env::default(); + env.mock_all_auths(); + let (_admin, _grant_token_addr, _treasury, _oracle, native_token_addr, client) = setup_test(&env); + let recipient = Address::generate(&env); + let native_token = token::Client::new(&env, &native_token_addr); + let native_token_admin = token::StellarAssetClient::new(&env, &native_token_addr); + let grant_id = 77u64; + let deposit = 100_000i128; + + native_token_admin.mint(&recipient, &1_000_000i128); + client.create_grant(&grant_id, &recipient, &1_000_000i128, &1_000i128, &0u64, &None, &None); + + let recipient_before = native_token.balance(&recipient); + let contract_before = native_token.balance(&client.address); + client.submit_milestone_proof(&grant_id, &0u32, &Symbol::new(&env, "m0"), &0u64).unwrap(); + let recipient_after_submit = native_token.balance(&recipient); + let contract_after_submit = native_token.balance(&client.address); + + assert_eq!(recipient_after_submit, recipient_before - deposit); + assert_eq!(contract_after_submit, contract_before + deposit); + + client.approve_milestone_submission(&grant_id, &0u32).unwrap(); + let recipient_after_approval = native_token.balance(&recipient); + let contract_after_approval = native_token.balance(&client.address); + + assert_eq!(recipient_after_approval, recipient_before); + assert_eq!(contract_after_approval, contract_before); +} + +#[test] +fn test_milestone_submission_deposit_slashed_to_treasury() { + let env = Env::default(); + env.mock_all_auths(); + let (_admin, _grant_token_addr, treasury, _oracle, native_token_addr, client) = setup_test(&env); + let recipient = Address::generate(&env); + let native_token = token::Client::new(&env, &native_token_addr); + let native_token_admin = token::StellarAssetClient::new(&env, &native_token_addr); + let grant_id = 78u64; + let deposit = 100_000i128; + + native_token_admin.mint(&recipient, &1_000_000i128); + client.create_grant(&grant_id, &recipient, &1_000_000i128, &1_000i128, &0u64, &None, &None); + + let treasury_before = native_token.balance(&treasury); + let contract_before = native_token.balance(&client.address); + client.submit_milestone_proof(&grant_id, &0u32, &Symbol::new(&env, "m1"), &0u64).unwrap(); + client.slash_milestone_submission_deposit(&grant_id, &0u32).unwrap(); + + let treasury_after = native_token.balance(&treasury); + let contract_after = native_token.balance(&client.address); + assert_eq!(treasury_after, treasury_before + deposit); + assert_eq!(contract_after, contract_before); +} + #[test] fn test_is_active_grantee_basic_functionality() { let env = Env::default();