Summary
In calculating payPerTeacher
, there is no division by the number of teachers, which means each teacher gets 35% of the bursary. This breaks a contract invariant and if there are 3 or more teachers, it will cause DOS of the upgrade functionality.
Vulnerability Details
Wages for teachers are supposed to be 35% of bursary total. In calculation of these fees in LevelOne.graduateAndUpgrade()
, the division by totalTeachers is missing. This leads to 35% of the bursary being sent to every teacher, instead of being the total transferred value. The relevant part of the code is below:
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);
}
Impact
No impact if there is only 1 teacher. 70% of the funds used instead of 35% if there are two teachers. 105% of the bursary funds would be required for 3 teachers. Having >= 3 teachers causes DOS in the function and the contract cannot be upgraded.
Proof Of Concept
Add the following line to LevelOneAndGraduateTeat.test_confirm_can_graduate()
and run with forge test --mt test_confirm_can_graduate -vvv
.
function test_confirm_can_graduate() public schoolInSession {
levelTwoImplementation = new LevelTwo();
levelTwoImplementationAddress = address(levelTwoImplementation);
bytes memory data = abi.encodeCall(LevelTwo.graduate, ());
vm.prank(principal);
levelOneProxy.graduateAndUpgrade(levelTwoImplementationAddress, data);
LevelTwo levelTwoProxy = LevelTwo(proxyAddress);
// console2.log(levelTwoProxy.bursary()); // this is a different bug.
// expected = 60% = 2e22. actual = 25% = 7.5e21
+ console2.log(usdc.balanceOf(address(levelTwoProxy))); //@audit
console2.log(levelTwoProxy.getTotalStudents());
}
Expected Result:
[PASS] test_confirm_can_graduate() (gas: 1174292)
Logs:
7500000000000000000000
6
Suite result: ok. 1 passed; 0 failed; 0 skipped; finished in 1.53ms (399.25µs CPU time)
Ran 1 test suite in 5.54ms (1.53ms CPU time): 1 tests passed, 0 failed, 0 skipped (1 total tests)
If you add another address to _teachersAdded()
in the test file (teachers = 3), and rerun the test, you get the following result:
Suite result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 6.33ms (1.10ms CPU time)
Ran 1 test suite in 1.28s (6.33ms CPU time): 0 tests passed, 1 failed, 0 skipped (1 total tests)
Failing tests:
Encountered 1 failing test in test/LeveOnelAndGraduateTest.t.sol:LevelOneAndGraduateTest
[FAIL: ERC20InsufficientBalance(0x90193C961A926261B756D1E5bb255e67ff9498A1, 9000000000000000000000 [9e21], 10500000000000000000000 [1.05e22])] test_confirm_can_graduate() (gas: 1340629)
Encountered a total of 1 failing tests, 0 tests succeeded
Tools Used
Manual Review
Recommendation
Divide payPerTeacher
by totalTeachers
.