Hawk High

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

Payment Distribution Overflow in Teacher Wage Calculation which can lead to denial of service

Summary

In LevelOne.sol, the graduateAndUpgrade function contains a critical vulnerability in the teacher wage distribution calculation that can lead to an overflow when calculating individual teacher payments, potentially causing loss of funds or system malfunction.

Vulnerability Details

Vulnerability Details: The vulnerability exists in the following code section of LevelOne.sol:

function graduateAndUpgrade(address _levelTwo, bytes memory) public onlyPrincipal {
uint256 totalTeachers = listOfTeachers.length;
uint256 payPerTeacher = (bursary * TEACHER_WAGE) / PRECISION;
for (uint256 n = 0; n < totalTeachers; n++) {
usdc.safeTransfer(listOfTeachers[n], payPerTeacher);
}
}

The issue arises because:

  1. The total teacher wage (35% of bursary) is calculated first: (bursary * TEACHER_WAGE) / PRECISION

  2. This amount is then transferred to each teacher individually

  3. If there are multiple teachers, the contract will attempt to pay each teacher the full 35% instead of dividing it among them

For example:

  • If bursary = 1000 USDC

  • TEACHER_WAGE = 35 (35%)

  • payPerTeacher = (1000 * 35) / 100 = 350 USDC

  • With 3 teachers, the contract tries to pay 350 USDC to each teacher

  • Total payout = 1050 USDC (exceeding the bursary)

POC

// SPDX-License-Identifier: MIT
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 {LevelTwo} from "../src/LevelTwo.sol";
import {MockUSDC} from "./mocks/MockUSDC.sol";
contract PaymentDistributionOverflowTest is Test {
DeployLevelOne deployBot;
LevelOne levelOneProxy;
LevelTwo levelTwoImplementation;
MockUSDC usdc;
address principal;
uint256 schoolFees;
address alice;
address bob;
address charlie;
address student1;
function setUp() public {
// Deploy contracts
deployBot = new DeployLevelOne();
levelOneProxy = LevelOne(deployBot.deployLevelOne());
levelTwoImplementation = new LevelTwo();
// Get deployment variables
usdc = deployBot.getUSDC();
principal = deployBot.getPrincipal();
schoolFees = deployBot.getSchoolFees(); // 5000e18
// Setup addresses
alice = makeAddr("alice");
bob = makeAddr("bob");
charlie = makeAddr("charlie");
student1 = makeAddr("student1");
// Mint USDC for student - only one student to demonstrate the issue clearly
usdc.mint(student1, schoolFees);
}
function testPaymentDistributionOverflow() public {
// 1. Add three teachers
vm.startPrank(principal);
levelOneProxy.addTeacher(alice);
levelOneProxy.addTeacher(bob);
levelOneProxy.addTeacher(charlie);
vm.stopPrank();
// 2. Enroll one student
vm.startPrank(student1);
usdc.approve(address(levelOneProxy), schoolFees);
levelOneProxy.enroll();
vm.stopPrank();
// 3. Start session with passing score
vm.prank(principal);
levelOneProxy.startSession(70);
// 4. Wait for session to end
vm.warp(block.timestamp + 4 weeks);
// Log initial state
console2.log("School Fees:", schoolFees);
console2.log(
"Contract Balance:",
usdc.balanceOf(address(levelOneProxy))
);
console2.log("Teacher Wage Percentage:", levelOneProxy.TEACHER_WAGE());
uint256 expectedTeacherAllocation = (schoolFees *
levelOneProxy.TEACHER_WAGE()) / levelOneProxy.PRECISION();
console2.log(
"Expected Total Teacher Allocation:",
expectedTeacherAllocation
);
console2.log(
"Expected Per Teacher (should be):",
expectedTeacherAllocation / 3
);
// 5. This should fail because each teacher will try to receive the full allocation
vm.expectRevert();
vm.prank(principal);
levelOneProxy.graduateAndUpgrade(address(levelTwoImplementation), "");
}
}

The vulnerability exists because:

  1. With one student paying 5000 USDC school fees:

    • Total teacher allocation should be 35% = 1750 USDC

    • With 3 teachers, each should get 583.33 USDC (1750/3)

  2. However, the contract is trying to pay each teacher the full 1750 USDC, resulting in:

    • Total attempted payment = 5250 USDC (1750 * 3)

    • This exceeds both the teacher allocation and the total funds available

  3. The issue is in the payment calculation in LevelOne.sol where it doesn't divide the teacher allocation by the number of teachers.

Test Output

