-
Notifications
You must be signed in to change notification settings - Fork 16
/
Copy pathSellCreditMarket.sol
204 lines (177 loc) · 8.24 KB
/
SellCreditMarket.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
// SPDX-License-Identifier: MIT
pragma solidity 0.8.23;
import {CreditPosition, DebtPosition, LoanLibrary, RESERVED_ID} from "@src/libraries/LoanLibrary.sol";
import {Math, PERCENT} from "@src/libraries/Math.sol";
import {LoanOffer, OfferLibrary} from "@src/libraries/OfferLibrary.sol";
import {VariablePoolBorrowRateParams} from "@src/libraries/YieldCurveLibrary.sol";
import {State} from "@src/SizeStorage.sol";
import {AccountingLibrary} from "@src/libraries/AccountingLibrary.sol";
import {RiskLibrary} from "@src/libraries/RiskLibrary.sol";
import {Errors} from "@src/libraries/Errors.sol";
import {Events} from "@src/libraries/Events.sol";
struct SellCreditMarketParams {
// The lender
address lender;
// The credit position ID to sell
// If RESERVED_ID, a new credit position will be created
uint256 creditPositionId;
// The amount of credit to sell
uint256 amount;
// The tenor of the loan
// If creditPositionId is not RESERVED_ID, this value is ignored and the tenor of the existing loan is used
uint256 tenor;
// The deadline for the transaction
uint256 deadline;
// The maximum APR for the loan
uint256 maxAPR;
// Whether amount means credit or cash
bool exactAmountIn;
}
/// @title SellCreditMarket
/// @custom:security-contact [email protected]
/// @author Size (https://size.credit/)
/// @notice Contains the logic for selling credit (borrowing) as a market order
library SellCreditMarket {
using OfferLibrary for LoanOffer;
using LoanLibrary for DebtPosition;
using LoanLibrary for CreditPosition;
using LoanLibrary for State;
using RiskLibrary for State;
using AccountingLibrary for State;
/// @notice Validates the input parameters for selling credit as a market order
/// @param state The state
/// @param params The input parameters for selling credit as a market order
function validateSellCreditMarket(State storage state, SellCreditMarketParams calldata params) external view {
LoanOffer memory loanOffer = state.data.users[params.lender].loanOffer;
uint256 tenor;
// validate msg.sender
// N/A
// validate lender
if (loanOffer.isNull()) {
revert Errors.INVALID_LOAN_OFFER(params.lender);
}
// validate creditPositionId
if (params.creditPositionId == RESERVED_ID) {
tenor = params.tenor;
// validate tenor
if (tenor < state.riskConfig.minTenor || tenor > state.riskConfig.maxTenor) {
revert Errors.TENOR_OUT_OF_RANGE(tenor, state.riskConfig.minTenor, state.riskConfig.maxTenor);
}
} else {
CreditPosition storage creditPosition = state.getCreditPosition(params.creditPositionId);
DebtPosition storage debtPosition = state.getDebtPositionByCreditPositionId(params.creditPositionId);
if (msg.sender != creditPosition.lender) {
revert Errors.BORROWER_IS_NOT_LENDER(msg.sender, creditPosition.lender);
}
if (!state.isCreditPositionTransferrable(params.creditPositionId)) {
revert Errors.CREDIT_POSITION_NOT_TRANSFERRABLE(
params.creditPositionId,
state.getLoanStatus(params.creditPositionId),
state.collateralRatio(debtPosition.borrower)
);
}
tenor = debtPosition.dueDate - block.timestamp; // positive since the credit position is transferrable, so the loan must be ACTIVE
// validate amount
if (params.amount > creditPosition.credit) {
revert Errors.NOT_ENOUGH_CREDIT(params.amount, creditPosition.credit);
}
}
// validate amount
if (params.amount < state.riskConfig.minimumCreditBorrowAToken) {
revert Errors.CREDIT_LOWER_THAN_MINIMUM_CREDIT(params.amount, state.riskConfig.minimumCreditBorrowAToken);
}
// validate tenor
if (block.timestamp + tenor > loanOffer.maxDueDate) {
revert Errors.DUE_DATE_GREATER_THAN_MAX_DUE_DATE(block.timestamp + tenor, loanOffer.maxDueDate);
}
// validate deadline
if (params.deadline < block.timestamp) {
revert Errors.PAST_DEADLINE(params.deadline);
}
// validate maxAPR
uint256 apr = loanOffer.getAPRByTenor(
VariablePoolBorrowRateParams({
variablePoolBorrowRate: state.oracle.variablePoolBorrowRate,
variablePoolBorrowRateUpdatedAt: state.oracle.variablePoolBorrowRateUpdatedAt,
variablePoolBorrowRateStaleRateInterval: state.oracle.variablePoolBorrowRateStaleRateInterval
}),
tenor
);
if (apr > params.maxAPR) {
revert Errors.APR_GREATER_THAN_MAX_APR(apr, params.maxAPR);
}
// validate exactAmountIn
// N/A
}
/// @notice Executes the selling of credit as a market order
/// @param state The state
/// @param params The input parameters for selling credit as a market order
function executeSellCreditMarket(State storage state, SellCreditMarketParams calldata params)
external
returns (uint256 cashAmountOut)
{
emit Events.SellCreditMarket(
params.lender, params.creditPositionId, params.tenor, params.amount, params.tenor, params.exactAmountIn
);
// slither-disable-next-line uninitialized-local
CreditPosition memory creditPosition;
uint256 tenor;
if (params.creditPositionId == RESERVED_ID) {
tenor = params.tenor;
} else {
DebtPosition storage debtPosition = state.getDebtPositionByCreditPositionId(params.creditPositionId);
creditPosition = state.getCreditPosition(params.creditPositionId);
tenor = debtPosition.dueDate - block.timestamp;
}
uint256 ratePerTenor = state.data.users[params.lender].loanOffer.getRatePerTenor(
VariablePoolBorrowRateParams({
variablePoolBorrowRate: state.oracle.variablePoolBorrowRate,
variablePoolBorrowRateUpdatedAt: state.oracle.variablePoolBorrowRateUpdatedAt,
variablePoolBorrowRateStaleRateInterval: state.oracle.variablePoolBorrowRateStaleRateInterval
}),
tenor
);
uint256 creditAmountIn;
uint256 fees;
if (params.exactAmountIn) {
creditAmountIn = params.amount;
(cashAmountOut, fees) = state.getCashAmountOut({
creditAmountIn: creditAmountIn,
maxCredit: params.creditPositionId == RESERVED_ID ? creditAmountIn : creditPosition.credit,
ratePerTenor: ratePerTenor,
tenor: tenor
});
} else {
cashAmountOut = params.amount;
(creditAmountIn, fees) = state.getCreditAmountIn({
cashAmountOut: cashAmountOut,
maxCashAmountOut: params.creditPositionId == RESERVED_ID
? cashAmountOut
: Math.mulDivDown(creditPosition.credit, PERCENT - state.getSwapFeePercent(tenor), PERCENT + ratePerTenor),
maxCredit: params.creditPositionId == RESERVED_ID
? Math.mulDivUp(cashAmountOut, PERCENT + ratePerTenor, PERCENT - state.getSwapFeePercent(tenor))
: creditPosition.credit,
ratePerTenor: ratePerTenor,
tenor: tenor
});
}
if (params.creditPositionId == RESERVED_ID) {
// slither-disable-next-line unused-return
state.createDebtAndCreditPositions({
lender: msg.sender,
borrower: msg.sender,
futureValue: creditAmountIn,
dueDate: block.timestamp + tenor
});
}
state.createCreditPosition({
exitCreditPositionId: params.creditPositionId == RESERVED_ID
? state.data.nextCreditPositionId - 1
: params.creditPositionId,
lender: params.lender,
credit: creditAmountIn
});
state.data.borrowAToken.transferFrom(params.lender, msg.sender, cashAmountOut);
state.data.borrowAToken.transferFrom(params.lender, state.feeConfig.feeRecipient, fees);
}
}