Skip to content

Campaign raisedAmount sums heterogeneous assets: multi-asset campaigns report corrupted totals #10

Description

@P3az3

Labels / Complexity: bug · High — 600 points

Problem

A Campaign has a single raisedAmount Decimal column (prisma/schema.prisma), yet it can accept multiple assets: acceptedAssets is parsed into { assetType: 'native' } | { assetType: 'credit', code, issuer } entries (parseAcceptedAssets in src/campaigns/campaigns.service.ts) and Donation.assetCode/assetIssuer store whatever asset the donor sent. Two code paths then collapse those distinct assets into one number:

recalculateCampaignStats (src/campaigns/campaigns.service.ts) sums donation amount across every confirmed donation with no asset filter:

const agg = await tx.donation.aggregate({
  where: { campaignId, status: 'CONFIRMED' },
  _sum: { amount: true },          // ← sums XLM + USDC + ... into one Decimal
});

getContractBalance (src/campaigns/campaigns.service.ts) does the same with on-chain balances and then overwrites the stored value when the numbers differ:

let onChainTotal = 0;
for (const b of balances) {
  onChainTotal += parseFloat(b.balance);   // ← native + every credit asset summed together
}
...
if (discrepancyDetected) {
  await this.prisma.campaign.update({ where: { id: campaignId }, data: { raisedAmount: onChainTotal } });
}

Consequence: for a campaign that accepts XLM and USDC, raisedAmount, progressPercentage, totalRaised, and the "most funded" sort (orderBy: { raisedAmount: 'desc' } in browseCampaigns) become a meaningless sum of unlike units. The auto-correction in getContractBalance actively persists that corruption, so a 100 XLM + 50 USDC campaign reports a "raised" total of 150 with no unit. Donors and creators see a number that does not correspond to any real monetary value.

Root cause

// src/campaigns/campaigns.service.ts — recalculateCampaignStats
_sum: { amount: true }            // ← no asset dimension in the WHERE or the aggregation
// src/campaigns/campaigns.service.ts — getContractBalance
onChainTotal += parseFloat(b.balance);   // ← XLM and USDC balances added arithmetically

Why this is architecturally hard

  1. The shortcut — dropping multi-asset support — contradicts the existing acceptedAssets/assetCode model. The real decision is the denomination strategy: (a) denominate raisedAmount in a single base asset (native XLM) with per-asset breakdown stored separately, or (b) keep per-asset amounts and compute progress against a per-asset goal. This changes the data model, not just one line.
  2. Any correct sum needs an exchange-rate source for cross-asset conversion (the README already documents that the USD-equivalent column was removed for exactly this reason and awaits a price-oracle integration). The fix must decide whether conversion is in-scope or whether the UI must show per-asset totals instead.
  3. getContractBalance writes back to raisedAmount automatically; that side-effect must be removed or made asset-aware, otherwise it keeps re-corrupting whatever the data model settles on.
  4. The change touches the campaign browse sort, getCampaignStats, getUserActivitySummary, and profile totalRaised/totalDonated computations, all of which currently reduce mixed assets to one parseFloat.

Proposed design

Store per-asset amounts alongside (or instead of) the single raisedAmount — for example a raisedByAsset JSON column keyed by assetCode/issuer — and derive any single-number summary only for a declared base asset. Change getContractBalance to return a per-asset balance list and never auto-write a mixed-unit total. Offer the tradeoff explicitly: either introduce a price oracle now, or present per-asset totals in the API and let the frontend render them.

Downstream impact

This changes a public API contract: raisedAmount, progressPercentage, totalRaised, and CampaignStats are consumed by the sibling MilestoneX-Frontend (campaign cards, analytics, and the Project type in types/api.ts). The frontend must be updated to render per-asset totals or the chosen base-asset summary. Not repo-local; no generated bindings, but the frontend types need to match the new shape.

Acceptance criteria

Service

  • recalculateCampaignStats no longer sums amounts across different assets.
  • getContractBalance returns balances per asset and never overwrites the stored total with a mixed-unit sum.
  • Browse sorting and progress percentages are computed from a single, well-defined unit or documented as per-asset.

Data

  • A migration (or equivalent) captures per-asset raised amounts for campaigns that accept multiple assets.

Tests

  • A unit test proves a campaign with 100 XLM + 50 USDC does not report raisedAmount = 150 as a scalar.
  • A unit test proves getContractBalance reports the two balances separately.

Docs

  • README/Swagger documents how multi-asset totals and progress are represented.

Out of scope

Integrating a live price oracle for fiat conversion is separate; this issue establishes the asset-aware data representation the oracle can later feed.

Getting started

Files in scope: src/campaigns/campaigns.service.ts, src/donations/donations.service.ts, prisma/schema.prisma.

npm run build
npm test
npx prisma validate

Good first files to read: src/campaigns/campaigns.service.ts, src/donations/donations.service.ts, prisma/schema.prisma.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardThird CampaignCampaign: Third CampaignbugSomething isn't workinghelp wantedExtra attention is needed

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions