Skip to content

Commit 4bb3249

Browse files
authored
feat: use msg.sender in clamp to not block determin action function (#719)
1 parent 6340e5b commit 4bb3249

File tree

2 files changed

+375
-2
lines changed

2 files changed

+375
-2
lines changed

contracts/liquidityStrategies/OpenLiquidityStrategy.sol

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,10 @@ contract OpenLiquidityStrategy is IOpenLiquidityStrategy, LiquidityStrategy {
7777
uint256 idealDebtToExpand,
7878
uint256 idealCollateralToPay
7979
) internal view override returns (uint256 debtToExpand, uint256 collateralToPay) {
80+
// If the caller is the zero address, return the ideal amounts used for external view call to determineAction
81+
if (msg.sender == address(0)) return (idealDebtToExpand, idealCollateralToPay);
8082
address debtToken = ctx.debtToken();
81-
uint256 debtBalance = IERC20(debtToken).balanceOf(_getRebalancer());
83+
uint256 debtBalance = IERC20(debtToken).balanceOf(msg.sender);
8284

8385
// slither-disable-next-line incorrect-equality
8486
if (debtBalance == 0) revert OLS_OUT_OF_DEBT();
@@ -112,8 +114,10 @@ contract OpenLiquidityStrategy is IOpenLiquidityStrategy, LiquidityStrategy {
112114
uint256 idealDebtToContract,
113115
uint256 idealCollateralToReceive
114116
) internal view override returns (uint256 debtToContract, uint256 collateralToReceive) {
117+
// If the caller is the zero address, return the ideal amounts used for external view call to determineAction
118+
if (msg.sender == address(0)) return (idealDebtToContract, idealCollateralToReceive);
115119
address collateralToken = ctx.collateralToken();
116-
uint256 collateralBalance = IERC20(collateralToken).balanceOf(_getRebalancer());
120+
uint256 collateralBalance = IERC20(collateralToken).balanceOf(msg.sender);
117121

118122
// slither-disable-next-line incorrect-equality
119123
if (collateralBalance == 0) revert OLS_OUT_OF_COLLATERAL();
Lines changed: 369 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,369 @@
1+
// SPDX-License-Identifier: GPL-3.0-or-later
2+
// solhint-disable func-name-mixedcase, var-name-mixedcase, state-visibility
3+
// solhint-disable const-name-snakecase, max-states-count, contract-name-camelcase
4+
pragma solidity ^0.8;
5+
6+
import { OpenLiquidityStrategy_BaseTest } from "./OpenLiquidityStrategy_BaseTest.sol";
7+
import { LiquidityStrategyTypes as LQ } from "contracts/libraries/LiquidityStrategyTypes.sol";
8+
import { IERC20 } from "openzeppelin-contracts/contracts/token/ERC20/IERC20.sol";
9+
import { MockERC20 } from "test/utils/mocks/MockERC20.sol";
10+
import { IOpenLiquidityStrategy } from "contracts/interfaces/IOpenLiquidityStrategy.sol";
11+
12+
contract OpenLiquidityStrategy_DetermineActionTest is OpenLiquidityStrategy_BaseTest {
13+
function setUp() public override {
14+
super.setUp();
15+
}
16+
17+
/* ============================================================ */
18+
/* ========= determineAction with sufficient balance ========== */
19+
/* ============================================================ */
20+
21+
function test_determineAction_expansion_whenCallerHasSufficientDebt_shouldReturnAction()
22+
public
23+
fpmmToken0Debt(18, 18)
24+
addFpmmWithIncentive(0, 100, 0.005e18, 0.005025125628140703e18, 0.005e18, 0.005025125628140703e18)
25+
{
26+
// Pool price above oracle => expansion (debt flows in)
27+
provideFPMMReserves(100e18, 200e18, true);
28+
setOracleRate(1e18, 1e18);
29+
30+
// Call determineAction as the rebalancer who has tokens
31+
vm.prank(rebalancer);
32+
(, LQ.Action memory action) = strategy.determineAction(address(fpmm));
33+
34+
assertEq(uint8(action.dir), uint8(LQ.Direction.Expand));
35+
assertTrue(action.amount1Out > 0, "Should have collateral out");
36+
assertTrue(action.amountOwedToPool > 0, "Should owe debt to pool");
37+
}
38+
39+
function test_determineAction_contraction_whenCallerHasSufficientCollateral_shouldReturnAction()
40+
public
41+
fpmmToken0Debt(18, 18)
42+
addFpmmWithIncentive(0, 100, 0.005e18, 0.005025125628140703e18, 0.005e18, 0.005025125628140703e18)
43+
{
44+
// Pool price below oracle => contraction (collateral flows in)
45+
provideFPMMReserves(200e18, 100e18, true);
46+
setOracleRate(1e18, 1e18);
47+
48+
vm.prank(rebalancer);
49+
(, LQ.Action memory action) = strategy.determineAction(address(fpmm));
50+
51+
assertEq(uint8(action.dir), uint8(LQ.Direction.Contract));
52+
assertTrue(action.amount0Out > 0, "Should have debt out");
53+
assertTrue(action.amountOwedToPool > 0, "Should owe collateral to pool");
54+
}
55+
56+
/* ============================================================ */
57+
/* ========== determineAction with zero balance =============== */
58+
/* ============================================================ */
59+
60+
function test_determineAction_expansion_whenCallerHasNoDebt_shouldRevert()
61+
public
62+
fpmmToken0Debt(18, 18)
63+
addFpmmWithIncentive(0, 100, 0.005e18, 0.005025125628140703e18, 0.005e18, 0.005025125628140703e18)
64+
{
65+
provideFPMMReserves(100e18, 200e18, true);
66+
setOracleRate(1e18, 1e18);
67+
68+
// Caller with no debt tokens
69+
address caller = makeAddr("NoDebtCaller");
70+
71+
vm.expectRevert(IOpenLiquidityStrategy.OLS_OUT_OF_DEBT.selector);
72+
vm.prank(caller);
73+
strategy.determineAction(address(fpmm));
74+
}
75+
76+
function test_determineAction_contraction_whenCallerHasNoCollateral_shouldRevert()
77+
public
78+
fpmmToken0Debt(18, 18)
79+
addFpmmWithIncentive(0, 100, 0.005e18, 0.005025125628140703e18, 0.005e18, 0.005025125628140703e18)
80+
{
81+
provideFPMMReserves(200e18, 100e18, true);
82+
setOracleRate(1e18, 1e18);
83+
84+
// Caller with no collateral tokens
85+
address caller = makeAddr("NoCollateralCaller");
86+
87+
vm.expectRevert(IOpenLiquidityStrategy.OLS_OUT_OF_COLLATERAL.selector);
88+
vm.prank(caller);
89+
strategy.determineAction(address(fpmm));
90+
}
91+
92+
/* ============================================================ */
93+
/* ========== determineAction with limited balance ============ */
94+
/* ============================================================ */
95+
96+
function test_determineAction_expansion_whenCallerHasLimitedDebt_shouldClamp()
97+
public
98+
fpmmToken0Debt(18, 18)
99+
addFpmmWithIncentive(0, 100, 0.005e18, 0.005025125628140703e18, 0.005e18, 0.005025125628140703e18)
100+
{
101+
// Large imbalance requiring lots of debt
102+
provideFPMMReserves(100e18, 1000e18, true);
103+
setOracleRate(1e18, 1e18);
104+
105+
// Caller with only 1e18 debt tokens
106+
address limitedCaller = makeAddr("LimitedDebtCaller");
107+
MockERC20(debtToken).mint(limitedCaller, 1e18);
108+
109+
vm.prank(limitedCaller);
110+
(, LQ.Action memory clampedAction) = strategy.determineAction(address(fpmm));
111+
112+
// Full-balance caller for comparison
113+
vm.prank(rebalancer);
114+
(, LQ.Action memory fullAction) = strategy.determineAction(address(fpmm));
115+
116+
assertEq(uint8(clampedAction.dir), uint8(LQ.Direction.Expand));
117+
// Clamped amounts should be less than full amounts
118+
assertTrue(clampedAction.amountOwedToPool < fullAction.amountOwedToPool, "Clamped debt should be less");
119+
assertTrue(
120+
clampedAction.amount1Out < fullAction.amount1Out || clampedAction.amount0Out < fullAction.amount0Out,
121+
"Clamped output should be less"
122+
);
123+
}
124+
125+
function test_determineAction_contraction_whenCallerHasLimitedCollateral_shouldClamp()
126+
public
127+
fpmmToken0Debt(18, 18)
128+
addFpmmWithIncentive(0, 100, 0.005e18, 0.005025125628140703e18, 0.005e18, 0.005025125628140703e18)
129+
{
130+
// Large imbalance requiring lots of collateral
131+
provideFPMMReserves(1000e18, 100e18, true);
132+
setOracleRate(1e18, 1e18);
133+
134+
// Caller with only 1e18 collateral tokens
135+
address limitedCaller = makeAddr("LimitedCollateralCaller");
136+
MockERC20(collToken).mint(limitedCaller, 1e18);
137+
138+
vm.prank(limitedCaller);
139+
(, LQ.Action memory clampedAction) = strategy.determineAction(address(fpmm));
140+
141+
// Full-balance caller for comparison
142+
vm.prank(rebalancer);
143+
(, LQ.Action memory fullAction) = strategy.determineAction(address(fpmm));
144+
145+
assertEq(uint8(clampedAction.dir), uint8(LQ.Direction.Contract));
146+
assertTrue(clampedAction.amountOwedToPool < fullAction.amountOwedToPool, "Clamped collateral should be less");
147+
}
148+
149+
/* ============================================================ */
150+
/* ==== determineAction matches rebalance execution =========== */
151+
/* ============================================================ */
152+
153+
function test_determineAction_shouldMatchRebalanceExecution_expansion()
154+
public
155+
fpmmToken0Debt(18, 18)
156+
addFpmmWithIncentive(0, 100, 0.005e18, 0.005025125628140703e18, 0.005e18, 0.005025125628140703e18)
157+
{
158+
provideFPMMReserves(100e18, 200e18, true);
159+
setOracleRate(1e18, 1e18);
160+
161+
// Preview via determineAction
162+
vm.prank(rebalancer);
163+
(, LQ.Action memory previewAction) = strategy.determineAction(address(fpmm));
164+
165+
// Execute actual rebalance and compare token flows
166+
uint256 rebalancerDebtBefore = IERC20(debtToken).balanceOf(rebalancer);
167+
uint256 rebalancerCollBefore = IERC20(collToken).balanceOf(rebalancer);
168+
169+
vm.prank(rebalancer);
170+
strategy.rebalance(address(fpmm));
171+
172+
uint256 debtSpent = rebalancerDebtBefore - IERC20(debtToken).balanceOf(rebalancer);
173+
uint256 collReceived = IERC20(collToken).balanceOf(rebalancer) - rebalancerCollBefore;
174+
175+
// The debt spent should match the action's amountOwedToPool
176+
assertEq(debtSpent, previewAction.amountOwedToPool, "Debt spent should match preview");
177+
// Collateral received = amount from pool - protocol incentive
178+
assertTrue(collReceived > 0, "Should have received collateral");
179+
}
180+
181+
function test_determineAction_shouldMatchRebalanceExecution_contraction()
182+
public
183+
fpmmToken0Debt(18, 18)
184+
addFpmmWithIncentive(0, 100, 0.005e18, 0.005025125628140703e18, 0.005e18, 0.005025125628140703e18)
185+
{
186+
provideFPMMReserves(200e18, 100e18, true);
187+
setOracleRate(1e18, 1e18);
188+
189+
// Preview via determineAction
190+
vm.prank(rebalancer);
191+
(, LQ.Action memory previewAction) = strategy.determineAction(address(fpmm));
192+
193+
// Execute actual rebalance and compare token flows
194+
uint256 rebalancerCollBefore = IERC20(collToken).balanceOf(rebalancer);
195+
uint256 rebalancerDebtBefore = IERC20(debtToken).balanceOf(rebalancer);
196+
197+
vm.prank(rebalancer);
198+
strategy.rebalance(address(fpmm));
199+
200+
uint256 collSpent = rebalancerCollBefore - IERC20(collToken).balanceOf(rebalancer);
201+
uint256 debtReceived = IERC20(debtToken).balanceOf(rebalancer) - rebalancerDebtBefore;
202+
203+
assertEq(collSpent, previewAction.amountOwedToPool, "Collateral spent should match preview");
204+
assertTrue(debtReceived > 0, "Should have received debt tokens");
205+
}
206+
207+
/* ============================================================ */
208+
/* ============ determineAction with token1 as debt =========== */
209+
/* ============================================================ */
210+
211+
function test_determineAction_expansion_whenToken1IsDebt_shouldWork()
212+
public
213+
fpmmToken1Debt(18, 18)
214+
addFpmmWithIncentive(0, 100, 0.005e18, 0.005025125628140703e18, 0.005e18, 0.005025125628140703e18)
215+
{
216+
// Token1 is debt: excess collateral in token0
217+
provideFPMMReserves(200e18, 100e18, false);
218+
setOracleRate(1e18, 1e18);
219+
220+
vm.prank(rebalancer);
221+
(, LQ.Action memory action) = strategy.determineAction(address(fpmm));
222+
223+
assertEq(uint8(action.dir), uint8(LQ.Direction.Expand));
224+
assertTrue(action.amountOwedToPool > 0);
225+
}
226+
227+
function test_determineAction_contraction_whenToken1IsDebt_shouldWork()
228+
public
229+
fpmmToken1Debt(18, 18)
230+
addFpmmWithIncentive(0, 100, 0.005e18, 0.005025125628140703e18, 0.005e18, 0.005025125628140703e18)
231+
{
232+
// Token1 is debt: excess debt in token1
233+
provideFPMMReserves(100e18, 200e18, false);
234+
setOracleRate(1e18, 1e18);
235+
236+
vm.prank(rebalancer);
237+
(, LQ.Action memory action) = strategy.determineAction(address(fpmm));
238+
239+
assertEq(uint8(action.dir), uint8(LQ.Direction.Contract));
240+
assertTrue(action.amountOwedToPool > 0);
241+
}
242+
243+
/* ============================================================ */
244+
/* ===== determineAction from address(0) returns ideal ======== */
245+
/* ============================================================ */
246+
247+
function test_determineAction_expansion_whenCallerIsAddressZero_shouldReturnIdealAmounts()
248+
public
249+
fpmmToken0Debt(18, 18)
250+
addFpmmWithIncentive(0, 100, 0.005e18, 0.005025125628140703e18, 0.005e18, 0.005025125628140703e18)
251+
{
252+
provideFPMMReserves(100e18, 200e18, true);
253+
setOracleRate(1e18, 1e18);
254+
255+
// address(0) should return ideal (unclamped) amounts
256+
vm.prank(address(0));
257+
(, LQ.Action memory idealAction) = strategy.determineAction(address(fpmm));
258+
259+
assertEq(uint8(idealAction.dir), uint8(LQ.Direction.Expand));
260+
assertTrue(idealAction.amount1Out > 0, "Should have collateral out");
261+
assertTrue(idealAction.amountOwedToPool > 0, "Should owe debt to pool");
262+
}
263+
264+
function test_determineAction_contraction_whenCallerIsAddressZero_shouldReturnIdealAmounts()
265+
public
266+
fpmmToken0Debt(18, 18)
267+
addFpmmWithIncentive(0, 100, 0.005e18, 0.005025125628140703e18, 0.005e18, 0.005025125628140703e18)
268+
{
269+
provideFPMMReserves(200e18, 100e18, true);
270+
setOracleRate(1e18, 1e18);
271+
272+
vm.prank(address(0));
273+
(, LQ.Action memory idealAction) = strategy.determineAction(address(fpmm));
274+
275+
assertEq(uint8(idealAction.dir), uint8(LQ.Direction.Contract));
276+
assertTrue(idealAction.amount0Out > 0, "Should have debt out");
277+
assertTrue(idealAction.amountOwedToPool > 0, "Should owe collateral to pool");
278+
}
279+
280+
function test_determineAction_expansion_addressZeroReturnsIdeal_greaterOrEqualToClampedAmounts()
281+
public
282+
fpmmToken0Debt(18, 18)
283+
addFpmmWithIncentive(0, 100, 0.005e18, 0.005025125628140703e18, 0.005e18, 0.005025125628140703e18)
284+
{
285+
// Large imbalance so clamping is likely to kick in for limited callers
286+
provideFPMMReserves(100e18, 1000e18, true);
287+
setOracleRate(1e18, 1e18);
288+
289+
// Ideal amounts from address(0)
290+
vm.prank(address(0));
291+
(, LQ.Action memory idealAction) = strategy.determineAction(address(fpmm));
292+
293+
// Clamped amounts from a caller with limited debt
294+
address limitedCaller = makeAddr("LimitedDebtCaller2");
295+
MockERC20(debtToken).mint(limitedCaller, 1e18);
296+
vm.prank(limitedCaller);
297+
(, LQ.Action memory clampedAction) = strategy.determineAction(address(fpmm));
298+
299+
// Ideal amounts should be >= clamped amounts
300+
assertTrue(idealAction.amountOwedToPool >= clampedAction.amountOwedToPool, "Ideal debt owed should be >= clamped");
301+
assertTrue(idealAction.amount1Out >= clampedAction.amount1Out, "Ideal collateral out should be >= clamped");
302+
}
303+
304+
function test_determineAction_contraction_addressZeroReturnsIdeal_greaterOrEqualToClampedAmounts()
305+
public
306+
fpmmToken0Debt(18, 18)
307+
addFpmmWithIncentive(0, 100, 0.005e18, 0.005025125628140703e18, 0.005e18, 0.005025125628140703e18)
308+
{
309+
// Large imbalance so clamping is likely to kick in for limited callers
310+
provideFPMMReserves(1000e18, 100e18, true);
311+
setOracleRate(1e18, 1e18);
312+
313+
// Ideal amounts from address(0)
314+
vm.prank(address(0));
315+
(, LQ.Action memory idealAction) = strategy.determineAction(address(fpmm));
316+
317+
// Clamped amounts from a caller with limited collateral
318+
address limitedCaller = makeAddr("LimitedCollCaller2");
319+
MockERC20(collToken).mint(limitedCaller, 1e18);
320+
vm.prank(limitedCaller);
321+
(, LQ.Action memory clampedAction) = strategy.determineAction(address(fpmm));
322+
323+
assertTrue(
324+
idealAction.amountOwedToPool >= clampedAction.amountOwedToPool,
325+
"Ideal collateral owed should be >= clamped"
326+
);
327+
assertTrue(idealAction.amount0Out >= clampedAction.amount0Out, "Ideal debt out should be >= clamped");
328+
}
329+
330+
function test_determineAction_expansion_addressZeroMatchesSufficientBalance()
331+
public
332+
fpmmToken0Debt(18, 18)
333+
addFpmmWithIncentive(0, 100, 0.005e18, 0.005025125628140703e18, 0.005e18, 0.005025125628140703e18)
334+
{
335+
provideFPMMReserves(100e18, 200e18, true);
336+
setOracleRate(1e18, 1e18);
337+
338+
// address(0) ideal amounts
339+
vm.prank(address(0));
340+
(, LQ.Action memory idealAction) = strategy.determineAction(address(fpmm));
341+
342+
// Rebalancer has plenty of tokens, so should also get ideal amounts
343+
vm.prank(rebalancer);
344+
(, LQ.Action memory rebalancerAction) = strategy.determineAction(address(fpmm));
345+
346+
assertEq(idealAction.amountOwedToPool, rebalancerAction.amountOwedToPool, "Ideal should match sufficient balance");
347+
assertEq(idealAction.amount1Out, rebalancerAction.amount1Out, "Collateral out should match");
348+
assertEq(idealAction.amount0Out, rebalancerAction.amount0Out, "Debt out should match");
349+
}
350+
351+
function test_determineAction_contraction_addressZeroMatchesSufficientBalance()
352+
public
353+
fpmmToken0Debt(18, 18)
354+
addFpmmWithIncentive(0, 100, 0.005e18, 0.005025125628140703e18, 0.005e18, 0.005025125628140703e18)
355+
{
356+
provideFPMMReserves(200e18, 100e18, true);
357+
setOracleRate(1e18, 1e18);
358+
359+
vm.prank(address(0));
360+
(, LQ.Action memory idealAction) = strategy.determineAction(address(fpmm));
361+
362+
vm.prank(rebalancer);
363+
(, LQ.Action memory rebalancerAction) = strategy.determineAction(address(fpmm));
364+
365+
assertEq(idealAction.amountOwedToPool, rebalancerAction.amountOwedToPool, "Ideal should match sufficient balance");
366+
assertEq(idealAction.amount0Out, rebalancerAction.amount0Out, "Debt out should match");
367+
assertEq(idealAction.amount1Out, rebalancerAction.amount1Out, "Collateral out should match");
368+
}
369+
}

0 commit comments

Comments
 (0)