Skip to content
Open
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
75 changes: 71 additions & 4 deletions campaign/src/multi_asset_release.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,8 +184,10 @@ pub fn release_milestone_multi_asset(
// 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::LedgerUnderflow));
if new_asset_raised < 0 {
panic_with_error!(env, Error::LedgerUnderflow);
}
storage_set_asset_raised(env, &token_address, new_asset_raised);

total_released = total_released
Expand All @@ -196,8 +198,10 @@ pub fn release_milestone_multi_asset(
// ── 8. Update global total-raised bookkeeping ────────────────────────────
let new_total_raised = total_raised
.checked_sub(total_released)
.unwrap_or(0)
.max(0);
.unwrap_or_else(|| panic_with_error!(env, Error::LedgerUnderflow));
if new_total_raised < 0 {
panic_with_error!(env, Error::LedgerUnderflow);
}
storage_set_total_raised(env, new_total_raised);
storage_increment_release_count(env);

Expand Down Expand Up @@ -258,4 +262,67 @@ 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::types::{CampaignData, CampaignStatus, MilestoneData, MilestoneStatus, StellarAsset};
use crate::storage::{set_campaign, set_milestone, storage_set_asset_raised, storage_set_total_raised};
use soroban_sdk::{testutils::Address as _, Address, Env, String, Vec};
use soroban_sdk::token::StellarAssetClient;

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);
}
}
9 changes: 9 additions & 0 deletions campaign/src/test/scratch_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
use soroban_sdk::{Env, Address};
use crate::test::release_milestone_tests::*;

#[test]
fn scratch() {
let env = Env::default();
env.mock_all_auths();
// ...
}
16 changes: 16 additions & 0 deletions campaign/src/test_token_scratch.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#[cfg(test)]
mod test {
use soroban_sdk::{Env, Address, token::StellarAssetClient, token::Client};
#[test]
fn test_mint() {
let env = Env::default();
env.mock_all_auths();
let admin = Address::generate(&env);
let token = env.register_stellar_asset_contract_v2(admin).address();
let admin_client = StellarAssetClient::new(&env, &token);
let user = Address::generate(&env);
admin_client.mint(&user, &1000);
let client = Client::new(&env, &token);
assert_eq!(client.balance(&user), 1000);
}
}
2 changes: 2 additions & 0 deletions campaign/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ pub enum Error {
// ── Upgrade / freeze ─────────────────────────────────────────────────── 8x
/// Contract is frozen; all mutating operations are blocked.
ContractFrozen = 80,
/// A ledger decrement operation underflowed, indicating an invariant violation.
LedgerUnderflow = 81,
}


Expand Down
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