fix(security): address HIGH audit findings across 5 pallets

identity-kyc (H1):
- Add IdentityHashToAccount reverse mapping to prevent same identity hash
  being used by multiple accounts
- Check uniqueness in apply_for_citizenship, populate on confirm_citizenship,
  clean up on renounce_citizenship

pez-rewards (H2):
- Add EpochTotalClaimed storage to track claimed amounts per epoch
- do_close_epoch now only claws back unclaimed rewards (total_allocated -
  total_claimed), not the entire pot balance

tiki (H3):
- Replace custom "locked" attribute with pezpallet_nfts::disable_transfer()
  which sets the system-level PalletAttributes::TransferDisabled attribute
  that is actually enforced during transfers

tiki (H4):
- Fix EnsureTiki to check UserTikis storage for non-unique roles (Wezir,
  Parlementer) instead of TikiHolder which only stores unique roles

perwerde (H5):
- Add MaxPointsPerCourse config constant (1000 in runtime)
- Validate points in complete_course against the max
- Use saturating_add in get_perwerde_score to prevent u32 overflow

welati (H6):
- Add NativeCurrency: ReservableCurrency to Config
- Actually reserve candidacy deposit from candidate's balance

welati (H7):
- Add MaxEndorsers config constant (1000 in runtime)
- Validate endorsers count at the start of register_candidate before
  any storage reads
This commit is contained in:
2026-03-21 21:58:24 +03:00
parent 645d8aea73
commit fe49037cbe
11 changed files with 171 additions and 70 deletions
@@ -142,6 +142,11 @@ pub mod pezpallet {
#[pezpallet::constant]
type MaxCoursesPerStudent: Get<u32>;
/// Maximum points that can be awarded per course completion.
/// Prevents unbounded point inflation by course owners.
#[pezpallet::constant]
type MaxPointsPerCourse: Get<u32>;
/// Trust score updater - notifies trust pallet when perwerde score changes
type TrustScoreUpdater: TrustScoreUpdater<Self::AccountId>;
}
@@ -220,6 +225,8 @@ pub mod pezpallet {
TooManyCourses,
/// Course ID counter overflow
CourseIdOverflow,
/// Points exceed the maximum allowed per course
PointsExceedMax,
}
#[pezpallet::call]
@@ -295,6 +302,9 @@ pub mod pezpallet {
) -> DispatchResult {
let caller = ensure_signed(origin)?;
// Validate points are within the allowed maximum
ensure!(points <= T::MaxPointsPerCourse::get(), Error::<T>::PointsExceedMax);
// Verify caller is the course owner
let course = Courses::<T>::get(course_id).ok_or(Error::<T>::CourseNotFound)?;
ensure!(course.owner == caller, Error::<T>::NotCourseOwner);
@@ -344,7 +354,7 @@ pub mod pezpallet {
.filter_map(|course_id| Enrollments::<T>::get((who, *course_id)))
.filter(|enrollment| enrollment.completed_at.is_some())
.map(|enrollment| enrollment.points_earned)
.sum()
.fold(0u32, |acc, points| acc.saturating_add(points))
}
}
}
@@ -85,6 +85,7 @@ parameter_types! {
pub const MaxCourseLinkLength: u32 = 200;
pub const MaxStudentsPerCourse: u32 = 100; // Reduced for test performance
pub const MaxCoursesPerStudent: u32 = 50; // Max courses a student can enroll in
pub const MaxPointsPerCourse: u32 = 1000; // Max points per course completion
}
// --- KESİN ÇÖZÜM BURADA BAŞLIYOR ---
@@ -111,6 +112,7 @@ impl pezpallet_perwerde::Config for Test {
type MaxCourseLinkLength = MaxCourseLinkLength;
type MaxStudentsPerCourse = MaxStudentsPerCourse;
type MaxCoursesPerStudent = MaxCoursesPerStudent;
type MaxPointsPerCourse = MaxPointsPerCourse;
type TrustScoreUpdater = ();
}
@@ -363,7 +363,7 @@ fn complete_course_with_zero_points() {
}
#[test]
fn complete_course_with_max_points() {
fn complete_course_with_max_allowed_points() {
new_test_ext().execute_with(|| {
let admin = 0;
let student = 1;
@@ -376,16 +376,38 @@ fn complete_course_with_max_points() {
));
assert_ok!(PerwerdePallet::enroll(RuntimeOrigin::signed(student), 0));
// Complete with maximum points
// Complete with maximum allowed points (MaxPointsPerCourse = 1000)
assert_ok!(PerwerdePallet::complete_course(
RuntimeOrigin::signed(admin),
student,
0,
u32::MAX
1000
));
let enrollment = crate::Enrollments::<Test>::get((student, 0)).unwrap();
assert_eq!(enrollment.points_earned, u32::MAX);
assert_eq!(enrollment.points_earned, 1000);
});
}
#[test]
fn complete_course_fails_points_exceed_max() {
new_test_ext().execute_with(|| {
let admin = 0;
let student = 1;
assert_ok!(PerwerdePallet::create_course(
RuntimeOrigin::signed(admin),
create_bounded_vec(b"Course"),
create_bounded_vec(b"Desc"),
create_bounded_vec(b"http://example.com")
));
assert_ok!(PerwerdePallet::enroll(RuntimeOrigin::signed(student), 0));
// Points exceeding MaxPointsPerCourse (1000) should fail
assert_noop!(
PerwerdePallet::complete_course(RuntimeOrigin::signed(admin), student, 0, 1001),
crate::Error::<Test>::PointsExceedMax
);
});
}