Summary
logic flaw in mulWadUp
function cause producing wrong results in certain cases.
Vulnerability Details
Library MathMasters
contains a given function
function mulWadUp(uint256 x, uint256 y) internal pure returns (uint256 z) {
assembly {
@> if mul(y, gt(x, or(div(not(0), y), x))) {
@> mstore(0x40, 0xbac65e5b)
revert(0x1c, 0x04)
}
@> if iszero(sub(div(add(z, x), y), 1)) { x := add(x, 1) }
z := add(iszero(iszero(mod(mul(x, y), WAD))), div(mul(x, y), WAD))
}
}
The given function is used to return ( x * y ) / WAD
rounded up. With current code it does so in most cases.
if we check line if mul(y, gt(x, or(div(not(0), y), x)))
. It has or
operation which will cause to overflow the multiplication of (x * y).
Let's check the other marked line if iszero(sub(div(add(z, x), y), 1)) { x := add(x, 1) }
, it
add(z, x): Adds the values of z and x.
div(add(z, x), y): Divides the result of the addition by the value of y.
sub(div(add(z, x), y), 1): Subtracts 1 from the result of the division.
iszero(...): Checks if the result of the subtraction is zero.
if iszero(...) { x := add(x, 1) }: If the result is zero, it increments the value of x by 1.
This statement cause issue to final result in many cases, which can be verified via fuzz testing.
it fails miserably when we do fuzz testing.
POC
In foundry.toml
update the fuzz runs to 1000000
let's test if reverts when (x * y) overflows.
In existing testsuite, write following test
function testOverFlowInMulWadUp () public {
uint256 x = 2;
uint256 y = type(uint256).max;
uint256 result = MathMasters.mulWadUp(x, y);
console2.log("Result:", result);
}
run forge test --mt testOverFlowInMulWadUp -vv
in your terminal and it will print following results.
Running 1 test for test/MathMasters.t.sol:MathMastersTest
[PASS] testOverFlowInMulWadUp() (gas: 3614)
Logs:
Result: 115792089237316195423570985008687907853269984665640564039458
Test result: ok. 1 passed; 0 failed; 0 skipped; finished in 429.63µs
then run forge --mt testMulWadFuzz -vv
in your terminal, to test the scenarios where it fails to hold condition
uint256 expected = x * y == 0 ? 0 : (x * y - 1) / 1e18 + 1
results shows like this --
[FAIL. Reason: assertion failed; counterexample: calldata=0xfb6ea94f00000000000000000000000000000000000000000002bfc66f73219d55eb3274000000000000000000000000000000000000000000015fe337b990ceaaf5993b args=[3323484123583475243233908 [3.323e24], 1661742061791737621616955 [1.661e24]]] testMulWadUpFuzz(uint256,uint256) (runs: 3979, μ: 921, ~: 1167)
Encountered a total of 1 failing tests, 0 tests succeeded
we run test again, just to double confirm
we get
Test result: FAILED. 0 passed; 1 failed; 0 skipped; finished in 179.89ms
Ran 1 test suites: 0 tests passed, 1 failed, 0 skipped (1 total tests)
Failing tests:
Encountered 1 failing test in test/MathMasters.t.sol:MathMastersTest
[FAIL. Reason: assertion failed; counterexample: calldata=0xfb6ea94f00000000000000000000000000000000000000000002bfc66f73219d55eb3274000000000000000000000000000000000000000000015fe337b990ceaaf5993b args=[3323484123583475243233908 [3.323e24], 1661742061791737621616955 [1.661e24]]] testMulWadUpFuzz(uint256,uint256) (runs: 3979, μ: 921, ~: 1167)
Encountered a total of 1 failing tests, 0 tests succeeded
Impact
Wrong results will lead to make this library useless and projects which using this can have loss of funds
Tools Used
Foundry, Manual Review
Recommendations
Here is the updated snippet to fix the code
/// @dev Equivalent to `(x * y) / WAD` rounded up.
function mulWadUp(uint256 x, uint256 y) internal pure returns (uint256 z) {
/// @solidity memory-safe-assembly
assembly {
// Equivalent to `require(y == 0 || x <= type(uint256).max / y)`.
- if mul(y, gt(x, or(div(not(0), y), x))) {
+ if mul(y, gt(x, div(not(0), y))){
- mstore(0x40, 0xbac65e5b) // `MathMasters__MulWadFailed()`.
+ mstore(0x00, 0xbac65e5b) // `MathMasters__MulWadFailed()`.
revert(0x1c, 0x04)
}
- if iszero(sub(div(add(z, x), y), 1)) { x := add(x, 1) }
z := add(iszero(iszero(mod(mul(x, y), WAD))), div(mul(x, y), WAD))
}
}