Summary
There is a division before multiplication at LibFertilizer:remainingRecapitalisation()
, which will cause the protocol to enable the mint of less fertilizer than intended. This will result in less value recapitalized than lost and therefor loss of value for the holders of UNRIPE_LP
tokens.
Vulnerability Details
When calling LibFertilizer::remainingRecapitalization()
the amount of totalDollars
this calculated that still need to be recapitalized:
uint256 totalDollars = C.dollarPerUnripeLP().mul(C.unripeLP().totalSupply()).div(DECIMALS);
For this the function C.dollarPerUnripeLP
is used invoking a division operation before the following multiplication, leading to precision loss:
function dollarPerUnripeLP() internal pure returns (uint256) {
return 1e12/UNRIPE_LP_PER_DOLLAR;
}
POC
Add this file to test folder and run forge test -vv --mt test_precision_loss
pragma solidity =0.7.6;
import {SafeMath} from "openzeppelin/utils/math/SafeMath.sol";
import "forge-std/Test.sol";
import "forge-std/console.sol";
contract TestPrecisionLoss is Test {
using SafeMath for uint256;
function setUp() public {}
function test_precision_loss() external {
uint256 UNRIPE_LP_TOTAL_SUPPLY = 96033202232306;
uint128 DECIMALS = 1e6;
uint256 UNRIPE_LP_PER_DOLLAR = 1884592;
uint256 dollarPerUnripeLP = 1e12 / UNRIPE_LP_TOTAL_SUPPLY;
uint256 totalDollars1 = (1e12 / UNRIPE_LP_PER_DOLLAR)
.mul(UNRIPE_LP_TOTAL_SUPPLY)
.div(DECIMALS);
console.log("totalDollars1: ", totalDollars1);
uint256 totalDollars2 = (1e12 * UNRIPE_LP_TOTAL_SUPPLY)
.div(UNRIPE_LP_PER_DOLLAR)
.div(DECIMALS);
console.log("totalDollars2: ", totalDollars2);
console.log("the diff: ", totalDollars2 - totalDollars1);
}
}
The output of the test is the following:
Running 1 test for test/testPrecisionLoss.sol:TestPrecisionLoss
[PASS] test_precision_loss() (gas: 5691)
Logs:
totalDollars1: 50956945702101
totalDollars2: 50957025304313
the diff: 79602212
Impact
remainingRecapitalization()
will always return lesser value than it should be. This will lead to less fertilizer available for mint and therefore less USD value that will get recapitalized for the UNRIPE_LP
holders. Here is the related code in FertilizerFacet::mintFertilizer()
:
uint128 remaining = uint128(LibFertilizer.remainingRecapitalization().div(1e6));
require(fertilizerAmountOut <= remaining, "Fertilizer: Not enough remaining.");
Tools Used
Foundry
Recommendations
--uint256 totalDollars = C.dollarPerUnripeLP().mul(C.unripeLP().totalSupply()).div(DECIMALS);
++uint256 totalDollars = (1e12).mul(C.unripeLP().totalSupply()).div(C.unripeLPPerDollara()).div(DECIMALS);