Skip to content

Commit 80f114e

Browse files
committed
Merge main into PR 1048
2 parents ee8708b + 8a2a16d commit 80f114e

49 files changed

Lines changed: 2728 additions & 105 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

AUDIT_LOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# 🛡️ Automated Security & Mutation Audit Log
2-
Generated on: Mon Aug 24 03:07:26 UTC 2026
2+
Generated on: Tue Aug 25 03:02:28 UTC 2026
33
---
44
## 📦 Dependency License & Advisory Checks (cargo-deny)
55
```text

Cargo.lock

Lines changed: 8 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

contracts/analytics/src/lib.rs

Lines changed: 161 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,18 @@
1+
#![allow(clippy::clone_on_copy)] // fires inside ink! generated storage code
12
#![cfg_attr(not(feature = "std"), no_std)]
23
#![allow(unexpected_cfgs)]
34
#![allow(clippy::new_without_default)]
45

56
use ink::prelude::string::String;
67
use ink::prelude::vec::Vec;
78

9+
/// Standalone staking-dashboard helper with its own events, error type and
10+
/// unit tests (wired into the crate per Issue #983; previously a dead file
11+
/// that was never compiled or tested).
12+
pub mod staking_dashboard;
13+
814
#[ink::contract]
9-
mod propchain_analytics {
15+
pub mod propchain_analytics {
1016
use super::*;
1117

1218
/// Market metrics representing aggregated property data.
@@ -224,13 +230,14 @@ mod propchain_analytics {
224230
average_price: u128,
225231
total_volume: u128,
226232
properties_listed: u64,
227-
) {
228-
self.ensure_admin();
233+
) -> Result<(), AnalyticsError> {
234+
self.ensure_admin()?;
229235
self.current_metrics = MarketMetrics {
230236
average_price,
231237
total_volume,
232238
properties_listed,
233239
};
240+
Ok(())
234241
}
235242

236243
/// Batch update multiple market metrics in a single transaction.
@@ -239,7 +246,7 @@ mod propchain_analytics {
239246
&mut self,
240247
updates: Vec<MetricUpdate>,
241248
) -> Result<(), AnalyticsError> {
242-
self.ensure_admin();
249+
self.ensure_admin()?;
243250
if updates.len() > MAX_BATCH_SIZE {
244251
return Err(AnalyticsError::BatchSizeExceeded);
245252
}
@@ -259,7 +266,7 @@ mod propchain_analytics {
259266
/// Batch add multiple market trends in a single transaction.
260267
#[ink(message)]
261268
pub fn batch_add_trends(&mut self, trends: Vec<MarketTrend>) -> Result<(), AnalyticsError> {
262-
self.ensure_admin();
269+
self.ensure_admin()?;
263270
if trends.len() > MAX_BATCH_SIZE {
264271
return Err(AnalyticsError::BatchSizeExceeded);
265272
}
@@ -273,12 +280,15 @@ mod propchain_analytics {
273280
Ok(())
274281
}
275282

276-
/// Create market trend analysis with historical data
283+
/// Create market trend analysis with historical data.
284+
///
285+
/// Admin only; returns [`AnalyticsError::Unauthorized`] for any other caller.
277286
#[ink(message)]
278-
pub fn add_market_trend(&mut self, trend: MarketTrend) {
279-
self.ensure_admin();
287+
pub fn add_market_trend(&mut self, trend: MarketTrend) -> Result<(), AnalyticsError> {
288+
self.ensure_admin()?;
280289
self.historical_trends.insert(self.trend_count, &trend);
281290
self.trend_count += 1;
291+
Ok(())
282292
}
283293
#[ink(message)]
284294
pub fn get_historical_trends(&self) -> Vec<MarketTrend> {
@@ -366,15 +376,18 @@ mod propchain_analytics {
366376
}
367377
}
368378

369-
/// Update market sentiment from prediction markets
379+
/// Update market sentiment from prediction markets.
380+
///
381+
/// Admin only (or an authorized prediction-market integration once such
382+
/// a role exists); returns [`AnalyticsError::Unauthorized`] otherwise.
370383
#[ink(message)]
371384
pub fn update_market_sentiment(
372385
&mut self,
373386
property_id: u64,
374387
bull_volume: u128,
375388
bear_volume: u128,
376-
) {
377-
self.ensure_admin(); // Prediction market or admin updates this
389+
) -> Result<(), AnalyticsError> {
390+
self.ensure_admin()?; // Prediction market or admin updates this
378391
let total_volume = bull_volume + bear_volume;
379392
let ratio = (bull_volume * 10000)
380393
.checked_div(total_volume)
@@ -407,17 +420,21 @@ mod propchain_analytics {
407420
.checked_div(total_overall)
408421
.map(|n| n as u32)
409422
.unwrap_or(self.overall_sentiment.bull_bear_ratio_bips);
423+
Ok(())
410424
}
411425

412426
/// Update portfolio positions for an owner.
427+
///
428+
/// Admin only; returns [`AnalyticsError::Unauthorized`] for any other caller.
413429
#[ink(message)]
414430
pub fn set_portfolio_positions(
415431
&mut self,
416432
owner: AccountId,
417433
positions: Vec<PortfolioPosition>,
418-
) {
419-
self.ensure_admin();
434+
) -> Result<(), AnalyticsError> {
435+
self.ensure_admin()?;
420436
self.portfolio_positions.insert(owner, &positions);
437+
Ok(())
421438
}
422439

423440
/// Retrieve portfolio positions for an owner.
@@ -427,14 +444,17 @@ mod propchain_analytics {
427444
}
428445

429446
/// Update property-type market trends used for portfolio rebalancing recommendations.
447+
///
448+
/// Admin only; returns [`AnalyticsError::Unauthorized`] for any other caller.
430449
#[ink(message)]
431450
pub fn update_property_type_trend(
432451
&mut self,
433452
property_type: propchain_traits::PropertyType,
434453
trend: MarketTrend,
435-
) {
436-
self.ensure_admin();
454+
) -> Result<(), AnalyticsError> {
455+
self.ensure_admin()?;
437456
self.property_type_trends.insert(property_type, &trend);
457+
Ok(())
438458
}
439459

440460
/// Get the stored market trend for a specific property type.
@@ -454,15 +474,18 @@ mod propchain_analytics {
454474
}
455475

456476
/// Update the benchmark index for a property type against a basket of reference indices.
477+
///
478+
/// Admin only; returns [`AnalyticsError::Unauthorized`] for any other caller.
457479
#[ink(message)]
458480
pub fn update_benchmark_index(
459481
&mut self,
460482
property_type: propchain_traits::PropertyType,
461483
performance_change_percentage: i32,
462-
) {
463-
self.ensure_admin();
484+
) -> Result<(), AnalyticsError> {
485+
self.ensure_admin()?;
464486
self.benchmark_indices
465487
.insert(property_type, &performance_change_percentage);
488+
Ok(())
466489
}
467490

468491
/// Get the stored benchmark index for a property type.
@@ -595,13 +618,15 @@ mod propchain_analytics {
595618
self.admin
596619
}
597620

598-
/// Ensure only the admin can modify metrics
599-
fn ensure_admin(&self) {
600-
assert_eq!(
601-
self.env().caller(),
602-
self.admin,
603-
"Unauthorized: Analytics admin only"
604-
);
621+
/// Ensure only the admin can modify metrics.
622+
///
623+
/// Returns a typed [`AnalyticsError::Unauthorized`] instead of
624+
/// panicking so integrators can distinguish authorization failures.
625+
fn ensure_admin(&self) -> Result<(), AnalyticsError> {
626+
if self.env().caller() != self.admin {
627+
return Err(AnalyticsError::Unauthorized);
628+
}
629+
Ok(())
605630
}
606631

607632
// ── Admin Key Rotation (Issue #496) ──────────────────────────────────
@@ -730,38 +755,141 @@ mod propchain_analytics {
730755
/// materially different market states produce different text.
731756
#[ink::test]
732757
fn insights_differ_between_market_states() {
733-
// Bullish market: rising prices, rising volume, bull-heavy sentiment.
734758
let mut bullish = AnalyticsDashboard::new();
735759
bullish.add_market_trend(trend(5, 10));
736760
bullish.update_market_sentiment(1, 800, 200);
737761
let bull_report = bullish.generate_market_report();
738-
assert!(
739-
bull_report.insights.contains("upward"),
740-
"{}",
741-
bull_report.insights
742-
);
762+
assert!(bull_report.insights.contains("upward"));
743763
assert!(bull_report.insights.contains("increasing"));
744764
assert!(bull_report.insights.contains("bullish"));
745765

746-
// Bearish market: falling prices, falling volume, bear-heavy sentiment.
747766
let mut bearish = AnalyticsDashboard::new();
748767
bearish.add_market_trend(trend(-7, -3));
749768
bearish.update_market_sentiment(1, 150, 850);
750769
let bear_report = bearish.generate_market_report();
751770
assert!(bear_report.insights.contains("downward"));
752771
assert!(bear_report.insights.contains("decreasing"));
753772
assert!(bear_report.insights.contains("bearish"));
754-
755773
assert_ne!(bull_report.insights, bear_report.insights);
756774
}
757775

758-
/// A contract with no data yet reports a stable/no-data insights text.
759776
#[ink::test]
760777
fn insights_without_data_mention_stability_and_missing_sentiment() {
761778
let contract = AnalyticsDashboard::new();
762779
let report = contract.generate_market_report();
763780
assert!(report.insights.contains("stable"));
764781
assert!(report.insights.contains("no crowd sentiment data"));
765782
}
783+
784+
type Environment = ink::env::DefaultEnvironment;
785+
786+
fn accounts() -> ink::env::test::DefaultAccounts<Environment> {
787+
ink::env::test::default_accounts::<Environment>()
788+
}
789+
790+
fn set_caller(caller: AccountId) {
791+
ink::env::test::set_caller::<Environment>(caller);
792+
}
793+
794+
fn sample_trend() -> MarketTrend {
795+
MarketTrend {
796+
period_start: 1_000,
797+
period_end: 2_000,
798+
price_change_percentage: 5,
799+
volume_change_percentage: 10,
800+
}
801+
}
802+
803+
fn admin_contract() -> AnalyticsDashboard {
804+
AnalyticsDashboard::new()
805+
}
806+
807+
#[ink::test]
808+
fn unauthorized_update_market_metrics_gets_typed_error() {
809+
let accounts = accounts();
810+
let mut c = admin_contract();
811+
set_caller(accounts.bob);
812+
assert_eq!(c.update_market_metrics(100, 200, 3), Err(AnalyticsError::Unauthorized));
813+
}
814+
815+
#[ink::test]
816+
fn unauthorized_batch_update_metrics_gets_typed_error() {
817+
let accounts = accounts();
818+
let mut c = admin_contract();
819+
set_caller(accounts.bob);
820+
assert_eq!(c.batch_update_metrics(Vec::new()), Err(AnalyticsError::Unauthorized));
821+
}
822+
823+
#[ink::test]
824+
fn unauthorized_batch_add_trends_gets_typed_error() {
825+
let accounts = accounts();
826+
let mut c = admin_contract();
827+
set_caller(accounts.bob);
828+
assert_eq!(c.batch_add_trends(Vec::new()), Err(AnalyticsError::Unauthorized));
829+
}
830+
831+
#[ink::test]
832+
fn unauthorized_add_market_trend_gets_typed_error() {
833+
let accounts = accounts();
834+
let mut c = admin_contract();
835+
set_caller(accounts.bob);
836+
assert_eq!(c.add_market_trend(sample_trend()), Err(AnalyticsError::Unauthorized));
837+
assert_eq!(c.get_historical_trends().len(), 0);
838+
}
839+
840+
#[ink::test]
841+
fn unauthorized_update_market_sentiment_gets_typed_error() {
842+
let accounts = accounts();
843+
let mut c = admin_contract();
844+
set_caller(accounts.bob);
845+
assert_eq!(c.update_market_sentiment(1, 100, 100), Err(AnalyticsError::Unauthorized));
846+
}
847+
848+
#[ink::test]
849+
fn unauthorized_set_portfolio_positions_gets_typed_error() {
850+
let accounts = accounts();
851+
let mut c = admin_contract();
852+
set_caller(accounts.bob);
853+
assert_eq!(c.set_portfolio_positions(accounts.alice, Vec::new()), Err(AnalyticsError::Unauthorized));
854+
}
855+
856+
#[ink::test]
857+
fn unauthorized_update_property_type_trend_gets_typed_error() {
858+
let accounts = accounts();
859+
let mut c = admin_contract();
860+
set_caller(accounts.bob);
861+
assert_eq!(c.update_property_type_trend(propchain_traits::PropertyType::Residential, sample_trend()), Err(AnalyticsError::Unauthorized));
862+
}
863+
864+
#[ink::test]
865+
fn unauthorized_update_benchmark_index_gets_typed_error() {
866+
let accounts = accounts();
867+
let mut c = admin_contract();
868+
set_caller(accounts.bob);
869+
assert_eq!(c.update_benchmark_index(propchain_traits::PropertyType::Residential, 7), Err(AnalyticsError::Unauthorized));
870+
}
871+
872+
#[ink::test]
873+
fn admin_succeeds_on_all_gated_messages() {
874+
let accounts = accounts();
875+
let mut c = admin_contract();
876+
assert_eq!(c.update_market_metrics(150, 300, 4), Ok(()));
877+
assert_eq!(c.get_market_metrics().average_price, 150);
878+
assert_eq!(c.add_market_trend(sample_trend()), Ok(()));
879+
assert_eq!(c.get_historical_trends().len(), 1);
880+
assert_eq!(c.batch_update_metrics(vec![MetricUpdate { average_price: 160, total_volume: 320, properties_listed: 5 }]), Ok(()));
881+
assert_eq!(c.batch_add_trends(vec![sample_trend()]), Ok(()));
882+
assert_eq!(c.get_historical_trends().len(), 2);
883+
assert_eq!(c.update_market_sentiment(1, 400, 100), Ok(()));
884+
assert_eq!(c.overall_sentiment.bull_volume, 400);
885+
let positions = vec![PortfolioPosition { property_type: propchain_traits::PropertyType::Residential, value: 1_000 }];
886+
assert_eq!(c.set_portfolio_positions(accounts.alice, positions), Ok(()));
887+
assert_eq!(c.get_portfolio_positions(accounts.alice).len(), 1);
888+
assert_eq!(c.update_property_type_trend(propchain_traits::PropertyType::Commercial, sample_trend()), Ok(()));
889+
assert_eq!(c.get_property_type_trend(propchain_traits::PropertyType::Commercial).price_change_percentage, 5);
890+
assert_eq!(c.update_benchmark_index(propchain_traits::PropertyType::Commercial, 9), Ok(()));
891+
assert_eq!(c.get_benchmark_index(propchain_traits::PropertyType::Commercial), 9);
892+
}
893+
}
766894
}
767895
}

contracts/analytics/src/staking_dashboard.rs

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -339,18 +339,18 @@ mod tests {
339339
use ink::env::test::default_accounts;
340340

341341
/// Build a fresh dashboard with zeroed-out state.
342+
///
343+
/// Must be called from within an `#[ink::test]` so the mapping's lazy
344+
/// storage accesses have an initialized off-chain environment.
342345
fn fresh() -> StakingDashboard {
343-
ink::env::test::run_test::<ink::env::DefaultEnvironment, _>(|_| {
344-
Ok(StakingDashboard {
345-
staker_records: Mapping::default(),
346-
total_staked: 0,
347-
total_stakers: 0,
348-
rewards_distributed: 0,
349-
unclaimed_rewards: 0,
350-
reward_per_token_stored: 0,
351-
})
352-
})
353-
.unwrap()
346+
StakingDashboard {
347+
staker_records: Mapping::default(),
348+
total_staked: 0,
349+
total_stakers: 0,
350+
rewards_distributed: 0,
351+
unclaimed_rewards: 0,
352+
reward_per_token_stored: 0,
353+
}
354354
}
355355

356356
fn accounts() -> ink::env::test::DefaultAccounts<ink::env::DefaultEnvironment> {

contracts/bridge/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
#![allow(clippy::clone_on_copy)] // fires inside ink! generated storage code
12
#![cfg_attr(not(feature = "std"), no_std)]
23
#![allow(unexpected_cfgs)]
34
#![allow(

0 commit comments

Comments
 (0)