Skip to content

Commit 7e28eeb

Browse files
authored
fix(rebalancer): forward liquidator rewards to the msg.sender (#633)
* fix(rebalancer): forward liquidator rewards to the msg.sender * fix: use safeTransfer instead of the simple transfer function
1 parent 4d6d383 commit 7e28eeb

File tree

3 files changed

+101
-20
lines changed

3 files changed

+101
-20
lines changed

src/Rebalancer/Rebalancer.sol

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,7 @@ contract Rebalancer is Ownable2Step, ReentrancyGuard, ERC165, IOwnershipCallback
469469
+ data.amountToCloseWithoutBonus * (data.protocolPosition.amount - data.currentPositionData.amount)
470470
/ data.currentPositionData.amount;
471471

472+
uint256 balanceOfAssetBefore = _asset.balanceOf(address(this));
472473
// slither-disable-next-line reentrancy-eth
473474
success_ = _usdnProtocol.initiateClosePosition{ value: msg.value }(
474475
Types.PositionId({
@@ -485,6 +486,7 @@ contract Rebalancer is Ownable2Step, ReentrancyGuard, ERC165, IOwnershipCallback
485486
previousActionsData,
486487
""
487488
);
489+
uint256 balanceOfAssetAfter = _asset.balanceOf(address(this));
488490

489491
if (success_) {
490492
if (data.remainingAssets == 0) {
@@ -507,6 +509,12 @@ contract Rebalancer is Ownable2Step, ReentrancyGuard, ERC165, IOwnershipCallback
507509
emit ClosePositionInitiated(msg.sender, amount, data.amountToClose, data.remainingAssets);
508510
}
509511

512+
// If the rebalancer received assets, it means it was rewarded for liquidating positions
513+
// So we need to forward those rewards to the msg.sender
514+
if (balanceOfAssetAfter > balanceOfAssetBefore) {
515+
_asset.safeTransfer(msg.sender, balanceOfAssetAfter - balanceOfAssetBefore);
516+
}
517+
510518
// sent back any ether left in the contract
511519
_refundEther();
512520
}

test/integration/UsdnProtocol/RebalancerInitiateClosePosition.t.sol

Lines changed: 89 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ contract TestRebalancerInitiateClosePosition is
3131
PositionId internal prevPosId;
3232
Position internal protocolPosition;
3333
uint128 internal wstEthPrice;
34+
uint128 internal securityDeposit;
3435

3536
function setUp() public {
3637
(, amountInRebalancer,,) = _setUpImbalanced(15 ether);
@@ -39,15 +40,7 @@ contract TestRebalancerInitiateClosePosition is
3940
rebalancer.setPositionMaxLeverage(maxLeverage);
4041
skip(5 minutes);
4142

42-
{
43-
wstEthPrice = 1490 ether;
44-
uint128 ethPrice = uint128(wstETH.getWstETHByStETH(wstEthPrice)) / 1e10;
45-
mockPyth.setPrice(int64(uint64(ethPrice)));
46-
mockPyth.setLastPublishTime(block.timestamp);
47-
wstEthPrice = uint128(wstETH.getStETHByWstETH(ethPrice * 1e10));
48-
mockChainlinkOnChain.setLastPublishTime(block.timestamp);
49-
mockChainlinkOnChain.setLastPrice(int256(uint256(ethPrice)));
50-
}
43+
wstEthPrice = _setOraclePrices(1490 ether);
5144

5245
uint256 oracleFee = oracleMiddleware.validationCost(MOCK_PYTH_DATA, ProtocolAction.Liquidation);
5346
protocol.liquidate{ value: oracleFee }(MOCK_PYTH_DATA);
@@ -60,6 +53,7 @@ contract TestRebalancerInitiateClosePosition is
6053
index: previousPositionData.index
6154
});
6255
(protocolPosition,) = protocol.getLongPosition(prevPosId);
56+
securityDeposit = protocol.getSecurityDepositValue();
6357
}
6458

6559
function test_setUp() public view {
@@ -74,9 +68,8 @@ contract TestRebalancerInitiateClosePosition is
7468
* @custom:then The call reverts because of the imbalance
7569
*/
7670
function test_rebalancerNoWithdrawalAfterRebalancerTrigger() public {
77-
uint256 securityDepositValue = protocol.getSecurityDepositValue();
7871
vm.expectPartialRevert(UsdnProtocolImbalanceLimitReached.selector);
79-
rebalancer.initiateClosePosition{ value: securityDepositValue }(
72+
rebalancer.initiateClosePosition{ value: securityDeposit }(
8073
amountInRebalancer, address(this), DISABLE_MIN_PRICE, type(uint256).max, "", EMPTY_PREVIOUS_DATA
8174
);
8275
}
@@ -110,7 +103,7 @@ contract TestRebalancerInitiateClosePosition is
110103

111104
vm.expectEmit();
112105
emit ClosePositionInitiated(address(this), amount, amountToClose, amountInRebalancer - amount);
113-
(bool success) = rebalancer.initiateClosePosition{ value: protocol.getSecurityDepositValue() }(
106+
(bool success) = rebalancer.initiateClosePosition{ value: securityDeposit }(
114107
amount, address(this), DISABLE_MIN_PRICE, type(uint256).max, "", EMPTY_PREVIOUS_DATA
115108
);
116109

@@ -151,7 +144,6 @@ contract TestRebalancerInitiateClosePosition is
151144
function test_RevertWhen_rebalancerInitiateClosePositionPartialTriggerImbalanceLimit() public {
152145
// choose an amount big enough to trigger imbalance limits
153146
uint88 amount = amountInRebalancer / 10;
154-
uint256 securityDeposit = protocol.getSecurityDepositValue();
155147

156148
vm.expectPartialRevert(UsdnProtocolImbalanceLimitReached.selector);
157149
rebalancer.initiateClosePosition{ value: securityDeposit }(
@@ -184,7 +176,7 @@ contract TestRebalancerInitiateClosePosition is
184176

185177
vm.expectEmit();
186178
emit ClosePositionInitiated(address(this), amountInRebalancer, amountToClose, 0);
187-
(bool success) = rebalancer.initiateClosePosition{ value: protocol.getSecurityDepositValue() }(
179+
(bool success) = rebalancer.initiateClosePosition{ value: securityDeposit }(
188180
amountInRebalancer, address(this), DISABLE_MIN_PRICE, type(uint256).max, "", EMPTY_PREVIOUS_DATA
189181
);
190182

@@ -207,6 +199,89 @@ contract TestRebalancerInitiateClosePosition is
207199
);
208200
}
209201

202+
/**
203+
* @custom:scenario A user closing its position through the rebalancer can also liquidate ticks
204+
* @custom:given A tick can be liquidated in the USDN protocol
205+
* @custom:when The user calls the rebalancer's `initiateClosePosition` function
206+
* @custom:then A ClosePositionInitiated event is emitted
207+
* @custom:and The user depositData is deleted
208+
* @custom:and The position data is updated
209+
* @custom:and The user initiate close position is pending in protocol
210+
* @custom:and The user receives the liquidation rewards
211+
*/
212+
function test_rebalancerInitiateClosePositionLiquidatesAPosition() public {
213+
vm.prank(SET_PROTOCOL_PARAMS_MANAGER);
214+
protocol.setExpoImbalanceLimits(0, 0, 0, 0, 0, 0);
215+
216+
skip(1 hours);
217+
// put the eth price a bit higher to avoid liquidating existing position
218+
wstEthPrice = _setOraclePrices(wstEthPrice * 15 / 10);
219+
220+
// open a position to liquidate during the initiateClose call
221+
(, PositionId memory posId) = protocol.initiateOpenPosition{ value: securityDeposit }(
222+
2 ether,
223+
wstEthPrice * 9 / 10,
224+
type(uint128).max,
225+
protocol.getMaxLeverage(),
226+
payable(address(this)),
227+
payable(address(this)),
228+
type(uint256).max,
229+
"",
230+
EMPTY_PREVIOUS_DATA
231+
);
232+
233+
skip(1 hours);
234+
// put the price below the above position's liquidation price
235+
wstEthPrice = _setOraclePrices(wstEthPrice * 8 / 10);
236+
237+
uint256 amountToCloseWithoutBonus = FixedPointMathLib.fullMulDiv(
238+
amountInRebalancer,
239+
rebalancer.getPositionData(rebalancer.getPositionVersion()).entryAccMultiplier,
240+
rebalancer.getPositionData(rebalancer.getUserDepositData(address(this)).entryPositionVersion)
241+
.entryAccMultiplier
242+
);
243+
244+
uint256 amountToClose = amountToCloseWithoutBonus
245+
+ amountToCloseWithoutBonus * (protocolPosition.amount - previousPositionData.amount)
246+
/ previousPositionData.amount;
247+
248+
uint256 balanceOfRebalancerBefore = wstETH.balanceOf(address(rebalancer));
249+
LiqTickInfo[] memory liqTickInfoArray;
250+
251+
// snapshot and liquidate to get the liquidated ticks data
252+
uint256 snapshotId = vm.snapshot();
253+
liqTickInfoArray = protocol.liquidate{
254+
value: oracleMiddleware.validationCost(MOCK_PYTH_DATA, ProtocolAction.Liquidation)
255+
}(MOCK_PYTH_DATA);
256+
vm.revertTo(snapshotId);
257+
258+
uint256 liquidationRewards = liquidationRewardsManager.getLiquidationRewards(
259+
liqTickInfoArray, wstEthPrice, false, RebalancerAction.None, ProtocolAction.InitiateClosePosition, "", ""
260+
);
261+
262+
vm.expectEmit(false, true, false, false);
263+
emit LiquidatedTick(posId.tick, 0, 0, 0, 0);
264+
vm.expectEmit();
265+
emit ClosePositionInitiated(address(this), amountInRebalancer, amountToClose, 0);
266+
vm.expectEmit();
267+
emit Transfer(address(rebalancer), address(this), liquidationRewards);
268+
(bool success) = rebalancer.initiateClosePosition{ value: securityDeposit }(
269+
amountInRebalancer, address(this), DISABLE_MIN_PRICE, type(uint256).max, "", EMPTY_PREVIOUS_DATA
270+
);
271+
272+
UserDeposit memory depositData = rebalancer.getUserDepositData(address(this));
273+
274+
assertTrue(success, "The rebalancer close should be successful");
275+
assertEq(depositData.amount, 0, "The user's deposited amount in rebalancer should be zero");
276+
assertEq(depositData.entryPositionVersion, 0, "The user's entry position version should be zero");
277+
278+
assertEq(
279+
balanceOfRebalancerBefore,
280+
wstETH.balanceOf(address(rebalancer)),
281+
"The wstETH balance of the rebalancer should not have changed"
282+
);
283+
}
284+
210285
/**
211286
* @custom:scenario The user sends too much ether when closing its position
212287
* @custom:when The user calls the rebalancer's {initiateClosePosition} function with too much ether
@@ -216,7 +291,6 @@ contract TestRebalancerInitiateClosePosition is
216291
vm.prank(SET_PROTOCOL_PARAMS_MANAGER);
217292
protocol.setExpoImbalanceLimits(0, 0, 0, 0, 0, 0);
218293

219-
uint256 securityDeposit = protocol.getSecurityDepositValue();
220294
uint256 userBalanceBefore = address(this).balance;
221295
uint256 excessAmount = 1 ether;
222296

@@ -240,7 +314,6 @@ contract TestRebalancerInitiateClosePosition is
240314
* @custom:then It should revert with `RebalancerUserLiquidated` error
241315
*/
242316
function test_RevertWhen_rebalancerUserLiquidated() public {
243-
uint256 securityDeposit = protocol.getSecurityDepositValue();
244317
// compensate imbalance to allow rebalancer users to close
245318
(, PositionId memory newPosId) = protocol.initiateOpenPosition{ value: securityDeposit }(
246319
10 ether,
@@ -355,7 +428,6 @@ contract TestRebalancerInitiateClosePosition is
355428

356429
// deposit assets in the protocol to imbalance it
357430
uint256 oracleFee = oracleMiddleware.validationCost(MOCK_PYTH_DATA, ProtocolAction.ValidateDeposit);
358-
uint256 securityDeposit = protocol.getSecurityDepositValue();
359431
protocol.initiateDeposit{ value: securityDeposit }(
360432
100 ether,
361433
DISABLE_SHARES_OUT_MIN,

test/integration/UsdnProtocol/utils/Fixtures.sol

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
WSTETH
2828
} from "../../../utils/Constants.sol";
2929
import { BaseFixture } from "../../../utils/Fixtures.sol";
30+
import { IEventsErrors } from "../../../utils/IEventsErrors.sol";
3031
import { IUsdnProtocolHandler } from "../../../utils/IUsdnProtocolHandler.sol";
3132
import { Sdex } from "../../../utils/Sdex.sol";
3233
import { WstETH } from "../../../utils/WstEth.sol";
@@ -47,7 +48,7 @@ import { IUsdnProtocolErrors } from "../../../../src/interfaces/UsdnProtocol/IUs
4748
import { IUsdnProtocolEvents } from "../../../../src/interfaces/UsdnProtocol/IUsdnProtocolEvents.sol";
4849
import { HugeUint } from "../../../../src/libraries/HugeUint.sol";
4950

50-
contract UsdnProtocolBaseIntegrationFixture is BaseFixture, IUsdnProtocolErrors, IUsdnProtocolEvents {
51+
contract UsdnProtocolBaseIntegrationFixture is BaseFixture, IUsdnProtocolErrors, IUsdnProtocolEvents, IEventsErrors {
5152
struct SetUpParams {
5253
uint128 initialDeposit;
5354
uint128 initialLong;
@@ -350,11 +351,11 @@ contract UsdnProtocolBaseIntegrationFixture is BaseFixture, IUsdnProtocolErrors,
350351
}
351352

352353
/// @dev Set the provided price and current timestamp in all of the mock oracles
353-
function _setOraclePrices(uint128 wstEthPrice) internal {
354+
function _setOraclePrices(uint128 wstEthPrice) internal returns (uint128 wstEthPrice_) {
354355
uint128 ethPrice = uint128(wstETH.getWstETHByStETH(wstEthPrice)) / 1e10;
355356
mockPyth.setPrice(int64(uint64(ethPrice)));
356357
mockPyth.setLastPublishTime(block.timestamp);
357-
wstEthPrice = uint128(wstETH.getStETHByWstETH(ethPrice * 1e10));
358+
wstEthPrice_ = uint128(wstETH.getStETHByWstETH(ethPrice * 1e10));
358359
mockChainlinkOnChain.setLastPublishTime(block.timestamp);
359360
mockChainlinkOnChain.setLastPrice(int256(uint256(ethPrice)));
360361
}

0 commit comments

Comments
 (0)