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
60 changes: 60 additions & 0 deletions contracts/grant_stream/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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<soroban_sdk::Bytes, Error> {
let mut grant = read_grant(&env, grant_id)?;
settle_grant(&mut grant, env.ledger().timestamp())?;
Expand Down
56 changes: 56 additions & 0 deletions contracts/grant_stream/src/test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading