Skip to content

feat: on-chain cross-asset aggregate risk engine - #16

Merged
Inkman007 merged 1 commit into
Ledger-Lenz:mainfrom
OlaGreat:feat/aggregate-risk-engine
Jun 17, 2026
Merged

feat: on-chain cross-asset aggregate risk engine#16
Inkman007 merged 1 commit into
Ledger-Lenz:mainfrom
OlaGreat:feat/aggregate-risk-engine

Conversation

@OlaGreat

Copy link
Copy Markdown
Contributor

Summary

Adds a live, weighted cross-asset aggregate risk score per wallet. A wallet that is moderately suspicious across several asset pairs (e.g. 60/65/70 on three pairs) is now visible as a single portfolio-level risk signal, instead of only as independent per-pair scores.

  • AggregateRiskScore type + AssetPairs(Address) / PairWeight(Symbol) / AggregateScore(Address) DataKey variants
  • storage::register_pair_for_wallet maintains a deduplicated list of a wallet's pairs; wired into submit_score and submit_scores_batch
  • get_aggregate_score(wallet) always recomputes from live per-pair scores — it never reads the AggregateScore(wallet) cache. That cache is refreshed as a side effect of every submit_score / submit_scores_batch call purely as a cheap snapshot for off-chain indexers, so the public read is guaranteed consistent with the latest submission.
  • Admin-configurable per-pair weights via set_pair_weight / get_pair_weight, defaulting to 1 (simple average). A weight of 0 excludes a pair from the denominator while still counting toward pair_count, max_pair_score, the flag counts, and last_updated.
  • Checked arithmetic throughout the weighted sum; returns the new ArithmeticOverflow error (appended as code 11, per the stable-ABI convention in CONTRIBUTING.md) instead of panicking on a pathological admin-set weight.
  • MAX_WALLET_PAIRS = 20 documents the practical O(N) bound get_aggregate_score is designed around (enforced only as a debug_assert!, a no-op in the release profile — not a hard on-chain cap, since the issue only asked for it to be documented).

Design comment / overflow strategy

The weighted sum is Σ (pair_weight[i] * pair_score[i]) / Σ pair_weight[i]. The per-pair multiplication weight.checked_mul(score) is done in u32 — matching the stored field widths — so weight = u32::MAX overflows on the very first multiplication once score ≥ 2, regardless of how many pairs are involved. Both running totals (weighted_sum, weight_sum) are then accumulated in u64 via checked_add, which is generous headroom for realistic weights/pair counts. The final aggregate_score downcast to u32 is always safe by construction: a weighted average of values in 0..=100 can never itself exceed 100.

Worked example (weighted case)

Pairs XLM_USDC(score 20, weight 1), XLM_BTC(score 80, weight 2), XLM_ETH(score 40, weight 1):

aggregate_score = (20*1 + 80*2 + 40*1) / (1 + 2 + 1)
                 = (20 + 160 + 40) / 4
                 = 220 / 4
                 = 55

This matches test_aggregate_weighted.

Test plan

  • cargo fmt --all -- --check
  • cargo clippy --all-targets -- -D warnings
  • cargo test — 50 passed (39 pre-existing + 11 new), covering every row in the issue's acceptance-criteria table: single pair, equal weights, weighted average, max-pair tracking, flag counts, rescoring, wallet-not-found, pair dedup, zero-weight exclusion, overflow protection
  • cargo build --target wasm32-unknown-unknown --release — please confirm in CI; my local environment didn't have network/target access to verify this one
  • README.md updated with AggregateRiskScore docs, function reference, event list, and the worked example above

Closes #11

Adds a live, weighted aggregate risk score per wallet across all of its
scored asset pairs, so a wallet that is moderately suspicious on several
pairs is visible as a single portfolio-level risk signal rather than only
as independent per-pair scores.

- AggregateRiskScore type + AssetPairs/PairWeight/AggregateScore DataKeys
- storage::register_pair_for_wallet deduplicates a wallet's pair list;
  wired into submit_score and submit_scores_batch
- get_aggregate_score always recomputes from live per-pair scores (never
  serves a stale cache); admin-configurable per-pair weights via
  set_pair_weight/get_pair_weight, defaulting to 1 (simple average)
- Checked arithmetic throughout the weighted sum, returning the new
  ArithmeticOverflow error instead of panicking on pathological weights
- 11 new tests covering the acceptance criteria (equal/weighted average,
  max-pair tracking, flag counts, rescoring, dedup, zero-weight exclusion,
  overflow protection) plus README docs and a worked example

Closes Ledger-Lenz#11
@Inkman007

Copy link
Copy Markdown
Contributor

please one test is failing,all test must pass

@OlaGreat

Copy link
Copy Markdown
Contributor Author
image

Hello @Inkman007 it not the test failling, it your github action, it says Error: Unable to resolve action swatinium/rust-cache, repository not found. please check and fix this.
Your CI workflow references a GitHub Action called swatinium/rust-cache, but that repository doesn't exist. which means it's a typo. The correct action is Swatinem/rust-cache (a very popular Rust caching action). Note the differences: it's Swatinem (ends in -em, not -ium), and the org name is capitalized. please check

@Inkman007

Copy link
Copy Markdown
Contributor

noted

@Inkman007
Inkman007 merged commit 0d2d46b into Ledger-Lenz:main Jun 17, 2026
8 of 10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Build cross-asset aggregate risk engine: weighted portfolio-level risk score per wallet

2 participants