Hawk High

First Flight #39
Beginner FriendlySolidity
100 EXP
View results
Submission Details
Impact: high
Likelihood: high
Invalid

Missing Payment Validation Checks in Graduation Process

Summary

The graduateAndUpgrade function in LevelOne.sol lacks critical validation checks for:

  1. Whether there are sufficient funds to make all required payments

  2. Whether the total distribution of funds (40% between teachers and principal) is calculated and distributed correctly

Vulnerability Details

In LevelOne.sol, the graduateAndUpgrade function handles payment distribution but has two critical oversights:

function graduateAndUpgrade(address _levelTwo, bytes memory) public onlyPrincipal {
uint256 totalTeachers = listOfTeachers.length;
uint256 payPerTeacher = (bursary * TEACHER_WAGE) / PRECISION; // 35%
uint256 principalPay = (bursary * PRINCIPAL_WAGE) / PRECISION; // 5%
// No check if (payPerTeacher * totalTeachers + principalPay) <= bursary
// No validation if total distribution equals exactly 40%
for (uint256 n = 0; n < totalTeachers; n++) {
usdc.safeTransfer(listOfTeachers[n], payPerTeacher);
}
usdc.safeTransfer(principal, principalPay);
}

Proof of Concept:

// 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 {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 PaymentValidationTest is Test {
DeployLevelOne deployBot;
LevelOne levelOneProxy;
LevelTwo levelTwoImplementation;
MockUSDC usdc;
address proxyAddress;
address principal;
uint256 schoolFees;
// Test addresses
address[] teachers;
address student;
function setUp() public {
// Deploy contracts
deployBot = new DeployLevelOne();
proxyAddress = deployBot.deployLevelOne();
levelOneProxy = LevelOne(proxyAddress);
// Get deployment variables
usdc = deployBot.getUSDC();
principal = deployBot.getPrincipal();
schoolFees = deployBot.getSchoolFees();
// Setup teachers array
teachers = new address[](10);
for (uint256 i = 0; i < 10; i++) {
teachers[i] = makeAddr(
string(abi.encodePacked("teacher_", vm.toString(i)))
);
}
// Setup student
student = makeAddr("student");
}
function testPaymentValidationVulnerability() public {
// 1. Setup initial state
vm.startPrank(principal);
// Add just 2 teachers instead of 10 to demonstrate the issue
levelOneProxy.addTeacher(teachers[0]);
levelOneProxy.addTeacher(teachers[1]);
vm.stopPrank();
// 2. Enroll student with school fees
vm.startPrank(student);
deal(address(usdc), student, schoolFees);
usdc.approve(address(levelOneProxy), schoolFees);
levelOneProxy.enroll();
vm.stopPrank();
// 3. Start session and give reviews
vm.startPrank(principal);
levelOneProxy.startSession(70);
vm.stopPrank();
// Simulate 4 weeks of reviews
uint256 weekDuration = 1 weeks;
uint256 currentTime = block.timestamp;
for (uint256 week = 0; week < 4; week++) {
currentTime += weekDuration;
vm.warp(currentTime);
vm.startPrank(teachers[0]);
levelOneProxy.giveReview(student, true);
vm.stopPrank();
}
// 4. Calculate expected payments for 2 teachers
uint256 bursaryAmount = schoolFees;
uint256 teacherWage = (bursaryAmount * 35) / 100;
uint256 principalWage = (bursaryAmount * 5) / 100;
uint256 totalPayout = (teacherWage * 2) + principalWage; // Only 2 teachers
// 5. Deploy LevelTwo for upgrade
levelTwoImplementation = new LevelTwo();
// 6. Attempt graduation
vm.startPrank(principal);
// Record balances before
uint256 initialUSDCBalance = usdc.balanceOf(address(levelOneProxy));
levelOneProxy.graduateAndUpgrade(address(levelTwoImplementation), "");
// 7. Verify vulnerability
uint256 finalUSDCBalance = usdc.balanceOf(address(levelOneProxy));
uint256 actualDistributed = initialUSDCBalance - finalUSDCBalance;
// Assert that total payout exceeds 40% due to rounding
assertGt(
totalPayout,
(bursaryAmount * 40) / 100,
"Payment should exceed 40% due to rounding"
);
// Assert that actual distributed amount doesn't match expected 40%
assertTrue(
actualDistributed != (bursaryAmount * 40) / 100,
"Distribution should not equal exactly 40%"
);
vm.stopPrank();
}
}

Impact

  1. Potential arithmetic overflow when calculating payments for many teachers

  2. Possible fund leakage due to rounding errors in percentage calculations

  3. Risk of failed transfers if total calculated payments exceed available balance

  4. Inconsistency between intended 40% distribution and actual distributed amount

Tools Used

  • Manual Review

  • Foundry Testing Framework

Recommendations

Add explicit validation checks:

function graduateAndUpgrade(address _levelTwo, bytes memory) public onlyPrincipal {
uint256 totalTeachers = listOfTeachers.length;
uint256 payPerTeacher = (bursary * TEACHER_WAGE) / PRECISION;
uint256 principalPay = (bursary * PRINCIPAL_WAGE) / PRECISION;
// Check total distribution
uint256 totalDistribution = (payPerTeacher * totalTeachers) + principalPay;
require(totalDistribution <= bursary, "Distribution exceeds bursary");
// Validate 40% distribution
require(totalDistribution == (bursary * 40) / PRECISION,
"Distribution must equal exactly 40% of bursary");
// Check USDC balance
require(usdc.balanceOf(address(this)) >= totalDistribution,
"Insufficient USDC balance");
for (uint256 n = 0; n < totalTeachers; n++) {
usdc.safeTransfer(listOfTeachers[n], payPerTeacher);
}
usdc.safeTransfer(principal, principalPay);
}

The vulnerability is particularly concerning because:

  1. It affects the core financial operations of the contract

  2. Could lead to permanent loss of funds

  3. Impacts the fairness of payment distribution

  4. Could potentially break the upgrade process if payments fail

Updates

Lead Judging Commences

yeahchibyke Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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