Hawk High

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

Enrollment Function Fails Without Prior Token Approval

Severity: High
Likelihood: High
Impact: Critical

Description

The LevelOne contract's enroll() function attempts to transfer USDC tokens from the user to the contract using safeTransferFrom() without verifying if the contract has received approval to spend the user's tokens. This violates ERC20 token standards and will cause all enrollment attempts to fail unless users have separately approved the contract beforehand.

Vulnerable Code

function enroll() external notYetInSession {
if (isTeacher[msg.sender] || msg.sender == principal) {
revert HH__NotAllowed();
}
if (isStudent[msg.sender]) {
revert HH__StudentExists();
}
usdc.safeTransferFrom(msg.sender, address(this), schoolFees);
listOfStudents.push(msg.sender);
isStudent[msg.sender] = true;
studentScore[msg.sender] = 100;
bursary += schoolFees;
emit Enrolled(msg.sender);
}

Technical Details

  1. ERC20 Transfer Requirements:
    The ERC20 standard requires a two-step process for third-party transfers:

    • The token owner must call approve() to authorize a spender

    • Only then can the spender call transferFrom() to move tokens

  2. Current Implementation Issues:

    • The contract makes an unchecked assumption that users have approved it to spend their USDC

    • No mechanisms exist to guide users through the approval process

    • Users encountering failed transactions will see generic error messages like "SafeERC20: low-level call failed"

  3. Transaction Flow Analysis:

    • User calls enroll()

    • Contract attempts safeTransferFrom()

    • If no approval exists, the call reverts

    • User receives a failed transaction with minimal explanation

    • The enrollment process cannot proceed

Impact Assessment

  1. Functional Breakdown:

    • The primary function of the contract (enrolling students) is non-operational by default

    • 100% of users will experience transaction failures unless they've separately performed an approval

  2. User Experience Implications:

    • Creates an invisible prerequisite step not documented in the contract

    • Results in gas costs for failed transactions

    • Leads to confusion and potential abandonment of the platform

  3. System Reliability:

    • Fundamentally undermines the reliability of the enrollment process

    • May create misconceptions that the contract is completely broken

Severity Justification

  • High Severity: This issue completely breaks the core functionality of the contract

  • High Likelihood: Affects all users who haven't manually approved the contract

  • Critical Impact: Prevents new students from enrolling, effectively halting the primary purpose of the system

Proof of Concept

// Example scenario in JavaScript/Web3
async function demonstrateEnrollmentFailure() {
const levelOne = await LevelOne.deployed();
const usdc = await IERC20.at(await levelOne.getSchoolFeesToken());
const user = accounts[1];
// User has USDC but hasn't approved LevelOne
await usdc.transfer(user, web3.utils.toWei("1000", "ether"));
try {
// This will fail
await levelOne.enroll({from: user});
console.log("Enrollment succeeded (unexpected)");
} catch (error) {
console.log("Enrollment failed as expected:", error.message);
// Error will show: "SafeERC20: low-level call failed"
}
// Solution: Approve first, then enroll
await usdc.approve(levelOne.address, web3.utils.toWei("1000", "ether"), {from: user});
await levelOne.enroll({from: user});
console.log("Enrollment succeeded after approval");
}

Recommendation

Implement one of the following solutions:

Option 1: Add Explicit Allowance Check

function enroll() external notYetInSession {
if (isTeacher[msg.sender] || msg.sender == principal) {
revert HH__NotAllowed();
}
if (isStudent[msg.sender]) {
revert HH__StudentExists();
}
// Check for sufficient allowance
uint256 allowance = usdc.allowance(msg.sender, address(this));
if (allowance < schoolFees) {
revert HH__InsufficientAllowance(allowance, schoolFees);
}
usdc.safeTransferFrom(msg.sender, address(this), schoolFees);
listOfStudents.push(msg.sender);
isStudent[msg.sender] = true;
studentScore[msg.sender] = 100;
bursary += schoolFees;
emit Enrolled(msg.sender);
}

Option 2: Add Helper Functions for Approval

// Add a convenience function
function approveSchoolFees() external returns (bool) {
return usdc.approve(address(this), schoolFees);
}
// Check approval status
function getApprovalStatus(address student) external view returns (uint256 currentAllowance, uint256 requiredAllowance) {
return (usdc.allowance(student, address(this)), schoolFees);
}

Updates

Lead Judging Commences

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

Support

FAQs

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