The $223 million Cetus Protocol exploit of May 2025 was caused not by a bug in Cetus’s own contracts but by a flaw in a shared math library called integer-mate. Specifically, a function named checked_shlw — designed to safely handle 256-bit left-shift operations — contained an incorrect overflow check that allowed corrupted intermediate values to propagate through liquidity calculations. This tutorial teaches you how to systematically test any smart contract math library for similar vulnerabilities, using techniques that go beyond standard auditing to catch the edge cases that formal reviews miss.
The Objective
By the end of this tutorial, you will be able to build a comprehensive test suite for any fixed-point math library used in DeFi protocols. You will learn how to identify boundary conditions in integer operations, construct targeted test vectors that probe overflow and underflow edges, use fuzzing to discover unexpected failure modes, and implement automated continuous testing that catches regressions before they reach production.
This tutorial assumes familiarity with smart contract development and basic testing frameworks. The techniques apply across programming languages — Move, Solidity, Rust — though specific code examples will use Move, given the Cetus context.
Prerequisites
Before beginning, ensure you have the following tools and knowledge in place.
A working Move development environment with the Sui CLI installed and configured. The Move Prover, a formal verification tool for Move programs, should be installed and accessible. A fuzzing framework — for Move, this includes move-cli’s built-in fuzzing support or custom property-based testing tools. Understanding of fixed-point arithmetic: how fractional numbers are represented using integer operations, and why precision and overflow management matter. Familiarity with concentrated liquidity market maker (CLMM) math: tick calculations, square root price representations, and liquidity delta computations.
If you are new to fixed-point arithmetic in smart contracts, spend time understanding how the Q64.64 representation works — this is the format at the center of the Cetus vulnerability. In Q64.64, a 128-bit number stores 64 bits of integer part and 64 bits of fractional part. When operations require intermediate values larger than 128 bits, implementations typically promote to 256-bit arithmetic — and this is where overflow risks emerge.
Step-by-Step Walkthrough
Step one: Map all arithmetic operations in the library. Begin by creating a complete inventory of every function in the math library that performs multiplication, division, or bit-shifting operations. For each function, document the input types, the expected output range, and any intermediate values that exceed the input bit width.
For the checked_shlw function, this mapping would reveal that the function takes a u256 input and attempts to left-shift it by 64 bits. The expected output range requires that the input value has no non-zero bits in its top 64 positions — otherwise, the shift would lose information through truncation.
Step two: Identify boundary values. For each arithmetic function, compute the exact boundary values where behavior should change. For a function that checks overflow before a left shift by N bits, the boundary is the largest value where the top N bits are all zero. Any value with a non-zero bit in the top N positions should trigger the overflow check.
In the Cetus case, the boundary for a 64-bit left shift is 2 to the power of 192 — any u256 value at or above this threshold would lose information when shifted. The checked_shlw function compared against a specific constant, and the bug was that this constant was incorrect.
Step three: Build targeted test vectors. Create test cases for every boundary condition you identified. Include the exact boundary value, the value one below it, and the value one above it. Add values with specific bit patterns — for example, a value with a single bit set in each of the top 64 positions individually.
// Example test vectors for checked_shlw
const MAX_SAFE: u256 = (1 << 192) - 1; // Largest safe value
const MIN_UNSAFE: u256 = (1 << 192); // Smallest unsafe value
const HIGH_BIT_ONLY: u256 = 1 << 255; // Only the highest bit set
const ALL_TOP_64: u256 = 0xFFFFFFFFFFFFFFFF << 192; // All top 64 bits set
Step four: Verify behavior at every boundary. For each test vector, confirm that the function’s behavior matches the specification exactly. The function should return a successful shifted result for all safe values and an overflow indicator for all unsafe values.
Step five: Implement property-based fuzzing. Targeted boundary tests are necessary but not sufficient — they only test values you thought to check. Property-based fuzzing generates random inputs and verifies that certain invariants hold for all of them.
For a checked shift function, the key invariant is: for any input value where the shift succeeds, the output must equal the input shifted by the specified number of bits. If this invariant ever fails, you have found a bug.
Step six: Test integration with consuming code. Library functions are tested in isolation, but bugs often emerge at the integration boundary. Build integration tests that exercise the full calculation pipeline — from user input through library function calls to final output — using values that stress the boundary conditions.
Troubleshooting
If your fuzzing tests are not finding bugs, you may need to guide the fuzzer toward interesting values. Many generic fuzzers produce random u256 values that are uniformly distributed — but overflow bugs live at the extremes. Configure your fuzzer to bias toward values near bit boundaries, maximum and minimum representable values, and values with specific bit patterns like all-zeros, all-ones, or single-bit-set.
If the Move Prover fails to verify properties on complex math functions, simplify the specification. Start with the most basic invariant — such as “the output is always less than 2 to the 256” — and gradually add complexity. Complex specifications can cause the prover to time out without producing useful results.
If integration tests pass but you suspect vulnerabilities, add assertion checks at every intermediate computation step. The Cetus bug could have been caught by asserting that the liquidity amount credited to a position is proportional to the token deposit — a relationship that broke when the overflow corrupted the intermediate value.
Mastering the Skill
Mathematical correctness in smart contract libraries is a specialized discipline that combines numerical analysis, cryptography, and software engineering. To deepen your expertise, study the audit reports published by firms like Cyfrin, Trail of Bits, and OpenZeppelin — particularly their analyses of DeFi math libraries.
Contribute to open-source math libraries by writing test cases and fuzzing harnesses. The integer-mate library at the center of the Cetus exploit was open-source and could have benefited from broader community testing. By contributing tests to shared libraries, you help protect the entire ecosystem.
Finally, build a personal checklist for reviewing any math library before use: are all arithmetic operations bounds-checked? Are intermediate values promoted to sufficient bit width? Are boundary values tested? Is the library fuzzed? Is it formally verified? Has it been audited by an independent team?
The $223 million Cetus exploit was preventable. The bug was in a simple function — a single incorrect comparison in a shared library. By applying the systematic testing approach described in this tutorial, similar vulnerabilities can be caught before they reach production. The investment in testing is always less than the cost of an exploit.
Disclaimer: This article is for educational purposes only and does not constitute security advice. Always engage qualified security professionals for production code audits.
the fact that a single incorrect overflow check in checked_shlw could cascade into a $223M loss is a brutal reminder that shared libraries are a single point of failure in DeFi
shared libraries are the supply chain problem of DeFi. one bug in a dependency and every protocol importing it is exposed. dependency audits should be standard
been saying this for months. everyone audits their own contracts but nobody audits the dependencies. integer-mate was used across multiple Sui protocols
the fuzzing section is solid. we ran similar tests on our fixed-point lib after Cetus and found 2 edge cases our manual review missed. this should be standard practice
fuzzing catching what manual review misses is the whole point. the Cetus team probably reviewed checked_shlw carefully but fuzzing would have hit the actual edge case
fuzz_me_ we ran similar fuzzing after Cetus and found 3 edge cases in our own math lib. fuzzing catches what human review fundamentally cant
$223M from a single incorrect overflow check in checked_shlw. shared math libraries are the weakest link in every DeFi stack