#49559 [SC-Low] The remove functionality in `UInt64SetLib::remove_item` underflows on empty array
Submitted on Jul 17th 2025 at 08:37:56 UTC by @NHristov for Audit Comp | Folks Smart Contract Library
Report ID: #49559
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 for more than one block
Description
Brief/Intro
The UInt64SetLib::remove_item
computes the last index as
last_idx = items.length - 1
without first checking if the passed array is empty. If you call remove_item
with an empty array, items.length - 1
underflows to a negative value and immediately aborts the TEAL execution, reverting the transaction.
Vulnerability Details
In contracts/library/UInt64SetLib.py
we have:
# filepath: contracts/library/UInt64SetLib.py
@subroutine
def remove_item(
to_remove: UInt64,
items: DynamicArray[ARC4UInt64]
) -> Tuple[Bool, DynamicArray[ARC4UInt64]]:
# ← no guard for empty `items`
last_idx = items.length - 1
for idx, item in uenumerate(items):
if item.native == to_remove.native:
last_item = items.pop()
if idx != last_idx:
items[idx] = last_item
return Bool(True), items.copy()
return Bool(False), items.copy()
and if we pass an empty array, items.length
is 0
, so last_idx
becomes -1
. This leads to an immediate TEAL abort with the error message copied from the PoC test below as follows:
frame_dig -1
intc_0 // 0
extract_uint16
dupn 2
intc_1 // 1
- <--- Error
swap
// contracts/library/UInt64SetLib.py:34
// if items.length == 0:
bnz remove_item_after_if_else@2
That “frame dig ‑1” error is coming from this line:
last_idx = items.length - 1
When items.length
is 0, you end up emitting TEAL that does:
// push items.length (0)
extract_uint16
dup
intc 1
-
swap
…
Which effectively computes 0 - 1
in unsigned land, underflows, and blows up with “frame dig ‑1”.
Impact Details
Any consumer of remove_item
that passes an empty array will experience an immediate revert. This can lead to:
Unexpected Denial-of-Service in higher-level logic that relies on safe removal.
Forcing callers to add additional pre-checks, undermining the library’s usability guarantees.
Remediation
Add an explicit guard against an empty array at the top of remove_item
, for example:
if items.length == 0:
return Bool(False), items.copy()
last_idx = items.length - 1
References
Proof of Concept
Proof of Concept
In tests/library/UInt64SetLib.test.ts
append the following test under the describe("remove item")
block:
// filepath: tests/library/UInt64SetLib.test.ts
describe("remove item", () => {
// …
test("fails when removing from empty array", async () => {
// calling removeItem on [] should revert
await expect(
client.removeItem({ args: [34n, []] })
).toThrow();
});
});
Was this helpful?