Platform: Code4rena
Start Date: 05/07/2023
Pot Size: $390,000 USDC
Total HM: 136
Participants: 132
Period: about 1 month
Judge: LSDan
Total Solo HM: 56
Id: 261
League: ETH
Rank: 72/132
Findings: 2
Award: $182.52
🌟 Selected for report: 0
🚀 Solo Findings: 0
🌟 Selected for report: 0xfuje
Also found by: Vagner, hassan-truscova, nadin
141.3621 USDC - $141.36
https://github.com/Tapioca-DAO/tap-token-audit/blob/main/contracts/twAML.sol#L6-L106
The FullMath abstract contract was taken from Uniswap v3-core. However, the original solidity version that was used was < 0.8.0, meaning that the execution didn't revert when an overflow was reached. This effectively means that when a phantom overflow (a multiplication and division where an intermediate value overflows 256 bits) occurs the execution will revert and the correct result won't be returned. The original library was designed in a way that could handle intermediate overflows.
The correct result isn't returned in this case and the execution gets reverted when a phantom overflows occurs because the solidity version is "pragma solidity ^0.8.18;".
Hence, The FullMath contract doesn't correctly handle the case when an intermediate value overflows 256 bits. This happens because an overflow is desired in this case but it's never reached.
function muldiv( uint256 a, uint256 b, uint256 denominator ) internal pure returns (uint256 result) { // Handle division by zero require(denominator > 0); // 512-bit multiply [prod1 prod0] = a * b // Compute the product mod 2**256 and mod 2**256 - 1 // then use the Chinese Remainder Theorem to reconstruct // the 512 bit result. The result is stored in two 256 // variables such that product = prod1 * 2**256 + prod0 uint256 prod0; // Least significant 256 bits of the product uint256 prod1; // Most significant 256 bits of the product assembly { let mm := mulmod(a, b, not(0)) prod0 := mul(a, b) prod1 := sub(sub(mm, prod0), lt(mm, prod0)) } ..... ..... result = prod0 * inv; return result; }
As noted in the FullMath library, it was designed in order to handle "phantom overflow" i.e., allows multiplication and division where an intermediate value overflows 256 bits. This means that the original library can handle statements like :
FullMath.mulDiv(type(uint256).max / 2, 3, 111));
This a feature of the library, as statements like this cannot be handled by solidity >0.8 directly as the execution will revert due to an overflow. Because the original library was created with solidity version <0.8.0 (which doesn't revert on overflows) this behaviour was allowed as the expected intermediatory overflow could be reached.
However, due to the fact that the FullMath Library was ported to solidity version 0.8.18 and above, which is >0.8, this operation would revert as the intermediate calculations would overflow, meaning that it can't handle those multiplication and division where an intermediate value overflows the 256 bits.
Manual review + in-house tool
The fix is to mark the full body in an unchecked block, in order to leverage the fact that the original version was designed in order to allow the intermediate overflow. A modified version of the original Fullmath library that uses unchecked blocks to handle the intended overflow, can be found in the 0.8 branch of the Uniswap v3-core repo.
Under/Overflow
#0 - c4-pre-sort
2023-08-05T10:52:14Z
minhquanym marked the issue as duplicate of #138
#1 - c4-judge
2023-09-19T16:35:57Z
dmvt marked the issue as satisfactory