Hawk High

First Flight #39
Beginner FriendlySolidity
100 EXP
View results
Submission Details
Severity: high
Valid

State-Balance Discrepancy: Unupdated Bursary After Fund Distribution

Summary

The graduateAndUpgrade function in LevelOne distributes 40% of the bursary funds but fails to update the bursary state variable to reflect this distribution. This creates a critical state inconsistency where the bursary value remains at 100% while only 60% of funds actually remain in the contract. This accounting error persists into the LevelTwo contract, potentially causing severe issues with future fund management.

Vulnerability Details

When funds are distributed during the upgrade process, the contract fails to update its internal accounting:

// LevelOne.sol:389-408
function graduateAndUpgrade(address _levelTwo, bytes memory) public onlyPrincipal {
// ...validation...
uint256 totalTeachers = listOfTeachers.length;
uint256 payPerTeacher = (bursary * TEACHER_WAGE) / PRECISION; // 35%
uint256 principalPay = (bursary * PRINCIPAL_WAGE) / PRECISION; // 5%
_authorizeUpgrade(_levelTwo);
for (uint256 n = 0; n < totalTeachers; n++) {
usdc.safeTransfer(listOfTeachers[n], payPerTeacher);
}
usdc.safeTransfer(principal, principalPay);
// Missing: bursary = bursary - distributedAmount;
}

Key issues in this implementation:

  1. Missing Accounting Update: After distributing 35% to teachers and 5% to principal (40% total), the bursary state variable is not decremented to reflect the reduced balance.

  2. Incorrect State Carried Forward: The bursary state variable in LevelTwo inherits this incorrect value:

    // LevelTwo.sol:7-14
    contract LevelTwo is Initializable {
    using SafeERC20 for IERC20;
    address principal;
    bool inSession;
    uint256 public sessionEnd;
    uint256 public bursary; // Inherits incorrect value from LevelOne
    uint256 public cutOffScore;
    // ...
    }

3.No Reconciliation Mechanism: The empty graduate() function in LevelTwo doesn't correct this inconsistency:

// LevelTwo.sol:19
function graduate() public reinitializer(2) {} // Empty implementation

The bursary value is critical as it serves as the source of truth for available funds in the contract. With this discrepancy:

  • State shows 100% of original funds

  • Actual balance is only 60% of what's recorded in state

Impact

The severity is high because:

  1. It creates a fundamental accounting error that persists across contract upgrades

  2. It breaks the core financial accounting of the system

  3. It could lead to attempted overdistribution of funds in future operations

  4. It violates the invariant that contract state should accurately reflect token balances

The likelihood is high because:

  1. The issue occurs in 100% of upgrade scenarios

  2. There is no conditional path where this wouldn't happen

  3. The upgrade process is a key feature of the system

  4. The issue persists until a new implementation fixes it

Real-world impact scenarios:

  • If bursary incorrectly shows 1000 USDC when only 600 USDC exists, future functions attempting to distribute based on bursary would fail

  • Accounting reports based on chain data would show incorrect fund balances

  • Attempts to implement additional fund distribution logic in LevelTwo would operate with incorrect assumptions

Tools Used

  • Manual review of fund flow between contracts

Recommendations

  1. Update Bursary State After Distribution:

function graduateAndUpgrade(address _levelTwo, bytes memory) public onlyPrincipal {
// ... existing validation ...
uint256 totalTeachers = listOfTeachers.length;
uint256 payPerTeacher = (bursary * TEACHER_WAGE) / PRECISION;
uint256 principalPay = (bursary * PRINCIPAL_WAGE) / PRECISION;
// Calculate total distributed amount
uint256 totalDistributed = (payPerTeacher * totalTeachers) + principalPay;
// Update bursary BEFORE upgrade to ensure correct state carries forward
bursary = bursary - totalDistributed;
_authorizeUpgrade(_levelTwo);
// Distribute funds after state is updated
for (uint256 n = 0; n < totalTeachers; n++) {
usdc.safeTransfer(listOfTeachers[n], payPerTeacher);
}
usdc.safeTransfer(principal, principalPay);
emit BursaryUpdated(bursary);
}
event BursaryUpdated(uint256 newBursaryValue);

2.Implement Proper Initialization in LevelTwo:

function graduate() public reinitializer(2) {
// Verify bursary matches actual balance and correct if needed
uint256 actualBalance = usdc.balanceOf(address(this));
if (bursary != actualBalance) {
// This serves as a safety check in case funds were distributed incorrectly
bursary = actualBalance;
emit BursaryReconciled(bursary);
}
}
event BursaryReconciled(uint256 correctedValue);

3.Add Balance Verification Function:

// Add to both contracts
function verifyBursaryBalance() public view returns (bool, uint256, uint256) {
uint256 actualBalance = usdc.balanceOf(address(this));
uint256 recordedBursary = bursary;
return (actualBalance == recordedBursary, actualBalance, recordedBursary);
}
Updates

Lead Judging Commences

yeahchibyke Lead Judge 6 months ago
Submission Judgement Published
Validated
Assigned finding tags:

bursary not updated

The bursary is not updated after wages have been paid in `graduateAndUpgrade()` function

Support

FAQs

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