Summary
The contract implements an incorrect payment distribution algorithm in the graduateAndUpgrade
function. Instead of splitting the designated teacher wage percentage (35%) among all teachers, the contract gives each teacher 35% of the bursary. This causes the total distribution to exceed the available bursary when there are more than 2 teachers, which will lead to transaction failures.
Vulnerability Details
function graduateAndUpgrade(address _levelTwo, bytes memory) public onlyPrincipal {
if (_levelTwo == address(0)) {
revert HH__ZeroAddress();
}
uint256 totalTeachers = listOfTeachers.length;
@> uint256 payPerTeacher = (bursary * TEACHER_WAGE) / PRECISION;
uint256 principalPay = (bursary * PRINCIPAL_WAGE) / PRECISION;
_authorizeUpgrade(_levelTwo);
for (uint256 n = 0; n < totalTeachers; n++) {
usdc.safeTransfer(listOfTeachers[n], payPerTeacher);
}
usdc.safeTransfer(principal, principalPay);
}
The issue occurs in the calculation of payPerTeacher
. Instead of dividing the total teacher allocation by the number of teachers, it assigns each teacher the full 35% allocation. This means:
With 2 teachers: 70% + 5% = 75% of bursary paid out
With 3 teachers: 105% + 5% = 110% of bursary paid out (exceeds available funds)
With 4 teachers: 140% + 5% = 145% of bursary paid out (exceeds available funds)
Impact
When there are 3 or more teachers, the function will attempt to distribute more funds than are available in the bursary.
This will cause the transaction to revert due to insufficient funds, making it impossible to upgrade the contract.
Even with only 2 teachers, the contract distributes 75% of the bursary instead of the intended 40%, depleting funds faster than intended.
Tools Used
Foundry, Manual Code Review
Recommendations
Modify the payment calculation to properly distribute the teacher allocation among all teachers:
function graduateAndUpgrade(address _levelTwo, bytes memory) public onlyPrincipal {
if (_levelTwo == address(0)) {
revert HH__ZeroAddress();
}
uint256 totalTeachers = listOfTeachers.length;
uint256 totalTeacherAllocation = (bursary * TEACHER_WAGE) / PRECISION;
uint256 payPerTeacher = totalTeachers > 0 ? totalTeacherAllocation / totalTeachers : 0;
uint256 principalPay = (bursary * PRINCIPAL_WAGE) / PRECISION;
_authorizeUpgrade(_levelTwo);
for (uint256 n = 0; n < totalTeachers; n++) {
usdc.safeTransfer(listOfTeachers[n], payPerTeacher);
}
usdc.safeTransfer(principal, principalPay);
}
This ensures that all teachers collectively receive 35% of the bursary, regardless of how many teachers there are
Poc
function test_Bursary_Payment_Precision_Error() public {
_teachersAdded();
_studentsEnrolled();
uint256 initialBursary = levelOneProxy.bursary();
uint256 totalTeachers = levelOneProxy.getTotalTeachers();
uint256 expectedPayPerTeacher = (initialBursary *
levelOneProxy.TEACHER_WAGE()) /
(levelOneProxy.PRECISION() * totalTeachers);
uint256 currentPayPerTeacher = (initialBursary *
levelOneProxy.TEACHER_WAGE()) / levelOneProxy.PRECISION();
uint256 principalPay = (initialBursary *
levelOneProxy.PRINCIPAL_WAGE()) / levelOneProxy.PRECISION();
uint256 totalPaidWithBug = (currentPayPerTeacher * totalTeachers) +
principalPay;
console.log("Initial Bursary:", initialBursary);
console.log("Number of Teachers:", totalTeachers);
console.log(
"Expected Pay Per Teacher (corrected calculation):",
expectedPayPerTeacher
);
console.log(
"Current Pay Per Teacher (buggy calculation):",
currentPayPerTeacher
);
console.log("Principal Pay:", principalPay);
console.log("Total Paid With Bug:", totalPaidWithBug);
uint256 intendedPercentage = levelOneProxy.TEACHER_WAGE() +
levelOneProxy.PRINCIPAL_WAGE();
uint256 actualPercentage = (((currentPayPerTeacher * totalTeachers) +
principalPay) * 100) / initialBursary;
console.log(
"Intended Distribution Percentage:",
intendedPercentage,
"%"
);
console.log("Actual Distribution Percentage:", actualPercentage, "%");
if (totalTeachers > 1) {
assertTrue(
actualPercentage > intendedPercentage,
"Bug not detected: Actual percentage should exceed intended percentage"
);
console.log(
"Each teacher gets 35% of the bursary instead of (35% / totalTeachers)"
);
uint256 teachersToExceedBursary = (100 -
levelOneProxy.PRINCIPAL_WAGE()) /
levelOneProxy.TEACHER_WAGE() +
1;
console.log(
"With",
teachersToExceedBursary,
"teachers, the total payout would exceed 100% of the bursary"
);
}
uint256 simulatedTeachers = 4;
uint256 simulatedPayout = (currentPayPerTeacher * simulatedTeachers) +
principalPay;
uint256 simulatedPercentage = (simulatedPayout * 100) / initialBursary;
console.log("Simulated with", simulatedTeachers, "teachers:");
console.log("Simulated Total Payout:", simulatedPayout);
console.log(
"Simulated Percentage of Bursary:",
simulatedPercentage,
"%"
);
assertTrue(
simulatedPercentage > 100,
"With enough teachers, payout should exceed 100% of bursary"
);
}
Output
[PASS] test_Bursary_Payment_Precision_Error() (gas: 791244)
Logs:
Initial Bursary: 30000000000000000000000
Number of Teachers: 2
Expected Pay Per Teacher (corrected calculation): 5250000000000000000000
Current Pay Per Teacher (buggy calculation): 10500000000000000000000
Principal Pay: 1500000000000000000000
Total Paid With Bug: 22500000000000000000000
Intended Distribution Percentage: 40 %
Actual Distribution Percentage: 75 %
Each teacher gets 35% of the bursary instead of (35% / totalTeachers)
With 3 teachers, the total payout would exceed 100% of the bursary
Simulated with 4 teachers:
Simulated Total Payout: 43500000000000000000000
Simulated Percentage of Bursary: 145 %