Mathematical primitives are a foundational component of DeFi protocols, regardless of language or execution environment. In the Sui ecosystem, this role is commonly fulfilled by the integer-mate library developed by Cetus Protocol. The library provides arithmetic helpers over u64 and u128 values, allowing intermediate computations to exceed the base type while requiring the final result to fit within the original integer width.
Due to its early adoption, the integer-mate implementation became a de facto standard and was reused across multiple protocols, including Dexlyn Labs’s CLMM_Dex, Bluefin’s integer-library, OmniBTC’s OmniSwap, and Turbos Finance’s turbos-sui-move-interface, each maintaining both u64 and u128 variants.
Among the provided helpers, mul_shl implements a multiply-then-left-shift pattern. The function widens operands to a larger integer type, performs multiplication, applies a left shift, and then coerces the result back into u64 or u128. While mul_shl is not heavily used in most code paths, its presence in a widely trusted math library makes its correctness non-negotiable.
In September 2025, with the release of version v1.3.0, both full_math_u64::mul_shl and full_math_u128::mul_shl were formally deprecated. The accompanying notice explains that the left-shift operation is performed without overflow checks, allowing intermediate values to overflow silently before narrowing. This behavior can lead to unexpected results, motivating the recommendation to avoid using the function altogether.
This function converts num1 and num2 to [u128 | u256], multiplies them, then left-shifts the result by the specified number of bits, and finally coerces the result into [u64 | u128]. The left shift does not perform overflow checks, so it is recommended not to use this function to avoid unexpected results.
Root Cause Analysis
The full_math_u64 and full_math_u128 libraries implement the mul_shl function aims to efficiently calculate (a * b) << c result. The libraries aim to implement safe math methods, avoiding acceptable parameters shrink and reverting in case of value overflow. However, the function does not follow the behavior and returns potentially inconsistent results.
The function returns a valid value if the result of (a * b) << c fits entirely within the base integer type, either u64 or u128. It aborts if any bits are set in the immediate overflow range, namely bits [64; 127] when operating on u64 or bits [128; 255] when operating on u128. If those ranges contain no set bits but higher-order bits beyond them are set, the function does not abort and instead returns a malformed value.
This happens due to the shift left operator being applied to the value expanded to a bigger type (either u128 or u256). The shift left operator silently drops the bits over 127 when operating on u64 or 255 when operating on u128. Later, the result value is wrapped to the u64 or u128 with a revert in case of overflow. Though, as certain bits are lost before the overflow check, the inconsistency appears.
public fun mul_shl(num1: u64, num2: u64, shift: u8): u64 {
// Shifts left the multiplication result dropping bits above 127
let r = full_mul(num1, num2) << shift;
// Wraps u128 to u64, reverts if any bits set in [64; 127] range
(r as u64)
}
public fun full_mul(num1: u64, num2: u64): u128 {
// Returns `num1 * num2` in u128, never reverts
((num1 as u128) * (num2 as u128))
}
public fun mul_shl(num1: u128, num2: u128, shift: u8): u128 {
// Shifts left the multiplication result dropping bits above 255
let r = full_mul(num1, num2) << shift;
// Wraps u256 to u128, reverts if any bits set in [128; 255] range
(r as u128)
}
public fun full_mul(num1: u128, num2: u128): u256 {
// Returns `num1 * num2` in u256, never reverts
(num1 as u256) * (num2 as u256)
}

