Hawk High

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

Wage distribution vulnerability Leads to excessive transfers to teachers

Summary

The graduateAndUpgrade function in the LevelOne contract incorrectly distributes teacher wages. It calculates the total teacher allocation (35% of the bursary) but then gives this full amount to each teacher instead of dividing it among all teachers. This creates a critical financial vulnerability that results in excessive payouts and potential insolvency of the system.

Vulnerability Details

In the graduateAndUpgrade function, the contract calculates the teacher payment amount as:

uint256 payPerTeacher = (bursary * TEACHER_WAGE) / PRECISION;

result

Logs:
Bursary amount: 5000000000000000000000
Alice balance before: 0
Bob balance before: 0
Alice balance after: 1750000000000000000000
Bob balance after: 1750000000000000000000
Alice payment: 1750000000000000000000
Bob payment: 1750000000000000000000
Total teacher payout: 3500000000000000000000
Expected total teacher share: 1750000000000000000000

This calculates 35% of the bursary, which should be the total allocation for all teachers combined. However, this amount is then distributed to each teacher:

for (uint256 n = 0; n < totalTeachers; n++) {
usdc.safeTransfer(listOfTeachers[n], payPerTeacher);
}

This means each teacher receives the full 35% of the bursary instead of their proportional share (35% divided by the number of teachers).

Impact

This vulnerability has severe financial implications:

  1. With 2 teachers: 70% of the bursary is distributed (instead of 35%)

  2. With 3 teachers: 105% of the bursary is distributed (exceeding available funds)

  3. With 4+ teachers: The contract will attempt to transfer more funds than available, causing transactions to fail

This leads to:

  • Excessive payouts that drain the school's funds

  • System insolvency when there are 3 or more teachers

  • Failed upgrades due to insufficient funds for transfers

  • Violation of the payment structure specified in the requirements (35% for all teachers combined)

The severity is high because:

  • Direct financial loss to the protocol

  • Complete breakdown of the economic model

  • Failure of core functionality (upgrades) as teacher count increases

PoC

// SPDX-License-Identifier: SEE LICENSE IN LICENSE
pragma solidity 0.8.26;
import {Test, console2} from "forge-std/Test.sol";
import {DeployLevelOne} from "../script/DeployLevelOne.s.sol";
import {LevelOne} from "../src/LevelOne.sol";
import {MockUSDC} from "./mocks/MockUSDC.sol";
contract WageDistributionVulnerabilityTest is Test {
DeployLevelOne deployBot;
LevelOne levelOneProxy;
MockUSDC usdc;
address principal;
uint256 schoolFees;
address alice; // teacher
address bob; // teacher
address clara; // student
address proxyAddress;
function setUp() public {
deployBot = new DeployLevelOne();
proxyAddress = deployBot.deployLevelOne();
levelOneProxy = LevelOne(proxyAddress);
usdc = deployBot.getUSDC();
principal = deployBot.principal();
schoolFees = deployBot.getSchoolFees();
alice = makeAddr("teacher");
bob = makeAddr("teacher2");
clara = makeAddr("student");
usdc.mint(clara, schoolFees);
usdc.mint(address(levelOneProxy), 1000 ether); // Additional USDC for bursary
}
function test_wage_distribution_error() public {
// Add two teachers
vm.startPrank(principal);
levelOneProxy.addTeacher(alice);
levelOneProxy.addTeacher(bob);
vm.stopPrank();
// Enroll student
vm.startPrank(clara);
usdc.approve(address(levelOneProxy), schoolFees);
levelOneProxy.enroll();
vm.stopPrank();
// Start session
vm.prank(principal);
levelOneProxy.startSession(70);
// Fast forward time to simulate weeks
for (uint256 i = 0; i < 4; i++) {
vm.warp(block.timestamp + 1 weeks);
vm.prank(alice);
levelOneProxy.giveReview(clara, true);
}
// Fast forward to session end
vm.warp(block.timestamp + 1 weeks);
// Record balances before upgrade and the bursary amount
uint256 aliceBalanceBefore = usdc.balanceOf(alice);
uint256 bobBalanceBefore = usdc.balanceOf(bob);
uint256 bursaryAmount = levelOneProxy.bursary();
console2.log("Bursary amount:", bursaryAmount);
console2.log("Alice balance before:", aliceBalanceBefore);
console2.log("Bob balance before:", bobBalanceBefore);
// We need to access the implementation of graduateAndUpgrade
// We'll inspect the code to understand what happens
// Execute upgrade (to any address, just to trigger wage distribution)
vm.prank(principal);
// We'll use a dummy address since we only care about the wage calculation
address dummyImpl = makeAddr("dummyImplementation");
bytes memory data = new bytes(0);
levelOneProxy.graduateAndUpgrade(dummyImpl, data);
// Check balances after wage distribution
uint256 aliceBalanceAfter = usdc.balanceOf(alice);
uint256 bobBalanceAfter = usdc.balanceOf(bob);
console2.log("Alice balance after:", aliceBalanceAfter);
console2.log("Bob balance after:", bobBalanceAfter);
uint256 alicePayment = aliceBalanceAfter - aliceBalanceBefore;
uint256 bobPayment = bobBalanceAfter - bobBalanceBefore;
uint256 totalTeacherPayout = alicePayment + bobPayment;
console2.log("Alice payment:", alicePayment);
console2.log("Bob payment:", bobPayment);
console2.log("Total teacher payout:", totalTeacherPayout);
// Calculate expected total teacher share (35% of bursary)
uint256 expectedTotalTeacherShare = (bursaryAmount * levelOneProxy.TEACHER_WAGE()) / levelOneProxy.PRECISION();
console2.log("Expected total teacher share:", expectedTotalTeacherShare);
// Each teacher should get half of the teacher share, but they each get the full amount
assertEq(alicePayment, expectedTotalTeacherShare, "Alice received the full teacher share");
assertEq(bobPayment, expectedTotalTeacherShare, "Bob received the full teacher share");
assertEq(totalTeacherPayout, 2 * expectedTotalTeacherShare, "Total payout is double the expected amount");
}
}

Tools Used

  • Manual code review

  • Foundry testing framework for vulnerability confirmation

Recommendations

Modify the wage distribution logic to properly divide the total teacher allocation among all teachers:

// Calculate total teacher allocation
uint256 totalTeacherPay = (bursary * TEACHER_WAGE) / PRECISION;
// Divide by number of teachers
uint256 payPerTeacher = totalTeacherPay / totalTeachers;
for (uint256 n = 0; n < totalTeachers; n++) {
usdc.safeTransfer(listOfTeachers[n], payPerTeacher);
}

Test for even/odd numbersd abd edge cases.
This ensures that the total payment to all teachers equals exactly 35% of the bursary, regardless of how many teachers are in the system, which aligns with the specified payment structure in the requirements.

Updates

Lead Judging Commences

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