Hawk High

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

"Denial of Service Through Overpayment in Teacher Upgrade Logic

Summary

A critical denial-of-service (DoS) vulnerability was discovered in the graduateAndUpgrade function of a proxy upgrade system. When there are more teachers than the available bursary balance can support, the function reverts entirely, preventing upgrades and locking the contract in an unusable state. This is due to an unchecked transfer loop that does not account for available funds.

Vulnerability Details

In the graduateAndUpgrade function of the LevelOne contract, the bursary is distributed to all teachers and the principal using fixed ratios. However, this distribution assumes that the contract's USDC balance is always sufficient to cover all calculated transfers.

for (uint256 n = 0; n < totalTeachers; n++) {
usdc.safeTransfer(listOfTeachers[n], payPerTeacher); // REVERTS IF INSUFFICIENT BALANCE
}
usdc.safeTransfer(principal, principalPay);

When the number of teachers is large or if the bursary is low (e.g., due to only one student enrolling), the safeTransfer call fails due to ERC20InsufficientBalance.

Proof Of Concept (POC)

function test_bursary_increased() public {
// Clara enrolls and pays school fees
vm.startPrank(clara);
usdc.approve(address(levelOneProxy), schoolFees);
levelOneProxy.enroll();
vm.stopPrank();
// Add a teacher (only principal can do this)
vm.startPrank(principal);
levelOneProxy.addTeacher(alice);
levelOneProxy.addTeacher(bob);
levelOneProxy.addTeacher(carlos);
levelOneProxy.addTeacher(Manc);
// Deploy LevelTwo implementation
levelTwoImplementation = new LevelTwo();
address levelTwoImplementationAddress = address(levelTwoImplementation);
// Encode graduate() call
bytes memory data = abi.encodeCall(LevelTwo.graduate, ());
// Graduate and upgrade via principal
levelOneProxy.graduateAndUpgrade(levelTwoImplementationAddress, data);
// Recast proxy as LevelTwo
LevelTwo levelTwoProxy = LevelTwo(address(levelOneProxy));
// Check final bursary value
uint256 actualBursary = levelTwoProxy.bursary();
console2.log("Bursary after 2 students enrolled:", actualBursary);
// Assert that bursary schoolFees
assertEq(actualBursary, schoolFees);
}

Test Script Output

