Overview
DonorRecord::apply_donation (campaign/src/types.rs lines 451–462) uses saturating_add for both total_donated and donation_count. The rest of the contract consistently uses checked_add(...).unwrap_or_else(|| panic_with_error(&env, Error::Overflow)). The saturating variant silently caps total_donated at i128::MAX for any donor who manages to push past the cap, after which every subsequent refund and proportional calculation is wrong (it always reports "fully refunded, no remaining balance"). The whole campaign contract freezes on the canonical Error path elsewhere; the inconsistency is a footgun.
Evidence
// campaign/src/types.rs (impl DonorRecord)
pub fn apply_donation(&mut self, amount: i128, time: u64, ledger: u32, asset: AssetInfo) {
self.total_donated = self.total_donated.saturating_add(amount);
self.last_donation_time = time;
self.last_donation_ledger = ledger;
self.donation_count = self.donation_count.saturating_add(1);
self.asset = asset;
}
Compare with campaign/src/lib.rs::donate line 196:
campaign.raised_amount = campaign
.raised_amount
.checked_add(amount)
.unwrap_or_else(|| panic_with_error(&env, Error::Overflow));
And storage_increment_total_raised in storage.rs line 199:
let new_total = current
.checked_add(delta)
.unwrap_or_else(|| panic_with_error!(env, Error::Overflow));
Impact
- Saturating
total_donated masks overflow. Once the donor's ledger hits i128::MAX, every later state read is silently wrong.
- The refund math (campaign/src/lib.rs
claim_refund line 401) uses this value: a saturated donor would always see refund = 0 for any further donation, defeating pro-rata refunds entirely.
- Off-chain indexers that query
DonorRecord would have to special-case the saturation; indexed UIs would silently misreport totals.
- Violates the project-wide "fail loudly on arithmetic overflow" convention documented in
Error::Overflow.
Recommended Approach
Either delete apply_donation entirely (it is not currently called by donate — see #38) or replace the saturating lines with typed overflow handling that requires an &Env so it can panic with Error::Overflow:
pub fn apply_donation(&mut self, env: &Env, amount: i128, time: u64, ledger: u32, asset: AssetInfo) {
self.total_donated = self.total_donated
.checked_add(amount)
.unwrap_or_else(|| panic_with_error!(env, Error::Overflow));
self.last_donation_time = time;
self.last_donation_ledger = ledger;
self.donation_count = self.donation_count
.checked_add(1)
.unwrap_or_else(|| panic_with_error!(env, Error::Overflow));
self.asset = asset;
}
If kept, this also fixes the donation_count saturation at the same time (see #49).
Acceptance Criteria
Affected Files
campaign/src/types.rs
campaign/src/lib.rs (if apply_donation is wired in)
campaign/src/test/integration_tests.rs or a new overflow test
Overview
DonorRecord::apply_donation(campaign/src/types.rs lines 451–462) usessaturating_addfor bothtotal_donatedanddonation_count. The rest of the contract consistently useschecked_add(...).unwrap_or_else(|| panic_with_error(&env, Error::Overflow)). The saturating variant silently capstotal_donatedati128::MAXfor any donor who manages to push past the cap, after which every subsequent refund and proportional calculation is wrong (it always reports "fully refunded, no remaining balance"). The whole campaign contract freezes on the canonical Error path elsewhere; the inconsistency is a footgun.Evidence
Compare with
campaign/src/lib.rs::donateline 196:And
storage_increment_total_raisedin storage.rs line 199:Impact
total_donatedmasks overflow. Once the donor's ledger hitsi128::MAX, every later state read is silently wrong.claim_refundline 401) uses this value: a saturated donor would always seerefund = 0for any further donation, defeating pro-rata refunds entirely.DonorRecordwould have to special-case the saturation; indexed UIs would silently misreport totals.Error::Overflow.Recommended Approach
Either delete
apply_donationentirely (it is not currently called bydonate— see #38) or replace the saturating lines with typed overflow handling that requires an&Envso it can panic withError::Overflow:If kept, this also fixes the
donation_countsaturation at the same time (see #49).Acceptance Criteria
DonorRecord::apply_donationno longer usessaturating_addfor totalsError::Overflowpanicdonate()test paths still pass (whether or notapply_donationis plugged in)total_donatedto i128::MAXAffected Files
campaign/src/types.rscampaign/src/lib.rs(ifapply_donationis wired in)campaign/src/test/integration_tests.rsor a new overflow test