Hawk High

First Flight #39
Beginner FriendlySolidity
100 EXP
View results
Submission Details
Severity: high
Valid

Incorrect teacher payout calculation in graduateAndUpgrade.

Summary

When graduateAndUpgrade is called and teachers receive their pay for the session, it is not divided by the number of teachers. Instead, the contract sends each teacher the full amount intended for all teachers.

Vulnerability Details

We can see that payPerTeacher is set to (bursary * TEACHER_WAGE) / PRECISION but this does not account for the number of teachers in the current session.

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);
}

This PoC only works if the vulnerablities affecting the storage layout and invalid implementation are fixed, otherwise we can't upgrade the contract and therefore cannot check it's state.

PoC:

function test_teacher_wage_is_incorrect() public {
// Teachers added
_teachersAdded();
console2.log(levelOneProxy.TEACHER_WAGE());
console2.log(levelOneProxy.bursary());
// Student enrolled
// schoolFees == 5_000e18 or 5K usdc
vm.startPrank(clara);
usdc.approve(address(levelOneProxy), schoolFees);
levelOneProxy.enroll();
vm.stopPrank();
console2.log(levelOneProxy.bursary());
// Start school session
vm.startPrank(principal);
levelOneProxy.startSession(70);
vm.stopPrank();
// Warp forward 4 weeks and call graduateAndUpgrade
vm.warp(block.timestamp + 4 weeks);
vm.startPrank(principal);
levelTwoImplementation = new LevelTwo();
levelTwoImplementationAddress = address(levelTwoImplementation);
bytes memory data = abi.encodeCall(levelTwoImplementation.graduate, ());
levelOneProxy.graduateAndUpgrade(levelTwoImplementationAddress, data);
// Principal performs actual upgrade
// This passes after fixing wrong storage layout and invalid implementation.
levelOneProxy.upgradeToAndCall(levelTwoImplementationAddress, data);
vm.stopPrank();
// Check that the teacher wage is incorrect
console2.log(usdc.balanceOf(alice));
console2.log(usdc.balanceOf(bob));
console2.log(levelOneProxy.bursary());
}

Impact

payPerTeacher is not divided by totalTeachers. This means that each teacher gets paid more than they should, effectively draining the contract and breaking the protocol's functionality.

Tools Used

Manual review

Recommendations

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 payPerTeacher = ((bursary * TEACHER_WAGE) / PRECISION) / totalTeachers;
uint256 principalPay = (bursary * PRINCIPAL_WAGE) / PRECISION;
_authorizeUpgrade(_levelTwo);
for (uint256 n = 0; n < totalTeachers; n++) {
usdc.safeTransfer(listOfTeachers[n], payPerTeacher);
}
usdc.safeTransfer(principal, principalPay);
}
Updates

Lead Judging Commences

yeahchibyke Lead Judge about 2 months ago
Submission Judgement Published
Validated
Assigned finding tags:

incorrect teacher pay calculation

`payPerTeacher` in `graduateAndUpgrade()` is incorrectly calculated.

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.