[772109] LevelOneAndGraduateTest::test_bursary_increased()
├─ [0] VM::startPrank(first_student: [0x0e0C2a2596E7bCd5122Ae32390d8C0657fe5b879])
│ └─ ← [Return]
├─ [24739] MockUSDC::approve(ERC1967Proxy: [0xA8452Ec99ce0C64f20701dB7dD3abDb607c00496], 5000000000000000000000 [5e21])
│ ├─ emit Approval(owner: first_student: [0x0e0C2a2596E7bCd5122Ae32390d8C0657fe5b879], spender: ERC1967Proxy: [0xA8452Ec99ce0C64f20701dB7dD3abDb607c00496], value: 5000000000000000000000 [5e21])
│ └─ ← [Return] true
├─ [157516] ERC1967Proxy::enroll()
│ ├─ [152629] LevelOne::enroll() [delegatecall]
│ │ ├─ [30869] MockUSDC::transferFrom(first_student: [0x0e0C2a2596E7bCd5122Ae32390d8C0657fe5b879], ERC1967Proxy: [0xA8452Ec99ce0C64f20701dB7dD3abDb607c00496], 5000000000000000000000 [5e21])
│ │ │ ├─ emit Transfer(from: first_student: [0x0e0C2a2596E7bCd5122Ae32390d8C0657fe5b879], to: ERC1967Proxy: [0xA8452Ec99ce0C64f20701dB7dD3abDb607c00496], value: 5000000000000000000000 [5e21])
│ │ │ └─ ← [Return] true
│ │ ├─ emit Enrolled(: first_student: [0x0e0C2a2596E7bCd5122Ae32390d8C0657fe5b879])
│ │ └─ ← [Stop]
│ └─ ← [Return]
├─ [0] VM::stopPrank()
│ └─ ← [Return]
├─ [0] VM::startPrank(principal: [0x6b9470599cb23a06988C6332ABE964d6608A50ca])
│ └─ ← [Return]
├─ [71191] ERC1967Proxy::addTeacher(first_teacher: [0xeeEeC5A3afd714e3C63A0b1ef6d80722Bcc514b3])
│ ├─ [70801] LevelOne::addTeacher(first_teacher: [0xeeEeC5A3afd714e3C63A0b1ef6d80722Bcc514b3]) [delegatecall]
│ │ ├─ emit TeacherAdded(: first_teacher: [0xeeEeC5A3afd714e3C63A0b1ef6d80722Bcc514b3])
│ │ └─ ← [Stop]
│ └─ ← [Return]
├─ [49291] ERC1967Proxy::addTeacher(second_teacher: [0xb4c265c1f1d07474E3715F65724E8fa9d662BF0e])
│ ├─ [48901] LevelOne::addTeacher(second_teacher: [0xb4c265c1f1d07474E3715F65724E8fa9d662BF0e]) [delegatecall]
│ │ ├─ emit TeacherAdded(: second_teacher: [0xb4c265c1f1d07474E3715F65724E8fa9d662BF0e])
│ │ └─ ← [Stop]
│ └─ ← [Return]
├─ [49291] ERC1967Proxy::addTeacher(third_teacher: [0x274526F5338cEA789F7a2Df38cF5d9aFF6070F5a])
│ ├─ [48901] LevelOne::addTeacher(third_teacher: [0x274526F5338cEA789F7a2Df38cF5d9aFF6070F5a]) [delegatecall]
│ │ ├─ emit TeacherAdded(: third_teacher: [0x274526F5338cEA789F7a2Df38cF5d9aFF6070F5a])
│ │ └─ ← [Stop]
│ └─ ← [Return]
├─ [49291] ERC1967Proxy::addTeacher(fourth_teacher: [0x8e0CCf7D980f4A1963EEAc41B560e5D10B458cF8])
│ ├─ [48901] LevelOne::addTeacher(fourth_teacher: [0x8e0CCf7D980f4A1963EEAc41B560e5D10B458cF8]) [delegatecall]
│ │ ├─ emit TeacherAdded(: fourth_teacher: [0x8e0CCf7D980f4A1963EEAc41B560e5D10B458cF8])
│ │ └─ ← [Stop]
│ └─ ← [Return]
├─ [228469] → new LevelTwo@0x83a4207Df92bA7f9DeD23D61A8802172740D7077
│ └─ ← [Return] 1141 bytes of code
├─ [56232] ERC1967Proxy::graduateAndUpgrade(LevelTwo: [0x83a4207Df92bA7f9DeD23D61A8802172740D7077], 0xd3618cca)
│ ├─ [55814] LevelOne::graduateAndUpgrade(LevelTwo: [0x83a4207Df92bA7f9DeD23D61A8802172740D7077], 0xd3618cca) [delegatecall]
│ │ ├─ [25188] MockUSDC::transfer(first_teacher: [0xeeEeC5A3afd714e3C63A0b1ef6d80722Bcc514b3], 1750000000000000000000 [1.75e21])
│ │ │ ├─ emit Transfer(from: ERC1967Proxy: [0xA8452Ec99ce0C64f20701dB7dD3abDb607c00496], to: first_teacher: [0xeeEeC5A3afd714e3C63A0b1ef6d80722Bcc514b3], value: 1750000000000000000000 [1.75e21])
│ │ │ └─ ← [Return] true
│ │ ├─ [25188] MockUSDC::transfer(second_teacher: [0xb4c265c1f1d07474E3715F65724E8fa9d662BF0e], 1750000000000000000000 [1.75e21])
│ │ │ ├─ emit Transfer(from: ERC1967Proxy: [0xA8452Ec99ce0C64f20701dB7dD3abDb607c00496], to: second_teacher: [0xb4c265c1f1d07474E3715F65724E8fa9d662BF0e], value: 1750000000000000000000 [1.75e21])
│ │ │ └─ ← [Return] true
│ │ ├─ [897] MockUSDC::transfer(third_teacher: [0x274526F5338cEA789F7a2Df38cF5d9aFF6070F5a], 1750000000000000000000 [1.75e21])
│ │ │ └─ ← [Revert] ERC20InsufficientBalance(0xA8452Ec99ce0C64f20701dB7dD3abDb607c00496, 1500000000000000000000 [1.5e21], 1750000000000000000000 [1.75e21])
│ │ └─ ← [Revert] ERC20InsufficientBalance(0xA8452Ec99ce0C64f20701dB7dD3abDb607c00496, 1500000000000000000000 [1.5e21], 1750000000000000000000 [1.75e21])
│ └─ ← [Revert] ERC20InsufficientBalance(0xA8452Ec99ce0C64f20701dB7dD3abDb607c00496, 1500000000000000000000 [1.5e21], 1750000000000000000000 [1.75e21])
└─ ← [Revert] ERC20InsufficientBalance(0xA8452Ec99ce0C64f20701dB7dD3abDb607c00496, 1500000000000000000000 [1.5e21], 1750000000000000000000 [1.75e21])
Suite result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 7.16ms (667.40µs CPU time)

Impact

High Severity: This issue completely halts contract upgrades and execution if usdc.safeTransfer reverts.

  • The contract becomes stuck in Level One, and teachers/principals receive no payments.

  • A single low-funded enrollment can permanently DoS the upgrade mechanism if many teachers are added.

Tools Used

Manual Review

Recommendations

  • Use pull-based payout mechanism instead of direct transfers

  • Add pre-check for sufficient USDC balance:

Updates

Lead Judging Commences

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

Appeal created

codeaudit0x1 Submitter
3 months ago
yeahchibyke Lead Judge
3 months ago
yeahchibyke Lead Judge 3 months ago
Submission Judgement Published
Invalidated
Reason: Incorrect statement

Support

FAQs

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