Acceptable parameters range (figure built for equal implementation based on u5 type):
- Green: (a * b) << c fully fits u64.
- Red: the parameters leading to invalid results.
- Orange: parameters accepted by naive (a * b) << c (multiplication overflow reverts, shift left ignores overflow), but not accepted by the mul_shl function.
It can be seen that the function is not consistent: certain acceptable parameters appear in random points of the plot and do not wrap a certain figure. Chaotic distribution is a clear sign of issues in the function design.
Impact Assessment
There is no usage of the mul_shl function within any open source protocols according to GitHub search. The function does not have a good use case and was implemented just as a pair to mul_shr which is commonly used in DEX’s math to compute square price at specified tick.
Unlike mul_shl, the mul_shr function is safe. In mul_shl, left shifts increase the significance of bits, and the unexpected truncation of the most important bits causes malformed results. In contrast, mul_shr performs a right shift, which may truncate lower-order bits, but this behavior is consistent because shifting right reduces the significance of bits, so the less important ones are safely discarded.
Though, the mul_shl function invalid behavior does not pose a significant risk. The function is already deprecated in its original implementation, however, as the code was copied in certain external protocols, caution should be exercised.
Summary
The mul_shl function in the integer-mate library was designed to compute (a * b) << c by widening operands, multiplying, and left-shifting the result. However, because the left shift is applied to a larger intermediate type without proper overflow checks, significant bits might be silently truncated before narrowing back to the original integer width. This inconsistency can produce malformed results for certain input ranges, even though some overflows are caught. For this reason, mul_shl was formally deprecated in version v1.3.0 of the library.
To safely achieve multiplication followed by a left shift, developers are encouraged to implement custom solutions depending on the specific requirements. The naive (a * b) << c can be used if the shift is architecturally justified and not just a quick multiplication-by-power-of-two shortcut. If overflow must be prevented, an explicit check can be added, for example: assert!(shifted >> c == mul), where mul = a * b and shifted = mul << c. This ensures that no significant bits are lost during the shift.
Appendix. Implementation References
- CetusProtocol/integer-mate [u64] and CetusProtocol/integer-mate [u128]
- DexlynLabs/CLMM_Dex [u64] and DexlynLabs/CLMM_Dex [u128]
- fireflyprotocol/integer-library [u64] and fireflyprotocol/integer-library [u128]
- OmniBTC/OmniSwap [u64] and OmniBTC/OmniSwap [u128]
- turbos-finance/turbos-sui-move-interface [u64] and turbos-finance/turbos-sui-move-interface [u128]
Appendix. Tests demonstrating Malformed Behavior
#[test, expected_failure]
public fun test_mul_shl_revert() {
let res = full_math_u64::mul_shl(0xffffffff, 0xffffffff, 64);
// (0xffffffff * 0xffffffff) << 64 = 0xfffffffe000000010000000000000000
// 0xfffffffe000000010000000000000000 % (2 ** 128) = 0xfffffffe000000010000000000000000
// The function reverts because 0xfffffffe000000010000000000000000 > 0xffffffffffffffff
}
#[test]
public fun test_mul_shl_pass_zero_result() {
let res = full_math_u64::mul_shl(0x100000000, 0x100000000, 64);
// (0x100000000 * 0x100000000) << 64 = 0x100000000000000000000000000000000
// 0x100000000000000000000000000000000 % (2 ** 128) = 0x0
// The function returns 0x0 because 0x0 <= 0xffffffffffffffff
assert!(res == 0x0, 1);
}
#[test]
public fun test_mul_shl_pass_positive_result() {
let res = full_math_u64::mul_shl(0xffffffffffffffff, 0x8000000000000001, 1);
// (
#[test, expected_failure]
public fun test_mul_shl_revert() {
let res = full_math_u128::mul_shl(0xffffffffffffffff, 0xffffffffffffffff, 128);
// (0xffffffffffffffff * 0xffffffffffffffff) << 128 =
// 0xfffffffffffffffe000000000000000100000000000000000000000000000000
// 0xfffffffffffffffe000000000000000100000000000000000000000000000000 % (2 ** 256) =
// 0xfffffffffffffffe000000000000000100000000000000000000000000000000
// The function reverts because
// 0xfffffffffffffffe000000000000000100000000000000000000000000000000
// > 0xffffffffffffffffffffffffffffffff
}
#[test]
public fun test_mul_shl_pass_zero_result() {
let res = full_math_u128::mul_shl(0x10000000000000000, 0x10000000000000000, 128);
// (0x10000000000000000 * 0x10000000000000000) << 128 =
// 0x10000000000000000000000000000000000000000000000000000000000000000
// 0x10000000000000000000000000000000000000000000000000000000000000000 % (2 ** 256) =
// 0x0
// The function returns 0x0 because
// 0x0 <= 0xffffffffffffffffffffffffffffffff
assert!(res == 0x0, 1);
}
#[test]
public fun test_mul_shl_pass_positive_result() {
let res = full_math_u128::mul_shl(
0xffffffffffffffffffffffffffffffff,
0x80000000000000000000000000000001,
1
);
// (0xffffffffffffffffffffffffffffffff *
// 0x80000000000000000000000000000001) << 1 =
// 0x100000000000000000000000000000000fffffffffffffffffffffffffffffffe
// 0x100000000000000000000000000000000fffffffffffffffffffffffffffffffe % (2 ** 256) =
// 0xfffffffffffffffffffffffffffffffe
// The function returns
// 0xfffffffffffffffffffffffffffffffe
// because it is <= 0xffffffffffffffffffffffffffffffff
assert!(res == 0xfffffffffffffffffffffffffffffffe, 1);
}
Appendix. Acceptable Parameters Range Plot Generation
import matplotlib as mpl
import matplotlib.pyplot as plt
import numpy as np
s = 5
x, y, z = np.indices((2 ** s - 1, 2 ** s - 1, 2 * s - 1))
fig_all = ((x * y * 2 ** z) % (2 ** s)) == ((x * y * 2 ** z) % (2 ** (2 * s)))
fig_shift_overflow = ((x * y) < (2 ** s))
fig_no_overflow = (x * y * 2 ** z) < (2 ** s)
voxelarray = fig_shift_overflow | fig_all | fig_no_overflow
colors = np.empty(voxelarray.shape, dtype=object)
colors[fig_shift_overflow] = 'orange'
colors[fig_all] = 'red'
colors[fig_no_overflow] = 'green'
fig = plt.figure(figsize=(10, 8))
ax = fig.add_subplot(projection='3d')
v = 0.32
xs, ys, zs = np.where(voxelarray)
for xi, yi, zi in zip(xs, ys, zs):
c = colors[xi, yi, zi]
ax.bar3d(
xi,
yi,
zi,
v,
v,
v * (2 * s - 1) / (2 ** s - 1),
color=c,
edgecolor='none',
shade=False
)
ax.set_box_aspect([1, 1, 1])
ax.axes.xaxis.set_ticklabels([])
ax.axes.yaxis.set_ticklabels([])
ax.axes.zaxis.set_ticklabels([])
mpl.rcParams['axes3d.mouserotationstyle'] = 'azel'
plt.show()



