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 10 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.

Give us feedback!