Incorrect Felt Comparison
In Cairo, the less than or equal to comparison operator has two methods: assert_le
and assert_nn_le
:
assert_le
asserts that a numbera
is less than or equal tob
, regardless of the size ofa
assert_nn_le
additionally asserts thata
is non-negative, essentially meaning it should be less than or equal to theRANGE_CHECK_BOUND
value of2^128
.
assert_nn_le
is suitable for comparing unsigned integers with values smaller than 2^128
(e.g., an Uint256 field). To compare felts as unsigned integers over the entire range (0, P], use assert_le_felt
. These functions also exist with the is_
prefix, where they return 1 (TRUE) or 0 (FALSE).
One common mistake resulting from the complexity of these assertions is using assert_le
instead of assert_nn_le
.
Example
Consider the example of a codebase that uses the following checks concerning a hypothetical ERC20 token. The first function may incorrectly pass the assertion even if the value
is greater than max_supply
, because the function does not verify that value >= 0
. The second function, however, asserts that 0 <= value <= max_supply
, which will correctly prevent an incorrect value
from passing the assertion.
@storage_var
func max_supply() -> (res: felt) {
}
@external
func bad_comparison{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}() {
let (value: felt) = ERC20.total_supply();
assert_le{range_check_ptr=range_check_ptr}(value, max_supply.read());
// do something...
return ();
}
@external
func better_comparison{syscall_ptr : felt*, pedersen_ptr : HashBuiltin*, range_check_ptr}() {
let (value: felt) = ERC20.total_supply();
assert_nn_le{range_check_ptr=range_check_ptr}(value, max_supply.read());
// do something...
return ();
}
Mitigations
- Carefully review all felt comparisons.
- Determine the desired behavior of the comparison and decide if
assert_nn_le
orassert_le_felt
is more appropriate. - Use
assert_le
if you explicitly want to compare signed integers. Otherwise, clearly document why it is used overassert_nn_le
.