Hawk High

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

Incorrect Teacher Payment Calculation in LevelOne Contract

Summary

The LevelOne contract's graduateAndUpgrade function contains a critical vulnerability in its teacher payment calculation, failing to ensure that teachers collectively receive 35% of bursary as specified ("Teachers: Will share in 35% of all school fees paid as their wages"). This financial logic error risks incorrect payouts, potential fund mismanagement, and transaction failures.

Vulnerability Details

The graduateAndUpgrade function incorrectly calculates payPerTeacher as (bursary * TEACHER_WAGE) / PRECISION, causing each teacher to receive 35% of the bursary instead of 35% split equally among totalTeachers. This overpays teachers when there is more than one, deviating from the 35% total wage invariant. With three or more teachers, payouts exceed 100% (e.g., 3 * 35% = 105%), causing transaction failures due to insufficient funds. The onlyPrincipal restriction limits external exploitation, but a malicious principal can call this function anytime, as there is no timing restriction, potentially amplifying the overpayment. The bug also triggers automatically during normal operation.

Testing suite:

// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity 0.8.26;
import {Test, console2} from "../../lib/forge-std/src/Test.sol";
import {DeployLevelOne} from "../script/DeployLevelOne.s.sol";
import {GraduateToLevelTwo} from "../script/GraduateToLevelTwo.s.sol";
import {LevelOne} from "../src/LevelOne.sol";
import {LevelTwo} from "../src/LevelTwo.sol";
import {MockUSDC} from "./mocks/MockUSDC.sol";
contract LevelOneAndGraduateTest is Test {
DeployLevelOne deployBot;
GraduateToLevelTwo graduateBot;
LevelOne levelOneProxy;
LevelTwo levelTwoImplementation;
address proxyAddress;
address levelOneImplementationAddress;
address levelTwoImplementationAddress;
MockUSDC usdc;
address principal;
uint256 schoolFees;
// teachers
address alice;
address bob;
address ed;
// students
address clara;
address dan;
address eli;
address fin;
address grey;
address harriet;
function setUp() public {
deployBot = new DeployLevelOne();
proxyAddress = deployBot.deployLevelOne();
levelOneProxy = LevelOne(proxyAddress);
usdc = deployBot.getUSDC();
principal = deployBot.principal();
schoolFees = deployBot.getSchoolFees();
levelOneImplementationAddress = deployBot.getImplementationAddress();
alice = makeAddr("first_teacher");
bob = makeAddr("second_teacher");
ed = makeAddr("third_teacher");
clara = makeAddr("first_student");
dan = makeAddr("second_student");
eli = makeAddr("third_student");
fin = makeAddr("fourth_student");
grey = makeAddr("fifth_student");
harriet = makeAddr("sixth_student");
usdc.mint(clara, schoolFees);
usdc.mint(dan, schoolFees);
usdc.mint(eli, schoolFees);
usdc.mint(fin, schoolFees);
usdc.mint(grey, schoolFees);
usdc.mint(harriet, schoolFees);
}
modifier schoolInSession() {
_teachersAdded();
_studentsEnrolled();
vm.prank(principal);
levelOneProxy.startSession(70);
_;
}
function test_confirm_can_graduate_but_overpayment() public schoolInSession {
// School's bursary before system upgrade.
uint256 bursaryBeforeUpgrade = usdc.balanceOf(address(levelOneProxy));
console2.log("Bursary before system upgrade: ", bursaryBeforeUpgrade); // 30000000000000000000000
// Balance of teachers before system upgrade.
uint256 aliceWageBeforeUpgrade = usdc.balanceOf(alice);
uint256 bobWageBeforeUpgrade = usdc.balanceOf(bob);
console2.log("Teacher Alice balance before system upgrade: ", aliceWageBeforeUpgrade); // 0
console2.log("Teacher Bob balance before system upgrade: ", bobWageBeforeUpgrade); // 0
levelTwoImplementation = new LevelTwo();
levelTwoImplementationAddress = address(levelTwoImplementation);
bytes memory data = abi.encodeCall(LevelTwo.graduate, ());
vm.prank(principal);
levelOneProxy.graduateAndUpgrade(levelTwoImplementationAddress, data);
// School's bursary after system upgrade.
uint256 bursaryAfterUpgrade = usdc.balanceOf(address(levelOneProxy));
console2.log("Bursary after system upgrade: ", bursaryAfterUpgrade); // 7500000000000000000000
// Balance of teachers after system upgrade.
uint256 aliceWageAfterUpgrade = usdc.balanceOf(alice);
uint256 bobWageAfterUpgrade = usdc.balanceOf(bob);
console2.log("Teacher Alice balance after system upgrade: ", aliceWageAfterUpgrade); // 10500000000000000000000
console2.log("Teacher Bob balance after system upgrade: ", bobWageAfterUpgrade); // 10500000000000000000000
// Assertions
assertEq((aliceWageAfterUpgrade * 100) / bursaryBeforeUpgrade, 35, "Alice wage is 35% of bursary");
assertEq((bobWageAfterUpgrade * 100) / bursaryBeforeUpgrade, 35, "Bob wage is 35% of bursary");
}
// ////////////////////////////////
// ///// /////
// ///// HELPER FUNCTIONS /////
// ///// /////
// ////////////////////////////////
function _teachersAdded() internal {
vm.startPrank(principal);
levelOneProxy.addTeacher(alice);
levelOneProxy.addTeacher(bob);
// levelOneProxy.addTeacher(ed); // Uncomment to add new teacher
vm.stopPrank();
}
function _studentsEnrolled() internal {
vm.startPrank(clara);
usdc.approve(address(levelOneProxy), schoolFees);
levelOneProxy.enroll();
vm.stopPrank();
vm.startPrank(dan);
usdc.approve(address(levelOneProxy), schoolFees);
levelOneProxy.enroll();
vm.stopPrank();
vm.startPrank(eli);
usdc.approve(address(levelOneProxy), schoolFees);
levelOneProxy.enroll();
vm.stopPrank();
vm.startPrank(fin);
usdc.approve(address(levelOneProxy), schoolFees);
levelOneProxy.enroll();
vm.stopPrank();
vm.startPrank(grey);
usdc.approve(address(levelOneProxy), schoolFees);
levelOneProxy.enroll();
vm.stopPrank();
vm.startPrank(harriet);
usdc.approve(address(levelOneProxy), schoolFees);
levelOneProxy.enroll();
vm.stopPrank();
}
}

Impact

  • Financial Loss: With N teachers, the contract pays 35% * N of the bursary, potentially draining all funds or causing the transaction to revert due to insufficient balance.

  • System Disruption: If the bursary is drained, future operations fail. If the transaction reverts (e.g., with 3+ teachers), upgrades and graduations are halted, stalling the system.

Tools Used

Foundry and manual review.

Recommendations

Recalculate payPerTeacher to divide the 35% among teachers:

Relevent github links:

https://github.com/CodeHawks-Contests/2025-05-hawk-high/blob/3a7251910c31739505a8699c7a0fc1b7de2c30b5/src/LevelOne.sol#L302
- uint256 payPerTeacher = (bursary * TEACHER_WAGE) / (PRECISION);
+ uint256 payPerTeacher = (bursary * TEACHER_WAGE) / (PRECISION * totalTeachers);
Updates

Lead Judging Commences

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

incorrect teacher pay calculation

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

yeahchibyke Lead Judge 3 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.