#49687 [SC-Low] An underflow in `remove_item` function in `Uint64SetLib` Contract.

Submitted on Jul 18th 2025 at 11:41:38 UTC by @Immanux2160 for Audit Comp | Folks Smart Contract Library

  • Report ID: #49687

  • 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 remove_item function in Uint64SetLib first line compute last_idx = items.length - 1, this line does not check if the array it is computing is empty(0) and this can lead to an underflow in the system which does revert.

Vulnerability Details

The remove_item function in Uint64SetLib.py compute last_idx = items.length - 1 the first line does not check if the array is empty, which can lead to an underflow. While the implementation is okay, there should be a check for empty(0) array, so as to avoid underflow, and it should also return a revert error, so the user knows that the array is empty instead of being clueless about what is wrong.

Impact Details

Unexpected revert, without knowing what is wrong, the user or developer may be confused whenever they use the remove_item function.

Lack of an error feedback when the user or developer uses the remove_item when the array is empty.

References

We can check how the standard Openzeppelin EnumerableSet Library works:

https://github.com/OpenZeppelin/openzeppelin-contracts/blob/6cfb6b5051a77a700593f7c7790bbad877139e38/contracts/utils/structs/EnumerableSet.sol

Proof of Concept

Proof of Concept

This is their current implementation of the remove functionality:

def remove_item(to_remove: UInt64, items: DynamicArray[ARC4UInt64]) -> Tuple[Bool, DynamicArray[ARC4UInt64]]:
    `last_idx = items.length - 1`
    for idx, item in uenumerate(items):
        if item.native == to_remove:
            # remove last item to replace the "to_remove" item or remove entirely if it's the match
            last_item = items.pop()
            if idx != last_idx:
                items[idx] = last_item
            # return with the item removed
            return Bool(True), items.copy()

    # if here then item is not present
    return Bool(False), items.copy()

I will recommend we should check that the length of the items is not zero and we handle such case gracefully increasing the robustness of the library.

if len(items) == 0: 
     return Bool(False), items.copy()
else:
    for idx, item in uenumerate(items):
        if item.native == to_remove:
            # remove last item to replace the "to_remove" item or remove entirely if it's the match
            last_item = items.pop()
            if idx != last_idx:
                items[idx] = last_item
            # return with the item removed
            return Bool(True), items.copy()

Was this helpful?