Hawk High

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

[M-3] Principal Can Add Themselves as a Teacher to Get More Money

Severity

Medium

Impact

The principal can add themselves as a teacher and receive both the 5% principal wages AND a share of the 35% teacher wages, violating the intended payment structure and reducing the amount that legitimate teachers receive.

Description

In the LevelOne.sol contract, the principal can call the addTeacher() function to add any address as a teacher, including their own address. While there are checks preventing zero addresses, existing teachers, and students from being added as teachers, there is no check preventing the principal from adding themselves.

When the graduateAndUpgrade() function is called at the end of the session, the payment distribution works as follows:

  • Each teacher receives an equal share of 35% of the bursary

  • The principal receives 5% of the bursary

If the principal adds themselves as a teacher, they would receive:

  • 5% of the bursary as the principal

  • An equal share of 35% of the bursary as a teacher

This violates the intended payment structure specified in the documentation and reduces the wages legitimate teachers would receive.

Proof of Concept

PoC test code
// SPDX-License-Identifier: MIT
pragma solidity 0.8.26;
import {Test, console2} from "forge-std/Test.sol";
import {LevelOne} from "../src/LevelOne.sol";
import {LevelTwo} from "../src/LevelTwo.sol";
import {ERC20Mock} from "@openzeppelin/contracts/mocks/token/ERC20Mock.sol";
contract PrincipalTeacherExploitTest is Test {
LevelOne levelOne;
LevelTwo levelTwo;
ERC20Mock usdc;
address principal;
address teacher1;
address student1;
uint256 schoolFees = 100 * 10**18;
uint256 startTime;
function setUp() public {
// Set initial block timestamp
startTime = block.timestamp;
// Create mock token and accounts
usdc = new ERC20Mock();
principal = makeAddr("principal");
teacher1 = makeAddr("teacher1");
student1 = makeAddr("student1");
// Deploy LevelOne and initialize it
levelOne = new LevelOne();
levelOne.initialize(principal, schoolFees, address(usdc));
// Deploy LevelTwo for upgrade
levelTwo = new LevelTwo();
// Fund accounts with USDC
usdc.mint(student1, schoolFees);
// Set allowances for students
vm.prank(student1);
usdc.approve(address(levelOne), schoolFees);
// Add normal teacher
vm.prank(principal);
levelOne.addTeacher(teacher1);
// Enroll student
vm.prank(student1);
levelOne.enroll();
}
function testPrincipalCanAddSelfAsTeacher() public {
// Record initial balances
uint256 principalInitialBalance = usdc.balanceOf(principal);
uint256 teacher1InitialBalance = usdc.balanceOf(teacher1);
// Principal adds themselves as a teacher
vm.prank(principal);
levelOne.addTeacher(principal);
// Verify principal is now also a teacher
assertTrue(levelOne.isTeacher(principal));
// Start session
vm.prank(principal);
levelOne.startSession(60); // 60 as cutOffScore
// Give 4 weekly reviews (one per week)
for (uint i = 0; i < 4; i++) {
// Move to the next week
vm.warp(startTime + ((i + 1) * 1 weeks));
// Teacher1 gives review
vm.prank(teacher1);
levelOne.giveReview(student1, true);
}
// Warp to end of session (4 weeks from session start)
vm.warp(startTime + 4 weeks);
// Graduate and upgrade
vm.prank(principal);
levelOne.graduateAndUpgrade(address(levelTwo), "");
// Check final balances
uint256 principalFinalBalance = usdc.balanceOf(principal);
uint256 teacher1FinalBalance = usdc.balanceOf(teacher1);
// Calculate expected payments
uint256 bursaryAmount = schoolFees;
uint256 principalWage = (bursaryAmount * 5) / 100;
uint256 teacherWagePool = (bursaryAmount * 35) / 100;
uint256 teacherShare = teacherWagePool / 2; // Split between 2 teachers (principal and teacher1)
console2.log("Total school fees collected: %s ETH", bursaryAmount / 1e18);
console2.log("Principal earned: %s ETH", (principalFinalBalance - principalInitialBalance) / 1e18);
console2.log("Expected principal earnings (principal wage only): %s ETH", principalWage / 1e18);
// Verify the principal is getting more than the expected 5% of the bursary
assertTrue(principalFinalBalance - principalInitialBalance > principalWage,
"Principal is not getting more than their 5% wage");
// Verify that principal is getting more than their fair share (5% + 35%/2)
assertTrue(principalFinalBalance - principalInitialBalance > principalWage + teacherShare,
"Principal is not getting more than principal wage + fair teacher share");
}
}

Recommended Mitigation

function addTeacher(address _teacher) public onlyPrincipal notYetInSession {
if (_teacher == address(0)) {
revert HH__ZeroAddress();
}
if (isTeacher[_teacher]) {
revert HH__TeacherExists();
}
if (isStudent[_teacher]) {
revert HH__NotAllowed();
}
+ // Prevent principal from adding themselves as a teacher
+ if (_teacher == principal) {
+ revert HH__NotAllowed();
+ }
listOfTeachers.push(_teacher);
isTeacher[_teacher] = true;
emit TeacherAdded(_teacher);
}
Updates

Lead Judging Commences

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

principal can become teacher

Principal can add themselves as teacher and share in teacher pay upon graduation

Support

FAQs

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