Ran 1 test for test/PaymentDistributionOverflowTest.t.sol:PaymentDistributionOverflowTest
[PASS] testPaymentDistributionOverflow() (gas: 491965)
Logs:
School Fees: 5000000000000000000000
Contract Balance: 5000000000000000000000
Teacher Wage Percentage: 35
Expected Total Teacher Allocation: 1750000000000000000000
Expected Per Teacher (should be): 583333333333333333333
Traces:
[516665] PaymentDistributionOverflowTest::testPaymentDistributionOverflow()
├─ [0] VM::startPrank(principal: [0x6b9470599cb23a06988C6332ABE964d6608A50ca])
│ └─ ← [Return]
├─ [78232] ERC1967Proxy::fallback(alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6])
│ ├─ [73258] LevelOne::addTeacher(alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6]) [delegatecall]
│ │ ├─ emit TeacherAdded(: alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6])
│ │ └─ ← [Stop]
│ └─ ← [Return]
├─ [49832] ERC1967Proxy::fallback(bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e])
│ ├─ [49358] LevelOne::addTeacher(bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e]) [delegatecall]
│ │ ├─ emit TeacherAdded(: bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e])
│ │ └─ ← [Stop]
│ └─ ← [Return]
├─ [49832] ERC1967Proxy::fallback(charlie: [0xea475d60c118d7058beF4bDd9c32bA51139a74e0])
│ ├─ [49358] LevelOne::addTeacher(charlie: [0xea475d60c118d7058beF4bDd9c32bA51139a74e0]) [delegatecall]
│ │ ├─ emit TeacherAdded(: charlie: [0xea475d60c118d7058beF4bDd9c32bA51139a74e0])
│ │ └─ ← [Stop]
│ └─ ← [Return]
├─ [0] VM::stopPrank()
│ └─ ← [Return]
├─ [0] VM::startPrank(student1: [0x3A17b82638fdF8A1cAf9c60d2B13CecB85ABb5A8])
│ └─ ← [Return]
├─ [25298] MockUSDC::approve(ERC1967Proxy: [0x90193C961A926261B756D1E5bb255e67ff9498A1], 5000000000000000000000 [5e21])
│ ├─ emit Approval(owner: student1: [0x3A17b82638fdF8A1cAf9c60d2B13CecB85ABb5A8], spender: ERC1967Proxy: [0x90193C961A926261B756D1E5bb255e67ff9498A1], value: 5000000000000000000000 [5e21])
│ └─ ← [Return] true
├─ [152722] ERC1967Proxy::fallback()
│ ├─ [152251] LevelOne::enroll() [delegatecall]
│ │ ├─ [31619] MockUSDC::transferFrom(student1: [0x3A17b82638fdF8A1cAf9c60d2B13CecB85ABb5A8], ERC1967Proxy: [0x90193C961A926261B756D1E5bb255e67ff9498A1], 5000000000000000000000 [5e21])
│ │ │ ├─ emit Transfer(from: student1: [0x3A17b82638fdF8A1cAf9c60d2B13CecB85ABb5A8], to: ERC1967Proxy: [0x90193C961A926261B756D1E5bb255e67ff9498A1], value: 5000000000000000000000 [5e21])
│ │ │ └─ ← [Return] true
│ │ ├─ emit Enrolled(: student1: [0x3A17b82638fdF8A1cAf9c60d2B13CecB85ABb5A8])
│ │ └─ ← [Stop]
│ └─ ← [Return]
├─ [0] VM::stopPrank()
│ └─ ← [Return]
├─ [0] VM::prank(principal: [0x6b9470599cb23a06988C6332ABE964d6608A50ca])
│ └─ ← [Return]
├─ [50532] ERC1967Proxy::fallback(70)
│ ├─ [50058] LevelOne::startSession(70) [delegatecall]
│ │ ├─ emit SchoolInSession(startTime: 1, endTime: 2419201 [2.419e6])
│ │ └─ ← [Stop]
│ └─ ← [Return]
├─ [0] VM::warp(2419201 [2.419e6])
│ └─ ← [Return]
├─ [0] console::log("School Fees:", 5000000000000000000000 [5e21]) [staticcall]
│ └─ ← [Stop]
├─ [851] MockUSDC::balanceOf(ERC1967Proxy: [0x90193C961A926261B756D1E5bb255e67ff9498A1]) [staticcall]
│ └─ ← [Return] 5000000000000000000000 [5e21]
├─ [0] console::log("Contract Balance:", 5000000000000000000000 [5e21]) [staticcall]
│ └─ ← [Stop]
├─ [910] ERC1967Proxy::fallback() [staticcall]
│ ├─ [436] LevelOne::TEACHER_WAGE() [delegatecall]
│ │ └─ ← [Return] 35
│ └─ ← [Return] 35
├─ [0] console::log("Teacher Wage Percentage:", 35) [staticcall]
│ └─ ← [Stop]
├─ [888] ERC1967Proxy::fallback() [staticcall]
│ ├─ [414] LevelOne::PRECISION() [delegatecall]
│ │ └─ ← [Return] 100
│ └─ ← [Return] 100
├─ [910] ERC1967Proxy::fallback() [staticcall]
│ ├─ [436] LevelOne::TEACHER_WAGE() [delegatecall]
│ │ └─ ← [Return] 35
│ └─ ← [Return] 35
├─ [0] console::log("Expected Total Teacher Allocation:", 1750000000000000000000 [1.75e21]) [staticcall]
│ └─ ← [Stop]
├─ [0] console::log("Expected Per Teacher (should be):", 583333333333333333333 [5.833e20]) [staticcall]
│ └─ ← [Stop]
├─ [0] VM::expectRevert(custom error 0xf4844814)
│ └─ ← [Return]
├─ [0] VM::prank(principal: [0x6b9470599cb23a06988C6332ABE964d6608A50ca])
│ └─ ← [Return]
├─ [60008] ERC1967Proxy::fallback(LevelTwo: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], 0x)
│ ├─ [59512] LevelOne::graduateAndUpgrade(LevelTwo: [0x2e234DAe75C793f67A35089C9d99245E1C58470b], 0x) [delegatecall]
│ │ ├─ [25750] MockUSDC::transfer(alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6], 1750000000000000000000 [1.75e21])
│ │ │ ├─ emit Transfer(from: ERC1967Proxy: [0x90193C961A926261B756D1E5bb255e67ff9498A1], to: alice: [0x328809Bc894f92807417D2dAD6b7C998c1aFdac6], value: 1750000000000000000000 [1.75e21])
│ │ │ └─ ← [Return] true
│ │ ├─ [25750] MockUSDC::transfer(bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], 1750000000000000000000 [1.75e21])
│ │ │ ├─ emit Transfer(from: ERC1967Proxy: [0x90193C961A926261B756D1E5bb255e67ff9498A1], to: bob: [0x1D96F2f6BeF1202E4Ce1Ff6Dad0c2CB002861d3e], value: 1750000000000000000000 [1.75e21])
│ │ │ └─ ← [Return] true
│ │ ├─ [1541] MockUSDC::transfer(charlie: [0xea475d60c118d7058beF4bDd9c32bA51139a74e0], 1750000000000000000000 [1.75e21])
│ │ │ └─ ← [Revert] ERC20InsufficientBalance(0x90193C961A926261B756D1E5bb255e67ff9498A1, 1500000000000000000000 [1.5e21], 1750000000000000000000 [1.75e21])
│ │ └─ ← [Revert] ERC20InsufficientBalance(0x90193C961A926261B756D1E5bb255e67ff9498A1, 1500000000000000000000 [1.5e21], 1750000000000000000000 [1.75e21])
│ └─ ← [Revert] ERC20InsufficientBalance(0x90193C961A926261B756D1E5bb255e67ff9498A1, 1500000000000000000000 [1.5e21], 1750000000000000000000 [1.75e21])
└─ ← [Stop]

