#49003 [SC-Low] Array Underflow Vulnerability in UInt64SetLib leads to contract failure

Submitted on Jul 10th 2025 at 12:44:28 UTC by @HandsomeEarthworm6 for Audit Comp | Folks Smart Contract Library

  • Report ID: #49003

  • Report Type: Smart Contract

  • Report severity: Low

  • Target: https://github.com/Folks-Finance/algorand-smart-contract-library/blob/main/contracts/library/UInt64SetLib.py

  • Impacts:

    • Temporary denial of service (smart contract is made unable to operate for one block, functionality is restored in the next block)

Description

Brief/Intro

The UInt64SetLib.py contract contains an integer underflow vulnerability in the remove_item function that causes temporary contract failure when attempting to remove items from empty arrays.

Vulnerability Details

The vulnerability exists in the remove_item_for_header function of contracts/library/UInt64SetLib.py:

last_idx = items.length - 1 # Critical underflow when items.length = 0

When items.length equals 0, the subtraction operation 0 - 1 causes an integer underflow in the underlying TEAL assembly code. Since Algorand uses unsigned 64-bit integers (UInt64), this underflow wraps the value to UInt64::MAX. The contract then attempts to access items[18446744073709551615], which immediately fails with an array bounds error as shown below. intc_0 // 0 <- Push 0 (array length) extract_uint16 <- Extract as 16-bit uint dup <- Duplicate the length value intc_1 // 1 <- Push 1

  • <--- Error <- Subtraction causes underflow and crash

The vulnerability affects multiple code paths:

Direct calls to removeItem() with empty arrays

Cascading failures when arrays become empty through normal operations

Batch operations that process items sequentially

Test Results Confirmation: Six separate test cases failed with identical underflow errors, confirming the vulnerability affects:

1.remove_item on empty array should not crash

2.remove_item on empty array with different values

3.single item removal leading to empty array

4.operations on empty arrays

5.consecutive operations on empty arrays

Impact Details

Contract Failure: Any call to remove_item with an empty array immediately crashes the contract

##Recommendation check if the array is empty before trying to calculate last_idx

Proof of Concept

Proof of Concept

add this test to tests/library/UInt64SetLib.test.ts and then run :;

npx jest tests/library/UInt64SetLib.test.ts --testTimeout=30000

/ Add these test cases to your existing describe("UInt64SetLib") block

describe("Security Vulnerability Tests", () => { describe("Critical Issue: Array Underflow on Empty Array", () => { test("remove_item on empty array should not crash", async () => { // This test will likely FAIL and expose the critical bug const result = await client.removeItem({ args: [1n, []] }); expect(result).toEqual([false, []]); });

  test("remove_item on empty array with different values", async () => {
    expect(await client.removeItem({ args: [0n, []] })).toEqual([false, []]);
    expect(await client.removeItem({ args: [999999n, []] })).toEqual([false, []]);
  });
});

describe("Edge Cases", () => {
  test("single item removal leading to empty array", async () => {
    // Remove the only item to create empty array
    const removeResult = await client.removeItem({ args: [42n, [42n]] });
    expect(removeResult).toEqual([true, []]);
    
    // Try to remove from the now-empty result
    const removeFromEmpty = await client.removeItem({ args: [42n, removeResult[1]] });
    expect(removeFromEmpty).toEqual([false, []]);
  });

  test("operations on empty arrays", async () => {
    // Test has_item on empty array
    expect(await client.hasItem({ args: [1n, []] })).toBeFalsy();
    expect(await client.hasItem({ args: [0n, []] })).toBeFalsy();
    
    // Test add_item on empty array
    expect(await client.addItem({ args: [1n, []] })).toEqual([true, [1n]]);
    expect(await client.addItem({ args: [0n, []] })).toEqual([true, [0n]]);
    
    // Test remove_item on empty array
    expect(await client.removeItem({ args: [1n, []] })).toEqual([false, []]);
    expect(await client.removeItem({ args: [0n, []] })).toEqual([false, []]);
  });

  test("consecutive operations on empty arrays", async () => {
    // Multiple remove operations on empty arrays
    for (let i = 0; i < 3; i++) {
      const result = await client.removeItem({ args: [BigInt(i), []] });
      expect(result).toEqual([false, []]);
    }
  });
});

describe("Resource Tests", () => {
  test("operations on large arrays", async () => {
    // Create a moderately large array
    const largeArray = Array.from({ length: 50 }, (_, i) => BigInt(i));
    
    // These should complete without issues
    const hasResult = await client.hasItem({ args: [25n, largeArray] });
    expect(typeof hasResult).toBe("boolean");
    
    const addResult = await client.addItem({ args: [999n, largeArray] });
    expect(Array.isArray(addResult)).toBe(true);
    
    const removeResult = await client.removeItem({ args: [25n, largeArray] });
    expect(Array.isArray(removeResult)).toBe(true);
  });
});

});

Was this helpful?