Description: Although reflecting 60% of the remaing usdc after upgrade is one of the protocol invariant, In LevelOne:graduateAndUpgrade function will not do anything to bursary variable after transfering usdc token to teachers and principal
Vulnerability Details: the variable bursary is not updated after transfering money in LevelOne:graduateAndUpgrade
function graduateAndUpgrade(address _levelTwo, bytes memory) public onlyPrincipal {
if (_levelTwo == address(0)) {
revert HH__ZeroAddress();
}
uint256 totalTeachers = listOfTeachers.length;
uint256 payPerTeacher = (bursary * TEACHER_WAGE) / PRECISION;
uint256 principalPay = (bursary * PRINCIPAL_WAGE) / PRECISION;
_authorizeUpgrade(_levelTwo);
for (uint256 n = 0; n < totalTeachers; n++) {
usdc.safeTransfer(listOfTeachers[n], payPerTeacher);
}
usdc.safeTransfer(principal, principalPay);
}
Impact: Protocol Invariant break and after upgrade wrong accounting information
Tools Used: Manual Review
Proof of Concept: Add this test suit in your LeveOnelAndGraduateTest.t.sol
contract LevelTwo is Initializable, UUPSUpgradeable {
using SafeERC20 for IERC20;
address principal;
bool inSession;
+ uint256 schoolFees;
+ uint256 public immutable reviewTime = 1 weeks;
uint256 public sessionEnd;
uint256 public bursary;
uint256 public cutOffScore;
mapping(address => bool) public isTeacher; // 6th
mapping(address => bool) public isStudent;
mapping(address => uint256) public studentScore;
address[] listOfStudents;
address[] listOfTeachers;
uint256 public constant TEACHER_WAGE_L2 = 40;
uint256 public constant PRINCIPAL_WAGE_L2 = 5;
uint256 public constant PRECISION = 100;
Proof of Code
function test_BursaryDoesNotChangeAfterUpgrade() public {
vm.startPrank(principal);
levelOneProxy.addTeacher(alice);
levelOneProxy.addTeacher(bob);
vm.stopPrank();
vm.startPrank(clara);
usdc.approve(address(levelOneProxy), schoolFees);
levelOneProxy.enroll();
vm.stopPrank();
vm.startPrank(dan);
usdc.approve(address(levelOneProxy), schoolFees);
levelOneProxy.enroll();
vm.stopPrank();
vm.startPrank(eli);
usdc.approve(address(levelOneProxy), schoolFees);
levelOneProxy.enroll();
vm.stopPrank();
vm.startPrank(fin);
usdc.approve(address(levelOneProxy), schoolFees);
levelOneProxy.enroll();
vm.stopPrank();
vm.startPrank(grey);
usdc.approve(address(levelOneProxy), schoolFees);
levelOneProxy.enroll();
vm.stopPrank();
vm.startPrank(harriet);
usdc.approve(address(levelOneProxy), schoolFees);
levelOneProxy.enroll();
vm.stopPrank();
uint256 cuttOffScroe = 70;
vm.prank(principal);
levelOneProxy.startSession(cuttOffScroe);
bool isSessionOver = block.timestamp > levelOneProxy.getSessionEnd();
bytes memory data = abi.encodeCall(LevelTwo.graduate, ());
vm.startPrank(principal);
levelOneProxy.graduateAndUpgrade(levelTwoImplementationAddress, data);
levelOneProxy.upgradeToAndCall(levelTwoImplementationAddress, data);
vm.stopPrank();
console2.log(LevelTwo(proxyAddress).bursary());
assert(usdc.balanceOf(principal) == 1500 ether);
assert(LevelTwo(proxyAddress).TEACHER_WAGE_L2() == 40);
assert(!isSessionOver);
assert(LevelTwo(proxyAddress).bursary() == 6 * schoolFees);
}
Recommendations: Update the value of bursary after transfering usdc token to teachers and principal
function graduateAndUpgrade(address _levelTwo, bytes memory) public onlyPrincipal {
if (_levelTwo == address(0)) {
revert HH__ZeroAddress();
}
uint256 totalTeachers = listOfTeachers.length;
uint256 payPerTeacher = (bursary * TEACHER_WAGE) / PRECISION;
uint256 principalPay = (bursary * PRINCIPAL_WAGE) / PRECISION;
+ bursary = bursary - (payPerTeacher + principalPay);
uint256 payForEachTeacher = payPerTeacher / totalTeachers;
_authorizeUpgrade(_levelTwo);
for (uint256 n = 0; n < totalTeachers; n++) {
// usdc.safeTransfer(listOfTeachers[n], payPerTeacher);
usdc.safeTransfer(listOfTeachers[n], payForEachTeacher);
}
usdc.safeTransfer(principal, principalPay);
}