Impact

  1. The contract will attempt to pay more than the available bursary

  2. Teachers receive incorrect (inflated) wages

  3. Potential contract failure due to insufficient funds

  4. Disruption of the entire graduation and upgrade process

Tools Used

  • Manual code review

  • Foundry testing framework

Recommendations

  • Modify the wage calculation to divide the total teacher allocation by the number of teachers:

function graduateAndUpgrade(address _levelTwo, bytes memory) public onlyPrincipal {
uint256 totalTeachers = listOfTeachers.length;
// Calculate total teacher allocation first
uint256 totalTeacherAllocation = (bursary * TEACHER_WAGE) / PRECISION;
// Then divide by number of teachers
uint256 payPerTeacher = totalTeacherAllocation / totalTeachers;
for (uint256 n = 0; n < totalTeachers; n++) {
usdc.safeTransfer(listOfTeachers[n], payPerTeacher);
}
}

Additional recommendations:

  1. Add checks for division by zero when no teachers exist

  2. Consider handling remainder amounts from division

  3. Add events to track wage distributions

  4. Implement maximum teacher limits

The payment distribution overflow vulnerability in LevelOne.sol can definitely be classified as a Denial of Service (DoS) vulnerability. Here's why:

Resource Exhaustion:

  • The contract attempts to pay each teacher the full 35% of the bursary instead of dividing it among them

  • This leads to the contract trying to transfer more funds than it actually has available

  • When the total attempted payout exceeds the contract's USDC balance, transactions will revert

Impact on Core Functions:

  • The graduateAndUpgrade function, which is critical for:

    • Graduating students

    • Upgrading to LevelTwo

    • Distributing wages

  • Will fail completely when there are insufficient funds

  • This blocks the entire graduation process

Practical Example:

  • With a bursary of 1000 USDC

  • TEACHER_WAGE = 35%

  • Expected total teacher allocation = 350 USDC

  • With 3 teachers, the contract tries to pay:

    • Teacher 1: 350 USDC

    • Teacher 2: 350 USDC

    • Teacher 3: 350 USDC

  • Total attempted: 1050 USDC > 1000 USDC available

  • Result: Transaction reverts, blocking graduation

System-Wide Impact:

  • Students cannot progress to the next level

  • Teachers cannot receive their wages

  • Principal cannot receive their share

  • The entire school system becomes stuck in the current session

Therefore, this vulnerability can be classified as a DoS because it prevents the execution of critical contract functions and blocks the normal operation of the school system when multiple teachers are present.

Updates

Lead Judging Commences

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