Beginner FriendlyFoundry
100 EXP
View results
Submission Details
Severity: high
Valid

The `MathMasters::mulWadUp` function gives incorrect result for some values of `x` and `y`

Summary

The MathMasters::mulWadUp function should calculates the expression (x * y) / 1e18 and to round the result up. The function correctly check if there is a remainder and add 1 if it is necessary. But by some values for x and y the function increments x with 1 which leads to incorrect calculation and incorrect final result of z.

Vulnerability Details

The function MathMasters::mulWadUp accepts two input parameters (uint256 x and uint256 y) and calculates the expression x * y / 1e18 and rounds the result up.

There is a if statement in the function that increments the x value with 1. Maybe, the reason for doing that is the result to be rounded up, but this is incorrect.

/// @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))) {
mstore(0x40, 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))
}
}

Let's take a look to the if statement that increments the value of x: iszero(sub(div(add(z, x), y), 1)).

  • add(z, x): This add z and x. But z is supposed to be the result from the MathMasters::mulWadUp function, it is not initialized and therefore its value is 0. So, this addition is useless.

  • div(add(z, x), y): This divides the result of addition that is x by the value of y.

  • sub(div(add(z, x), y), 1): This subtracts the result of division by 1.

  • iszero(sub(div(add(z, x), y), 1)): This checks if the result after subtraction is 0. If it is 0, the if statement will be true and the x value will be incremented by 1.

When this result can be 0?

  • If the values of x and y are equal.

  • If the value of x is slightly greater than the value of y but not enough to make the division result reach 2 when rounded down to the nearest integer.

In theese cases the value of x will be incremented by 1. But this incremention leads to incorrect final result.
In the last line of the function is assigned the final result to the variable z: z := add(iszero(iszero(mod(mul(x, y), WAD))), div(mul(x, y), WAD)). Also, there is a check if the result of the expression (x * y) / WAD has a remainder. If there is a remainder, 1 is added to round up the result.

Impact

Let's consider the following scenario:

The values of x and y are both equal to 3e18. The following test function testMulWadUpCalculation calculates the expected solution and compares it to the solution retrieved from the MathMasters::mulWadUp function. You can add this test function in the MathMasters.t.sol file and execute it with the Foundry command: forge test --match-test "testMulWadUpCalculation"

function testMulWadUpCalculation() public {
uint256 solution = (3e18 * 3e18) / 1e18;
if (solution * 1e18 < 3e18 * 3e18) {
solution += 1;
}
assertEq(MathMasters.mulWadUp(3e18, 3e18), solution);
}

Test fails:

[FAIL. Reason: assertion failed] testMulWadUpCalculation() (gas: 21368)
Logs:
Error: a == b not satisfied [uint]
Left: 9000000000000000003
Right: 9000000000000000000

The result from the MathMasters::mulWadUp function is 9000000000000000003 and the expected result is 9000000000000000000. This difference is because the incorrect incrementation of the value of x.

The following test in Halmos found also the case by which the value of x is slightly greater than the value of y but not enough to make the division result reach 2 when rounded down to the nearest integer. Halmos is a great tool for formal verification. To execute this test you should have Halmos installed and import the SymTest from lib/halmos-cheatcodes/src/SymTest.sol. Then you can use the command to execute the test function: halmos --function check_check_MulWadUp --solver-timeout-assertion 0.

function check_MulWadUp(uint128 x, uint128 y) public {
unchecked {
if (x != 0 && (x * y) / x != y) return;
}
uint128 solution = x * y / 1e18;
if (solution * 1e18 < x * y) {
solution += 1;
}
uint256 mathSolution = MathMasters.mulWadUp(x,y);
assertEq(solution, mathSolution);
}

The input arguments for the functions are of type uint128, because with the type uint256 the result was Killed.
So, the Halmos found the counterexample: x = 20901944742440337407 and y = 15364007485707028177. I stoped Halmos after the first found example because it took about 30 minutes. After that, I decided to compare the results from MathMasters::mulWadUp function and Solody::mulWadUp function. The following test demonstrates this, again with the help of Halmos:

function testCheck__MulWadUpEquivalence(uint256 x, uint256 y) public {
uint256 mathMasters = MathMasters.mulWadUp(x, y);
//mulWadUp2 is the name of `Solady::mulWadUp` function in the `MathMasters.sol` file
uint256 solady = MathMasters.mulWadUp2(x, y);
assertEq(mathMasters, solady);
}

This time the type of the input arguments is not changed and the result is:

p_x_uint256 = 0x000000000000000000000000000000013806bd089fb6d2f4ec4bba7f87c865be (414754122524714488867341405485055632830)
p_y_uint256 = 0x00000000000000000000000000000000ccf76b10a93fec70b20605325b922003 (272447180002039376997384714789787410435)
[FAIL] testCheck__MulWadUpEquivalence(uint256,uint256) (paths: 9, time: 1714.20s, bounds: [])

All tests show that the function MathMasters::mulWadUp calculates the expression (x * y) / 1e18 incorrectly due to the unnecessary increment of x.

Tools Used

VS Code, Foundry, Halmos

Recommendations

Remove the if statement that increments the value of x.

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))) {
mstore(0x40, 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))
}
}
Updates

Lead Judging Commences

inallhonesty Lead Judge over 1 year ago
Submission Judgement Published
Validated
Assigned finding tags:

`mulWadUp` has an unnecessary line that makes the result wrong for some inputs

Support

FAQs

Can't find an answer? Chat with us on Discord, Twitter or Linkedin.