Hawk High

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

Failed ERC20 Token Flow in Enrollment Process


Executive Summary

The Hawk High School smart contract (LevelOne) implements student enrollment functionality that fatally mishandles the token transfer process. The contract attempts to transfer USDC tokens from users without verifying they have granted approval for the transfer. This flawed implementation violates the ERC20 token standard's two-phase transfer requirement, rendering the core enrollment functionality completely non-operational for users who haven't separately approved the contract to spend their tokens.

Technical Analysis

ERC20 Token Transfer Pattern

ERC20 token transfers from a user's address to another address (via a third party) require a mandatory two-step process:

  1. The token holder must first call approve() on the token contract to grant permission to a spender address

  2. Only after approval can the spender address successfully call transferFrom() to move tokens

Vulnerable Code Examination

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); // CRITICAL ISSUE HERE
listOfStudents.push(msg.sender);
isStudent[msg.sender] = true;
studentScore[msg.sender] = 100;
bursary += schoolFees;
emit Enrolled(msg.sender);
}

The issue manifests in the following sequence:

  1. The function verifies the caller isn't a teacher or principal

  2. It checks the caller isn't already a student

  3. It immediately attempts to transfer tokens without checking for allowance

  4. The transfer will fail for any user who hasn't separately approved the contract

Deep Dive Into Transaction Flow

When a user attempts to enroll:

Technical Execution Path:

  1. User calls enroll()

  2. Contract calls usdc.safeTransferFrom()

  3. USDC token contract checks if address(this) has sufficient allowance from msg.sender

  4. Without prior approval, this check fails

  5. The token contract reverts the transaction

  6. The enrollment function never reaches the state-changing logic

Calldata Analysis:

→ LevelOne.enroll()
→ USDC.safeTransferFrom(msg.sender, LevelOne, schoolFees)
→ USDC.transferFrom(msg.sender, LevelOne, schoolFees)
→ Check if allowance[msg.sender][LevelOne] >= schoolFees
→ FAIL: Insufficient allowance
← Revert with "SafeERC20: low-level call failed"

Token State Interrogation:

// Status check query that would reveal the issue
function checkAllowanceStatus(address user) external view returns (uint256, bool) {
uint256 currentAllowance = usdc.allowance(user, address(this));
return (currentAllowance, currentAllowance >= schoolFees);
}

Impact Analysis

Quantified Impact

  1. Systemic Functionality:

    • 100% of enrollment attempts will fail without manual off-chain approval

    • Prevents core operational capability of the educational system

    • Blocks bursary accumulation and thus downstream payment functionality

  2. Financial Implications:

    • Users lose gas costs on failed transactions

    • Contract deployment resources are wasted on non-functional code

    • Educational ecosystem cannot receive payments or operate

  3. Protocol Analysis:

    • Circular dependency created: Users can't participate without understanding technical details not expressed in the contract

    • Hidden prerequisites undermine contract transparency

    • Breaks the operational promise of the smart contract

  4. UX Consequences:

    • Opaque error messages ("SafeERC20: low-level call failed") confuse users

    • Multiple transaction requirement increases friction (approve, then enroll)

    • Forces users to understand low-level ERC20 mechanics

Exploitability/Likeliness Assessment

This is not an exploitable vulnerability but a fundamental operational failure with 100% certainty of occurrence for any user who hasn't separately performed token approval. The likelihood is classified as "Guaranteed" because there is no path through which the function can succeed without prior manual intervention.

Code Paths Demonstrating Issue

Failed Execution Path

User with USDC → Calls enroll() → Transaction reverts → No state change occurs

Required Path (Not Supported by Contract)

User with USDC → Approves contractCalls enroll() → Successful enrollment

Remediation Options

Solution 1: Add Explicit Verification with Helpful Error Messages

// Create custom error
error HH__InsufficientTokenAllowance(uint256 current, uint256 required);
function enroll() external notYetInSession {
if (isTeacher[msg.sender] || msg.sender == principal) {
revert HH__NotAllowed();
}
if (isStudent[msg.sender]) {
revert HH__StudentExists();
}
// Check allowance before attempting transfer
uint256 currentAllowance = usdc.allowance(msg.sender, address(this));
if (currentAllowance < schoolFees) {
revert HH__InsufficientTokenAllowance(currentAllowance, schoolFees);
}
usdc.safeTransferFrom(msg.sender, address(this), schoolFees);
// Remaining code unchanged...
}

Solution 2: Implement Comprehensive Approval Management

// Add tracking for required approvals
mapping(address => bool) private hasApprovedEnrollment;
// Verify and mark approval status
function checkAndRegisterApproval() external returns (bool) {
uint256 currentAllowance = usdc.allowance(msg.sender, address(this));
if (currentAllowance >= schoolFees) {
hasApprovedEnrollment[msg.sender] = true;
return true;
}
return false;
}
// Get approval requirements
function getApprovalRequirements() external view returns (
address tokenAddress,
uint256 requiredAmount,
bool hasApproved
) {
return (
address(usdc),
schoolFees,
usdc.allowance(msg.sender, address(this)) >= schoolFees
);
}
// Modified enrollment function
function enroll() external notYetInSession {
// Existing checks...
// Helper check for better UX
if (usdc.allowance(msg.sender, address(this)) < schoolFees) {
revert HH__InsufficientTokenAllowance();
}
// Proceed with enrollment...
}

Solution 3: Two-Step Enrollment Pattern

// Track enrollment intent
mapping(address => bool) private enrollmentRequested;
// Step 1: Express intent to enroll
function requestEnrollment() external notYetInSession {
if (isTeacher[msg.sender] || msg.sender == principal) {
revert HH__NotAllowed();
}
if (isStudent[msg.sender]) {
revert HH__StudentExists();
}
enrollmentRequested[msg.sender] = true;
emit EnrollmentRequested(msg.sender, schoolFees);
}
// Step 2: Complete enrollment with payment
function completeEnrollment() external {
require(enrollmentRequested[msg.sender], "No enrollment request found");
// Transfer tokens after request is registered
usdc.safeTransferFrom(msg.sender, address(this), schoolFees);
// Complete enrollment
listOfStudents.push(msg.sender);
isStudent[msg.sender] = true;
studentScore[msg.sender] = 100;
bursary += schoolFees;
// Clean up request
enrollmentRequested[msg.sender] = false;
emit Enrolled(msg.sender);
}

Business Logic & Standards Observations

  1. ERC20 Compliance Violation: The contract fails to respect the token standard's design requirements for third-party transfers.

  2. Educational System Integrity: The non-functional enrollment process prevents students from joining the system, undermining the entire educational platform.

  3. Architectural Pattern Weakness: The contract uses a direct-transfer pattern for payments rather than a more robust pull-payment pattern.

  4. Transparency Principle Violation: The contract creates implicit requirements not documented within the contract itself.

Conclusion

The missing allowance verification in the enrollment function represents a critical functional failure that renders the contract's primary purpose inoperable. This issue has a 100% likelihood of occurrence for any user who hasn't performed separate approval steps. While not an exploitable security vulnerability in the traditional sense, it represents a fundamental design flaw that completely blocks the intended functionality of the contract.

The remediation is straightforward but essential - implement proper validation of token allowances before attempting transfers, or restructure the enrollment process to better guide users through the required steps.

Updates

Lead Judging Commences

yeahchibyke Lead Judge 4 months ago
Submission Judgement Published
Invalidated
Reason: Design choice

Support

FAQs

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