-
Notifications
You must be signed in to change notification settings - Fork 47
/
Copy pathLoanClosingsRollover.sol
316 lines (283 loc) · 12.6 KB
/
LoanClosingsRollover.sol
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
/**
* Copyright 2017-2020, bZeroX, LLC. All Rights Reserved.
* Licensed under the Apache License, Version 2.0.
*/
pragma solidity 0.5.17;
pragma experimental ABIEncoderV2;
import "../mixins/LiquidationHelper.sol";
import "../interfaces/ILoanPool.sol";
import "./LoanClosingsShared.sol";
/**
* @title LoanClosingsRollover contract.
* @notice Ways to close a loan: rollover. Margin trade
* positions are always closed with a swap.
*
* */
contract LoanClosingsRollover is LoanClosingsShared, LiquidationHelper {
uint256 internal constant MONTH = 365 days / 12;
constructor() public {}
function() external {
revert("fallback not allowed");
}
function initialize(address target) external onlyOwner {
address prevModuleContractAddress = logicTargets[this.rollover.selector];
_setTarget(this.rollover.selector, target);
emit ProtocolModuleContractReplaced(
prevModuleContractAddress,
target,
"LoanClosingsRollover"
);
}
/**
* @notice Roll over a loan.
*
* @dev Public wrapper for _rollover internal function.
*
* Each loan has a duration. In case of a margin trade it is set to 28
* days, in case of borrowing, it can be set by the user. On loan
* openning, the user pays the interest for this duration in advance.
* If closing early, he gets the excess refunded. If it is not closed
* before the end date, it needs to be rolled over. On rollover the
* interest is paid for the next period. In case of margin trading
* it's 28 days, in case of borrowing it's a month.
*
* The function rollover on the protocol contract extends the loan
* duration by the maximum term (28 days for margin trades at the moment
* of writing), pays the interest to the lender and refunds the caller
* for the gas cost by sending 2 * the gas cost using the fast gas price
* as base for the calculation.
*
* @param loanId The ID of the loan to roll over.
* // param calldata The payload for the call. These loan DataBytes are additional loan data (not in use for token swaps).
* */
function rollover(
bytes32 loanId,
bytes calldata // for future use /*loanDataBytes*/
) external nonReentrant globallyNonReentrant iTokenSupplyUnchanged(loanId) whenNotPaused {
// restrict to EOAs to prevent griefing attacks, during interest rate recalculation
require(msg.sender == tx.origin, "EOAs call");
return
_rollover(
loanId,
"" // loanDataBytes
);
}
/**
* @notice Internal function for roll over a loan.
*
* Each loan has a duration. In case of a margin trade it is set to 28
* days, in case of borrowing, it can be set by the user. On loan
* openning, the user pays the interest for this duration in advance.
* If closing early, he gets the excess refunded. If it is not closed
* before the end date, it needs to be rolled over. On rollover the
* interest is paid for the next period. In case of margin trading
* it's 28 days, in case of borrowing it's a month.
*
* @param loanId The ID of the loan to roll over.
* @param loanDataBytes The payload for the call. These loan DataBytes are
* additional loan data (not in use for token swaps).
* */
function _rollover(bytes32 loanId, bytes memory loanDataBytes) internal {
(Loan storage loanLocal, LoanParams storage loanParamsLocal) = _checkLoan(loanId);
require(block.timestamp > loanLocal.endTimestamp.sub(3600), "healthy position");
require(loanPoolToUnderlying[loanLocal.lender] != address(0), "invalid lender");
// pay outstanding interest to lender
_payInterest(loanLocal.lender, loanParamsLocal.loanToken);
LoanInterest storage loanInterestLocal = loanInterest[loanLocal.id];
LenderInterest storage lenderInterestLocal = lenderInterest[loanLocal.lender][
loanParamsLocal.loanToken
];
_settleFeeRewardForInterestExpense(
loanInterestLocal,
loanLocal.id,
loanParamsLocal.loanToken, /// fee token
loanParamsLocal.collateralToken, /// pairToken (used to check if there is any special rebates or not) -- to pay fee reward
loanLocal.borrower,
block.timestamp
);
// Handle back interest: calculates interest owned since the loan endtime passed but the loan remained open
uint256 backInterestTime;
uint256 backInterestOwed;
if (block.timestamp > loanLocal.endTimestamp) {
backInterestTime = block.timestamp.sub(loanLocal.endTimestamp);
backInterestOwed = backInterestTime.mul(loanInterestLocal.owedPerDay);
backInterestOwed = backInterestOwed.div(1 days);
}
//note: to avoid code duplication, it would be nicer to store loanParamsLocal.maxLoanTerm in a local variable
//however, we've got stack too deep issues if we do so.
if (loanParamsLocal.maxLoanTerm != 0) {
// fixed-term loan, so need to query iToken for latest variable rate
uint256 owedPerDay = loanLocal
.principal
.mul(ILoanPool(loanLocal.lender).borrowInterestRate())
.div(365 * 10 ** 20);
lenderInterestLocal.owedPerDay = lenderInterestLocal.owedPerDay.add(owedPerDay);
lenderInterestLocal.owedPerDay = lenderInterestLocal.owedPerDay.sub(
loanInterestLocal.owedPerDay
);
loanInterestLocal.owedPerDay = owedPerDay;
//if the loan has been open for longer than an additional period, add at least 1 additional day
if (backInterestTime >= loanParamsLocal.maxLoanTerm) {
loanLocal.endTimestamp = loanLocal.endTimestamp.add(backInterestTime).add(1 days);
}
//extend by the max loan term
else {
loanLocal.endTimestamp = loanLocal.endTimestamp.add(loanParamsLocal.maxLoanTerm);
}
} else {
// loanInterestLocal.owedPerDay doesn't change
if (backInterestTime >= MONTH) {
loanLocal.endTimestamp = loanLocal.endTimestamp.add(backInterestTime).add(1 days);
} else {
loanLocal.endTimestamp = loanLocal.endTimestamp.add(MONTH);
}
}
uint256 interestAmountRequired = loanLocal.endTimestamp.sub(block.timestamp);
interestAmountRequired = interestAmountRequired.mul(loanInterestLocal.owedPerDay);
interestAmountRequired = interestAmountRequired.div(1 days);
loanInterestLocal.depositTotal = loanInterestLocal.depositTotal.add(
interestAmountRequired
);
lenderInterestLocal.owedTotal = lenderInterestLocal.owedTotal.add(interestAmountRequired);
// add backInterestOwed
interestAmountRequired = interestAmountRequired.add(backInterestOwed);
// collect interest (needs to be converted from the collateral)
(uint256 destTokenAmountReceived, uint256 sourceTokenAmountUsed, ) = _doCollateralSwap(
loanLocal,
loanParamsLocal,
0, //min swap 0 -> swap connector estimates the amount of source tokens to use
interestAmountRequired, //required destination tokens
true, // returnTokenIsCollateral
loanDataBytes
);
//received more tokens than needed to pay the interest
if (destTokenAmountReceived > interestAmountRequired) {
// swap rest back to collateral, if the amount is big enough to cover gas cost
if (
worthTheTransfer(
loanParamsLocal.loanToken,
destTokenAmountReceived - interestAmountRequired
)
) {
(destTokenAmountReceived, , ) = _swapBackExcess(
loanLocal,
loanParamsLocal,
destTokenAmountReceived - interestAmountRequired, //amount to be swapped
loanDataBytes
);
sourceTokenAmountUsed = sourceTokenAmountUsed.sub(destTokenAmountReceived);
}
//else give it to the protocol as a lending fee
else {
_payLendingFee(
loanLocal.borrower,
loanParamsLocal.loanToken,
destTokenAmountReceived - interestAmountRequired
);
}
}
//subtract the interest from the collateral
loanLocal.collateral = loanLocal.collateral.sub(sourceTokenAmountUsed);
if (backInterestOwed != 0) {
// pay out backInterestOwed
_payInterestTransfer(loanLocal.lender, loanParamsLocal.loanToken, backInterestOwed);
}
uint256 rolloverReward = _getRolloverReward(
loanParamsLocal.collateralToken,
loanParamsLocal.loanToken,
loanLocal.principal
);
if (rolloverReward != 0) {
// if the reward > collateral:
if (rolloverReward > loanLocal.collateral) {
// 1. pay back the remaining loan to the lender
// 2. pay the remaining collateral to msg.sender
// 3. close the position & emit close event
_closeWithSwap(
loanLocal.id,
msg.sender,
loanLocal.collateral,
false,
"" // loanDataBytes
);
} else {
// pay out reward to caller
loanLocal.collateral = loanLocal.collateral.sub(rolloverReward);
_withdrawAsset(loanParamsLocal.collateralToken, msg.sender, rolloverReward);
}
}
if (loanLocal.collateral > 0) {
//close whole loan if tiny position will remain
if (_getAmountInRbtc(loanParamsLocal.loanToken, loanLocal.principal) <= TINY_AMOUNT) {
_closeWithSwap(
loanLocal.id,
loanLocal.borrower,
loanLocal.collateral, // swap all collaterals
false,
"" /// loanDataBytes
);
} else {
(uint256 currentMargin, ) = IPriceFeeds(priceFeeds).getCurrentMargin(
loanParamsLocal.loanToken,
loanParamsLocal.collateralToken,
loanLocal.principal,
loanLocal.collateral
);
require(
currentMargin > 3 ether, // ensure there's more than 3% margin remaining
"unhealthy position"
);
}
}
if (loanLocal.active) {
emit Rollover(
loanLocal.borrower, // user (borrower)
loanLocal.lender, // lender
loanLocal.id, // loanId
loanLocal.principal, // principal
loanLocal.collateral, // collateral
loanLocal.endTimestamp, // endTimestamp
msg.sender, // rewardReceiver
rolloverReward // reward
);
}
}
/**
* @notice Swap back excessive loan tokens to collateral tokens.
*
* @param loanLocal The loan object.
* @param loanParamsLocal The loan parameters.
* @param swapAmount The amount to be swapped.
* @param loanDataBytes Additional loan data (not in use for token swaps).
*
* @return destTokenAmountReceived The amount of destiny tokens received.
* @return sourceTokenAmountUsed The amount of source tokens used.
* @return collateralToLoanSwapRate The swap rate of collateral.
* */
function _swapBackExcess(
Loan memory loanLocal,
LoanParams memory loanParamsLocal,
uint256 swapAmount,
bytes memory loanDataBytes
)
internal
returns (
uint256 destTokenAmountReceived,
uint256 sourceTokenAmountUsed,
uint256 collateralToLoanSwapRate
)
{
(destTokenAmountReceived, sourceTokenAmountUsed, collateralToLoanSwapRate) = _loanSwap(
loanLocal.id,
loanParamsLocal.loanToken,
loanParamsLocal.collateralToken,
loanLocal.borrower,
swapAmount, // minSourceTokenAmount
swapAmount, // maxSourceTokenAmount
0, // requiredDestTokenAmount
false, // bypassFee
loanDataBytes
);
require(sourceTokenAmountUsed <= swapAmount, "excessive source amount");
}
}