-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathPositionSwapper.sol
More file actions
411 lines (341 loc) · 17.7 KB
/
PositionSwapper.sol
File metadata and controls
411 lines (341 loc) · 17.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
// SPDX-License-Identifier: BSD-3-Clause
pragma solidity 0.8.25;
import { Ownable2StepUpgradeable } from "@openzeppelin/contracts-upgradeable/access/Ownable2StepUpgradeable.sol";
import { SafeERC20Upgradeable, IERC20Upgradeable } from "@openzeppelin/contracts-upgradeable/token/ERC20/utils/SafeERC20Upgradeable.sol";
import { ReentrancyGuardUpgradeable } from "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol";
import { IVToken, IComptroller, IVBNB } from "../Interfaces.sol";
import { ISwapHelper } from "./ISwapHelper.sol";
/**
* @title PositionSwapper
* @author Venus
* @notice A contract to facilitate swapping collateral and debt positions between different vToken markets.
* @custom:security-contact https://github.com/VenusProtocol/venus-periphery
*/
contract PositionSwapper is Ownable2StepUpgradeable, ReentrancyGuardUpgradeable {
using SafeERC20Upgradeable for IERC20Upgradeable;
/// @notice The Comptroller used for permission and liquidity checks.
IComptroller public immutable COMPTROLLER;
/// @notice The vToken representing the native asset (e.g., vBNB).
address public immutable NATIVE_MARKET;
/// @notice Mapping of approved swap pairs. (marketFrom => marketTo => helper => status)
mapping(address => mapping(address => mapping(address => bool))) public approvedPairs;
/// @notice Emitted after a successful swap and mint.
event CollateralSwapped(address indexed user, address marketFrom, address marketTo, uint256 amountOut);
/// @notice Emitted when a user swaps their debt from one market to another.
event DebtSwapped(address indexed user, address marketFrom, address marketTo, uint256 amountOut);
/// @notice Emitted when the owner sweeps leftover ERC-20 tokens.
event SweepToken(address indexed token, address indexed receiver, uint256 amount);
/// @notice Emitted when the owner sweeps leftover native tokens (e.g., BNB).
event SweepNative(address indexed receiver, uint256 amount);
/// @notice Emitted when an approved pair is updated.
event ApprovedPairUpdated(address marketFrom, address marketTo, address helper, bool oldStatus, bool newStatus);
/// @custom:error Unauthorized Caller is neither the user nor an approved delegate.
error Unauthorized();
/// @custom:error SeizeFailed
error SeizeFailed(uint256 err);
/// @custom:error RedeemFailed
error RedeemFailed(uint256 err);
/// @custom:error BorrowFailed
error BorrowFailed(uint256 err);
/// @custom:error MintFailed
error MintFailed(uint256 err);
/// @custom:error RepayFailed
error RepayFailed(uint256 err);
/// @custom:error NoVTokenBalance
error NoVTokenBalance();
/// @custom:error NoBorrowBalance
error NoBorrowBalance();
/// @custom:error ZeroAmount
error ZeroAmount();
/// @custom:error NoUnderlyingReceived
error NoUnderlyingReceived();
/// @custom:error SwapCausesLiquidation
error SwapCausesLiquidation(uint256 err);
/// @custom:error MarketNotListed
error MarketNotListed();
/// @custom:error ZeroAddress
error ZeroAddress();
/// @custom:error TransferFailed
error TransferFailed();
/// @custom:error EnterMarketFailed
error EnterMarketFailed(uint256 err);
/// @custom:error NotApprovedHelper
error NotApprovedHelper();
/**
* @notice Constructor to set immutable variables.
* @param _comptroller The address of the Comptroller contract.
* @param _nativeMarket The address of the native market (e.g., vBNB).
* @custom:error Throw ZeroAddress if comptroller address is zero.
*/
/// @custom:oz-upgrades-unsafe-allow constructor
constructor(address _comptroller, address _nativeMarket) {
if (_comptroller == address(0)) revert ZeroAddress();
if (_nativeMarket == address(0)) revert ZeroAddress();
COMPTROLLER = IComptroller(_comptroller);
NATIVE_MARKET = _nativeMarket;
_disableInitializers();
}
/**
* @notice Initializes the contract, setting the deployer as the initial owner.
*/
function initialize() external initializer {
__Ownable2Step_init();
__ReentrancyGuard_init();
}
/**
* @notice Accepts native tokens (e.g., BNB) sent to this contract.
*/
receive() external payable {}
/**
* @notice Swaps the full vToken collateral of a user from one market to another.
* @param user The address whose collateral is being swapped.
* @param marketFrom The vToken market to seize from.
* @param marketTo The vToken market to mint into.
* @param helper The ISwapHelper contract for performing the token swap.
* @custom:error Throw NoVTokenBalance The user has no vToken balance in the marketFrom.
* @custom:event Emits CollateralSwapped event.
*/
function swapFullCollateral(
address user,
IVToken marketFrom,
IVToken marketTo,
ISwapHelper helper
) external payable nonReentrant {
uint256 userBalance = marketFrom.balanceOf(user);
if (userBalance == 0) revert NoVTokenBalance();
_swapCollateral(user, marketFrom, marketTo, userBalance, helper);
emit CollateralSwapped(user, address(marketFrom), address(marketTo), userBalance);
}
/**
* @notice Swaps a specific amount of collateral from one market to another.
* @param user The address whose collateral is being swapped.
* @param marketFrom The vToken market to seize from.
* @param marketTo The vToken market to mint into.
* @param amountToSwap The amount of vTokens to seize and swap.
* @param helper The ISwapHelper contract for performing the token swap.
* @custom:error Throw NoVTokenBalance The user has insufficient vToken balance in the marketFrom.
* @custom:error Throw ZeroAmount The amountToSwap is zero.
* @custom:event Emits CollateralSwapped event.
*/
function swapCollateralWithAmount(
address user,
IVToken marketFrom,
IVToken marketTo,
uint256 amountToSwap,
ISwapHelper helper
) external payable nonReentrant {
if (amountToSwap == 0) revert ZeroAmount();
if (amountToSwap > marketFrom.balanceOf(user)) revert NoVTokenBalance();
_swapCollateral(user, marketFrom, marketTo, amountToSwap, helper);
emit CollateralSwapped(user, address(marketFrom), address(marketTo), amountToSwap);
}
/**
* @notice Swaps the full debt of a user from one market to another.
* @param user The address whose debt is being swapped.
* @param marketFrom The vToken market from which debt is swapped.
* @param marketTo The vToken market into which the new debt is borrowed.
* @param helper The ISwapHelper contract for performing the token swap.
* @custom:error Throw NoBorrowBalance The user has no borrow balance in the marketFrom.
* @custom:event Emits DebtSwapped event.
*/
function swapFullDebt(
address user,
IVToken marketFrom,
IVToken marketTo,
ISwapHelper helper
) external payable nonReentrant {
uint256 borrowBalance = marketFrom.borrowBalanceCurrent(user);
if (borrowBalance == 0) revert NoBorrowBalance();
_swapDebt(user, marketFrom, marketTo, borrowBalance, helper);
emit DebtSwapped(user, address(marketFrom), address(marketTo), borrowBalance);
}
/**
* @notice Swaps a specific amount of debt from one market to another.
* @param user The address whose debt is being swapped.
* @param marketFrom The vToken market from which debt is swapped.
* @param marketTo The vToken market into which the new debt is borrowed.
* @param amountToSwap The amount of debt to swap.
* @param helper The ISwapHelper contract for performing the token swap.
* @custom:error Throw NoBorrowBalance The user has insufficient borrow balance in the marketFrom.
* @custom:error Throw ZeroAmount The amountToSwap is zero.
* @custom:event Emits DebtSwapped event.
*/
function swapDebtWithAmount(
address user,
IVToken marketFrom,
IVToken marketTo,
uint256 amountToSwap,
ISwapHelper helper
) external payable nonReentrant {
if (amountToSwap == 0) revert ZeroAmount();
if (amountToSwap > marketFrom.borrowBalanceCurrent(user)) revert NoBorrowBalance();
_swapDebt(user, marketFrom, marketTo, amountToSwap, helper);
emit DebtSwapped(user, address(marketFrom), address(marketTo), amountToSwap);
}
/**
* @notice Allows the owner to sweep leftover ERC-20 tokens from the contract.
* @param token The token to sweep.
* @custom:event Emits SweepToken event.
*/
function sweepToken(IERC20Upgradeable token) external onlyOwner {
uint256 balance = token.balanceOf(address(this));
if (balance > 0) {
token.safeTransfer(owner(), balance);
emit SweepToken(address(token), owner(), balance);
}
}
/**
* @notice Allows the owner to sweep leftover native tokens (e.g., BNB) from the contract.
* @custom:event Emits SweepToken event.
*/
function sweepNative() external onlyOwner {
uint256 balance = address(this).balance;
if (balance > 0) {
(bool success, ) = payable(owner()).call{ value: balance }("");
if (!success) revert TransferFailed();
emit SweepNative(owner(), balance);
}
}
/**
* @notice Sets the approval status for a specific swap pair and helper.
* @param marketFrom The vToken market to swap from.
* @param marketTo The vToken market to swap to.
* @param helper The ISwapHelper contract used for the swap.
* @param status The approval status to set (true = approved, false = not approved).
* @custom:event Emits ApprovedPairUpdated event.
*/
function setApprovedPair(address marketFrom, address marketTo, address helper, bool status) external onlyOwner {
emit ApprovedPairUpdated(marketFrom, marketTo, helper, approvedPairs[marketFrom][marketTo][helper], status);
approvedPairs[marketFrom][marketTo][helper] = status;
}
/**
* @notice Internal function that performs the full collateral swap process.
* @param user The address whose collateral is being swapped.
* @param marketFrom The vToken market from which collateral is seized.
* @param marketTo The vToken market into which the swapped collateral is minted.
* @param amountToSeize The amount of vTokens to seize and convert.
* @param swapHelper The swap helper contract used to perform the token conversion.
* @custom:error Throw NotApprovedHelper if the specified swap pair and helper are not approved.
* @custom:error Throw MarketNotListed if one of the specified markets is not listed in the Comptroller.
* @custom:error Throw Unauthorized if the caller is neither the user nor an approved delegate.
* @custom:error Throw SeizeFailed if the seize operation fails.
* @custom:error Throw RedeemFailed if the redeem operation fails.
* @custom:error Throw NoUnderlyingReceived if no underlying tokens are received from the swap.
* @custom:error Throw MintFailed if the mint operation fails.
*/
function _swapCollateral(
address user,
IVToken marketFrom,
IVToken marketTo,
uint256 amountToSeize,
ISwapHelper swapHelper
) internal {
if (!approvedPairs[address(marketFrom)][address(marketTo)][address(swapHelper)]) {
revert NotApprovedHelper();
}
(bool isMarketListed, , ) = COMPTROLLER.markets(address(marketFrom));
if (!isMarketListed) revert MarketNotListed();
(isMarketListed, , ) = COMPTROLLER.markets(address(marketTo));
if (!isMarketListed) revert MarketNotListed();
if (user != msg.sender && !COMPTROLLER.approvedDelegates(user, msg.sender)) {
revert Unauthorized();
}
_checkAccountSafe(user);
uint256 err = marketFrom.seize(address(this), user, amountToSeize);
if (err != 0) revert SeizeFailed(err);
address toUnderlyingAddress = marketTo.underlying();
IERC20Upgradeable toUnderlying = IERC20Upgradeable(toUnderlyingAddress);
uint256 toUnderlyingBalanceBefore = toUnderlying.balanceOf(address(this));
if (address(marketFrom) == NATIVE_MARKET) {
uint256 nativeBalanceBefore = address(this).balance;
err = marketFrom.redeem(amountToSeize);
if (err != 0) revert RedeemFailed(err);
uint256 receivedNative = address(this).balance - nativeBalanceBefore;
if (receivedNative == 0) revert NoUnderlyingReceived();
swapHelper.swapInternal{ value: receivedNative }(address(0), toUnderlyingAddress, receivedNative);
} else {
IERC20Upgradeable fromUnderlying = IERC20Upgradeable(marketFrom.underlying());
uint256 fromUnderlyingBalanceBefore = fromUnderlying.balanceOf(address(this));
err = marketFrom.redeem(amountToSeize);
if (err != 0) revert RedeemFailed(err);
uint256 receivedFromToken = fromUnderlying.balanceOf(address(this)) - fromUnderlyingBalanceBefore;
if (receivedFromToken == 0) revert NoUnderlyingReceived();
fromUnderlying.forceApprove(address(swapHelper), receivedFromToken);
swapHelper.swapInternal(address(fromUnderlying), toUnderlyingAddress, receivedFromToken);
}
uint256 toUnderlyingReceived = toUnderlying.balanceOf(address(this)) - toUnderlyingBalanceBefore;
if (toUnderlyingReceived == 0) revert NoUnderlyingReceived();
toUnderlying.forceApprove(address(marketTo), toUnderlyingReceived);
err = marketTo.mintBehalf(user, toUnderlyingReceived);
if (err != 0) revert MintFailed(err);
if (COMPTROLLER.checkMembership(user, marketFrom) && !COMPTROLLER.checkMembership(user, marketTo)) {
err = COMPTROLLER.enterMarket(user, address(marketTo));
if (err != 0) revert EnterMarketFailed(err);
}
_checkAccountSafe(user);
}
/**
* @notice Internal function that performs the full debt swap process.
* @param user The address whose debt is being swapped.
* @param marketFrom The vToken market to which debt is repaid.
* @param marketTo The vToken market into which the new debt is borrowed.
* @param amountToBorrow The amount of new debt to borrow.
* @param swapHelper The swap helper contract used to perform the token conversion.
* @custom:error Throw NotApprovedHelper if the swap helper is not approved for the given markets.
* @custom:error Throw MarketNotListed if one of the specified markets is not listed in the Comptroller.
* @custom:error Throw Unauthorized if the caller is neither the user nor an approved delegate.
* @custom:error Throw BorrowFailed if the borrow operation fails.
* @custom:error Throw NoUnderlyingReceived if no underlying tokens are received from the swap.
* @custom:error Throw RepayFailed if the repay operation fails.
*/
function _swapDebt(
address user,
IVToken marketFrom,
IVToken marketTo,
uint256 amountToBorrow,
ISwapHelper swapHelper
) internal {
if (!approvedPairs[address(marketFrom)][address(marketTo)][address(swapHelper)]) {
revert NotApprovedHelper();
}
(bool isMarketListed, , ) = COMPTROLLER.markets(address(marketFrom));
if (!isMarketListed) revert MarketNotListed();
(isMarketListed, , ) = COMPTROLLER.markets(address(marketTo));
if (!isMarketListed) revert MarketNotListed();
if (user != msg.sender && !COMPTROLLER.approvedDelegates(user, msg.sender)) {
revert Unauthorized();
}
_checkAccountSafe(user);
address toUnderlyingAddress = marketTo.underlying();
IERC20Upgradeable toUnderlying = IERC20Upgradeable(toUnderlyingAddress);
uint256 toUnderlyingBalanceBefore = toUnderlying.balanceOf(address(this));
uint256 err = marketTo.borrowBehalf(user, amountToBorrow);
if (err != 0) revert BorrowFailed(err);
uint256 receivedToUnderlying = toUnderlying.balanceOf(address(this)) - toUnderlyingBalanceBefore;
toUnderlying.forceApprove(address(swapHelper), receivedToUnderlying);
if (address(marketFrom) == NATIVE_MARKET) {
uint256 fromUnderlyingBalanceBefore = address(this).balance;
swapHelper.swapInternal(toUnderlyingAddress, address(0), receivedToUnderlying);
uint256 receivedFromNative = address(this).balance - fromUnderlyingBalanceBefore;
IVBNB(NATIVE_MARKET).repayBorrowBehalf{ value: receivedFromNative }(user);
} else {
IERC20Upgradeable fromUnderlying = IERC20Upgradeable(marketFrom.underlying());
uint256 fromUnderlyingBalanceBefore = fromUnderlying.balanceOf(address(this));
swapHelper.swapInternal(toUnderlyingAddress, address(fromUnderlying), receivedToUnderlying);
uint256 receivedFromToken = fromUnderlying.balanceOf(address(this)) - fromUnderlyingBalanceBefore;
fromUnderlying.forceApprove(address(marketFrom), receivedFromToken);
err = marketFrom.repayBorrowBehalf(user, receivedFromToken);
if (err != 0) revert RepayFailed(err);
}
_checkAccountSafe(user);
}
/**
* @dev Checks if a user's account is safe post-swap.
* @param user The address to check.
* @custom:error Throw SwapCausesLiquidation if the user's account is undercollateralized.
*/
function _checkAccountSafe(address user) internal view {
(uint256 err, , uint256 shortfall) = COMPTROLLER.getAccountLiquidity(user);
if (err != 0 || shortfall > 0) revert SwapCausesLiquidation(err);
}
}