The Cetus AMM $200M Hack: How a Flawed “Overflow” Check Led to Catastrophic Loss
On May 22, 2025, the Cetus AMM on the Sui Network suffered a devastating hack resulting in over $200 million in losses. This incident represents one of the most significant DeFi exploits in recent history, caused by a subtle but critical flaw in “overflow” protection. This analysis dissects the technical details of the exploit and examines when this issue was introduced, fixed, and re-introduced.
Executive Summary
The attacker exploited a vulnerability that truncates the most significant bits in a liquidity calculation function of Cetus AMM. This calculation is invoked when a user opens an LP position. When opening such position, a user can open a large or small position by specifying a “liquidity” parameter (what fraction of the pool you would like to get in return), and supplying the corresponding amount of tokens. By manipulating the liquidity parameter to an extremely high value, they caused an overflow in the intermediate calculations that went undetected due to a flawed truncation check. This allowed them to add massive liquidity positions with just 1 unit of token input, subsequently draining pools collectively containing hundreds of millions of dollars worth of token.
Note: the technical term for the issue is not “overflow”, but an MSB (most significant bits) truncation, but let’s call it “overflow” for simplicity.
The Attack Sequence
The attack unfolded in a carefully orchestrated sequence. Here’s an example of one such attack transaction (simplified):
- Flash Swap Initiation: The attacker borrowed 10 million haSUI through a flash swap with maximum slippage tolerance
- Position Creation: Opened a new liquidity position with tick range [300000, 300200] – an extremely narrow range at the upper bounds
- Liquidity Addition: Added liquidity with just 1 unit of token A, but received a massive liquidity value of 10,365,647,984,364,446,732,462,244,378,333,008. This action succeeded due to an undetected bitwise truncation.
- Liquidity Removal: Immediately removed the liquidity in multiple transactions, draining the pool
- Flash Loan Repayment: Repaid the flash swap and kept approximately 5.7 million SUI as profit
Technical Deep Dive: The “Overflow” Vulnerability
The root cause lies in the get_delta_a
function within clmm_math.move
, which calculates the amount of token A required for a given liquidity amount:
public fun get_delta_a(
sqrt_price_0: u128,
sqrt_price_1: u128,
liquidity: u128,
round_up: bool
): u64 {
let sqrt_price_diff = sqrt_price_1 - sqrt_price_0;
let (numberator, overflowing) = math_u256::checked_shlw(
// Dedaub: result doesn't fit in 192 bits
full_math_u128::full_mul(liquidity, sqrt_price_diff)
);
// Dedaub: checked_shlw "overflows" result, since it << 64
assert!(overflowing);
let denominator = full_math_u128::full_mul(sqrt_price_0, sqrt_price_1);
let quotient = math_u256::div_round(numberator, denominator, round_up);
(quotient as u64)
}
The Mathematical Breakdown
Using the actual values from the transaction:
liquidity
: 10,365,647,984,364,446,732,462,244,378,333,008 (approximately 2^113)sqrt_price_0
: 60,257,519,765,924,248,467,716,150 (tick 300000)sqrt_price_1
: 60,863,087,478,126,617,965,993,239 (tick 300200)sqrt_price_diff
: 605,567,712,202,369,498,277,089 (approximately 2^79)
The critical calculation:
numerator = checked_shlw(liquidity * sqrt_price_diff)
= checked_shlw(~2^113 * ~2^79)
= checked_shlw(2^192 + ε)
// checked_shlw shifts a 256-bit register by 64
= ((2^192 + ε) * 2^64) mod 2^256
= ε
This multiplication produces a result exceeding 192 bits. When this value is left-shifted by 64 bits in checked_shlw
(i.e., “checked shift left by one 64-bit word”) it overflows a 256-bit integer, but an overflow check designed for this check fails.
But wait. Isn’t a checked operation supposed to prevent this issue?
The Flawed Overflow Check
The critical flaw lies in the checked_shlw
function:
public fun checked_shlw(n: u256): (u256, bool) {
let mask = 0xffffffffffffffff << 192; // This is incorrect!
if (n > mask) {
(0, true)
} else {
((n << 64), false) // exact location of overflow
}
}
The mask calculation 0xffffffffffffffff << 192
doesn’t produce the intended result. The developers likely intended to check if n >= (1 << 192)
, but the actual mask doesn’t serve this purpose. As a result, most values greater than 2^192 pass through undetected, and the subsequent left shift by 64 bits causes a silent overflow in Move (which doesn’t trigger runtime errors for shift operations).
Integer Considerations
In Move, the security around integer operations is designed to prevent overflow and underflow which can cause unexpected behavior or vulnerabilities. Specifically:
- Additions (+) and multiplications (*) cause the program to abort if the result is too large for the integer type. An abort in this context means that the program will terminate immediately.
- Subtractions (-) abort if the result is less than zero.
- Division (/) abort if the divisor is zero.
- Left Shift (<<), uniquely, does not abort in the event of an overflow. This means if the shifted bits exceed the storage capacity of the integer type, the program will not terminate, resulting in incorrect values or unpredictable behavior.
It is normal for languages with checked arithmetic to not trigger errors when bit shifting truncates the result. Most smart contract auditors understand this.
The Exploitation Impact
Due to the overflow, the numerator
wraps around to a very small value. When divided by the denominator, it produces a quotient close to 0. This means the function returns that only 1 unit of token A is required to mint the massive liquidity position.
In mathematical terms:
- Expected: very larger number of tokens required
- Actual (due to overflow): 1 token required
It is worth noting that the numeric values involved in the attack are precisely calculated – the attacker utilized some existing functions in the contract to compute these, notably get_liquidity_from_a
.
The Audit Trail: Similar Issue Found Before
Ottersec’s audit identified an eerily similar overflow vulnerability in an earlier variant of the code (early 2023), specifically designed for Aptos:
“The numberator value is not validated before running u256::shlw on it. As a result, the non-zero bytes might be removed, which leads to an incorrect calculation of the value.”
They recommended replacing u256::shlw
with u256::checked_shlw
and adding overflow detection, which solved the issue. Note that this version of the code had custom implementations of 256-bit unsigned integer operations since Aptos didn’t support this naively at the time. Move 2 / Aptos CLI ≈ v1.10 rolled to mainnet early 2024.
It is really unfortunate that when the team ported the code to SUI a couple of months later (Sui always supported 256-bit integers), a bug was introduced in checked_shlw
. Audits to this version of the AMM by Ottersec and MoveBit do not find this issue. A subsequent audit by Zellic in April 2025 found no issues beyond informational findings. It is possible that library code performing numerical calculations were out of scope and moreover, since 256-bit operations are natively supported, issues like these could have been overlooked.
Lessons for Developers
1. Understand Your Language’s Integer Semantics
- Know which operations abort and which silently overflow
- Pay special attention to bit shift operations
- Test your overflow checks with actual overflow conditions
2. Mathematical Rigor is Non-Negotiable
- DeFi protocols need to handle extreme values by design
- The bounds of every mathematical operation need to be clearly understood
- Consider using formal methods for verifying critical calculations (our team can assist)
3. Test Edge Cases Exhaustively
- Maximum values aren’t theoretical – they’re attack vectors
- Combine multiple edge cases
4. Audit Fixes, Not Just Changes
- Consider independent verification of critical fixes
5. Domain Expertise Matters
- AMM mathematics involves complex invariants
- Work with auditors who understand DeFi edge cases
In DeFi, edge cases aren’t edge cases – they’re attack vectors. AMMs are particularly vulnerable as they involve complex mathematical operations across extreme ranges. The Cetus hack demonstrates that even “checked” operations require careful verification.
Conclusion
The Cetus hack serves as a stark reminder that security in DeFi is hard, but not impossible to achieve. A single flawed overflow check, combined with the composability of flash loans and concentrated liquidity mechanics, enabled the theft of over $200 million.
For developers building on Move-based chains like Sui and Aptos, this incident underscores the importance of understanding your language’s integer semantics, rigorously testing edge cases, and working with auditors who deeply understand both the platform and the DeFi domain.
Contact us at Dedaub if you need help securing your Aptos or Sui Network project – our team specializes in the mathematical complexities and edge cases that come up in complex DeFi protocols.