Skip to content
79 changes: 77 additions & 2 deletions campaign/src/multi_asset_release.rs
Original file line number Diff line number Diff line change
Expand Up @@ -178,8 +178,10 @@ pub fn release_milestone_multi_asset(env: &Env, milestone_index: u32, recipient:
// Update per-asset accounting
let new_asset_raised = asset_raised
.checked_sub(clamped_release)
.unwrap_or(0)
.max(0);
.unwrap_or_else(|| panic_with_error!(env, Error::Overflow));
if new_asset_raised < 0 {
panic_with_error!(env, Error::Overflow);
}
storage_set_asset_raised(env, &token_address, new_asset_raised);

total_released = total_released
Expand All @@ -188,6 +190,12 @@ pub fn release_milestone_multi_asset(env: &Env, milestone_index: u32, recipient:
}

// ── 8. Update global total-raised bookkeeping ────────────────────────────
let new_total_raised = total_raised
.checked_sub(total_released)
.unwrap_or_else(|| panic_with_error!(env, Error::Overflow));
if new_total_raised < 0 {
panic_with_error!(env, Error::Overflow);
}
let new_total_raised = total_raised.checked_sub(total_released).unwrap_or(0).max(0);
storage_set_total_raised(env, new_total_raised);
storage_increment_release_count(env);
Expand Down Expand Up @@ -249,4 +257,71 @@ mod tests {
let result = compute_asset_release(-100, 1000, 1000);
assert_eq!(result, None);
}

#[test]
#[should_panic(expected = "HostError")]
fn test_release_underflow_panics() {
use crate::storage::{
set_campaign, set_milestone, storage_set_asset_raised, storage_set_total_raised,
};
use crate::types::{
CampaignData, CampaignStatus, MilestoneData, MilestoneStatus, StellarAsset,
};
use soroban_sdk::token::StellarAssetClient;
use soroban_sdk::{testutils::Address as _, Address, Env, String, Vec};

let env = Env::default();
env.mock_all_auths();

let creator = Address::generate(&env);
let recipient = Address::generate(&env);

let token_admin = Address::generate(&env);
let token_issuer = env.register_stellar_asset_contract(token_admin.clone());
let token_client = StellarAssetClient::new(&env, &token_issuer);
token_client.mint(&env.current_contract_address(), &5000);

let mut assets = Vec::new(&env);
assets.push_back(StellarAsset {
asset_code: String::from_str(&env, "USDC"),
issuer: Some(token_issuer.clone()),
});

let campaign = CampaignData {
creator,
goal_amount: 3000,
raised_amount: 3000,
end_time: env.ledger().timestamp() + 86400,
status: CampaignStatus::Active,
accepted_assets: assets,
milestone_count: 1,
min_donation_amount: 0,
created_at_ledger: 0,
created_at_time: 0,
concluded_at_ledger: None,
};
set_campaign(&env, &campaign);

let milestone = MilestoneData {
index: 0,
target_amount: 3000, // milestone_release = 3000
released_amount: 0,
description_hash: soroban_sdk::BytesN::from_array(&env, &[0; 32]),
status: MilestoneStatus::Unlocked,
released_at: None,
released_at_ledger: None,
release_tx: None,
released_to: None,
};
set_milestone(&env, 0, &milestone);

// Force underflow condition: total_raised = 1000, asset_raised = 500
// asset_release = 500 * 3000 / 1000 = 1500
// clamped_release = min(1500, 5000) = 1500
// new_asset_raised = 500 - 1500 => underflow!
storage_set_total_raised(&env, 1000);
storage_set_asset_raised(&env, &token_issuer, 500);

release_milestone_multi_asset(&env, 0, recipient);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
{
"generators": {
"address": 4,
"nonce": 0,
"mux_id": 0
},
"auth": [
[],
[
[
"GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJXFF",
{
"function": {
"contract_fn": {
"contract_address": "CCABDO7UZXYE4W6GVSEGSNNZTKSLFQGKXXQTH6OX7M7GKZ4Z6CUJNGZN",
"function_name": "set_admin",
"args": [
{
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
}
]
}
},
"sub_invocations": []
}
]
]
],
"ledger": {
"protocol_version": 26,
"sequence_number": 0,
"timestamp": 0,
"network_id": "0000000000000000000000000000000000000000000000000000000000000000",
"base_reserve": 0,
"min_persistent_entry_ttl": 4096,
"min_temp_entry_ttl": 16,
"max_entry_ttl": 6312000,
"ledger_entries": [
{
"entry": {
"last_modified_ledger_seq": 0,
"data": {
"account": {
"account_id": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJXFF",
"balance": "0",
"seq_num": "0",
"num_sub_entries": 0,
"inflation_dest": null,
"flags": 0,
"home_domain": "",
"thresholds": "01010101",
"signers": [],
"ext": "v0"
}
},
"ext": "v0"
},
"live_until": null
},
{
"entry": {
"last_modified_ledger_seq": 0,
"data": {
"contract_data": {
"ext": "v0",
"contract": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJXFF",
"key": {
"ledger_key_nonce": {
"nonce": "801925984706572462"
}
},
"durability": "temporary",
"val": "void"
}
},
"ext": "v0"
},
"live_until": 6311999
},
{
"entry": {
"last_modified_ledger_seq": 0,
"data": {
"contract_data": {
"ext": "v0",
"contract": "CCABDO7UZXYE4W6GVSEGSNNZTKSLFQGKXXQTH6OX7M7GKZ4Z6CUJNGZN",
"key": "ledger_key_contract_instance",
"durability": "persistent",
"val": {
"contract_instance": {
"executable": "stellar_asset",
"storage": [
{
"key": {
"symbol": "METADATA"
},
"val": {
"map": [
{
"key": {
"symbol": "decimal"
},
"val": {
"u32": 7
}
},
{
"key": {
"symbol": "name"
},
"val": {
"string": "aaa:GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJXFF"
}
},
{
"key": {
"symbol": "symbol"
},
"val": {
"string": "aaa"
}
}
]
}
},
{
"key": {
"vec": [
{
"symbol": "Admin"
}
]
},
"val": {
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
}
},
{
"key": {
"vec": [
{
"symbol": "AssetInfo"
}
]
},
"val": {
"vec": [
{
"symbol": "AlphaNum4"
},
{
"map": [
{
"key": {
"symbol": "asset_code"
},
"val": {
"string": "aaa\\0"
}
},
{
"key": {
"symbol": "issuer"
},
"val": {
"bytes": "0000000000000000000000000000000000000000000000000000000000000004"
}
}
]
}
]
}
}
]
}
}
}
},
"ext": "v0"
},
"live_until": 120960
}
]
},
"events": [
{
"event": {
"ext": "v0",
"contract_id": "CCABDO7UZXYE4W6GVSEGSNNZTKSLFQGKXXQTH6OX7M7GKZ4Z6CUJNGZN",
"type_": "contract",
"body": {
"v0": {
"topics": [
{
"symbol": "set_admin"
},
{
"address": "GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJXFF"
},
{
"string": "aaa:GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJXFF"
}
],
"data": {
"address": "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAHK3M"
}
}
}
},
"failed_call": false
}
]
}
Loading
Loading