Skip to content

Commit 43aeb0c

Browse files
authored
fix(invariants): account for market share price rounding (#318)
1 parent 9100b28 commit 43aeb0c

2 files changed

Lines changed: 30 additions & 2 deletions

File tree

test/invariants/LidoARM/FuzzerFoundry.t.sol

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,25 @@ contract FuzzerFoundry_LidoARM is Properties {
120120
assertEq(maxWethBalanceDrift(), 0, "unaccounted WETH drift");
121121
}
122122

123+
function test_setActiveMarketAllowsTrackedThreeWeiRoundingLoss() public {
124+
uint256 targetArmWeth = 1_725_354_192_264_043_820;
125+
targetDonate(targetArmWeth - weth.balanceOf(address(lidoARM)), 0);
126+
127+
// At this market share price, depositing all ARM liquidity loses 3 wei when the minted
128+
// market shares are converted back to assets. This is within one market share of rounding.
129+
deal(address(weth), address(mockERC4626Market_A), 21_842_260_027_410_998_593_879);
130+
131+
uint256 assetsBefore = lidoARM.totalAssets();
132+
uint256 priceBefore = assetsBefore * 1e18 / lidoARM.totalSupply();
133+
targetSetActiveMarket(0);
134+
uint256 assetsAfter = lidoARM.totalAssets();
135+
uint256 priceAfter = assetsAfter * 1e18 / lidoARM.totalSupply();
136+
137+
assertEq(assetsBefore - assetsAfter, 3, "market rounding loss");
138+
assertEq(priceBefore - priceAfter, 3, "share price rounding loss");
139+
assertEq(sum_weth_marketRoundingLoss, 3, "tracked market rounding loss");
140+
}
141+
123142
/// @notice Optimization: fuzzer maximizes the worst-case LP rounding loss.
124143
function invariant_optimize_maxLpLoss() public view returns (int256) {
125144
return maxLpLoss();

test/invariants/LidoARM/helpers/Helpers.t.sol

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ pragma solidity 0.8.23;
44
import {Vm} from "forge-std/Vm.sol";
55
import {Base_Test_} from "../base/Base.t.sol";
66
import {IERC4626} from "@openzeppelin/contracts/interfaces/IERC4626.sol";
7+
import {Math} from "@openzeppelin/contracts/utils/math/Math.sol";
78

89
// Mocks
910
import {MockERC20} from "@solmate/test/utils/mocks/MockERC20.sol";
@@ -17,11 +18,19 @@ abstract contract Helpers is Base_Test_ {
1718
////////////////////////////////////////////////////
1819

1920
modifier ensureSharePriceNotDecreased() {
21+
uint256 marketRoundingLossBefore = sum_weth_marketRoundingLoss;
2022
uint256 priceBefore = lidoARM.totalAssets() * 1e18 / lidoARM.totalSupply();
2123
_;
22-
uint256 priceAfter = lidoARM.totalAssets() * 1e18 / lidoARM.totalSupply();
24+
uint256 totalSupplyAfter = lidoARM.totalSupply();
25+
uint256 priceAfter = lidoARM.totalAssets() * 1e18 / totalSupplyAfter;
2326
// Allow 2 wei tolerance for ERC4626 convertToAssets rounding on split operations
24-
require(priceAfter + 2 >= priceBefore, "SHARE_PRICE_DECREASED");
27+
// plus the share-price impact of any market rounding loss tracked by the inner modifier.
28+
if (priceAfter < priceBefore) {
29+
uint256 marketRoundingLoss = sum_weth_marketRoundingLoss - marketRoundingLossBefore;
30+
uint256 marketRoundingPriceTolerance =
31+
Math.mulDiv(marketRoundingLoss, 1e18, totalSupplyAfter, Math.Rounding.Ceil);
32+
require(priceBefore - priceAfter <= 2 + marketRoundingPriceTolerance, "SHARE_PRICE_DECREASED");
33+
}
2534
ghost_lastSharePrice = priceAfter;
2635
}
2736

0 commit comments

Comments
 (0)