1
+ /*
2
+ Copyright 2021 Set Labs Inc.
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+
16
+ SPDX-License-Identifier: Apache License, Version 2.0
17
+ */
18
+
19
+ pragma solidity 0.6.10 ;
20
+ pragma experimental "ABIEncoderV2 " ;
21
+
22
+ import { IERC20 } from "@openzeppelin/contracts/token/ERC20/IERC20.sol " ;
23
+ import { SafeERC20 } from "@openzeppelin/contracts/token/ERC20/SafeERC20.sol " ;
24
+
25
+ import { DebtIssuanceModule } from "./DebtIssuanceModule.sol " ;
26
+ import { IController } from "../../interfaces/IController.sol " ;
27
+ import { Invoke } from "../lib/Invoke.sol " ;
28
+ import { ISetToken } from "../../interfaces/ISetToken.sol " ;
29
+ import { IssuanceValidationUtils } from "../lib/IssuanceValidationUtils.sol " ;
30
+ import { Position } from "../lib/Position.sol " ;
31
+
32
+ /**
33
+ * @title DebtIssuanceModuleV2
34
+ * @author Set Protocol
35
+ *
36
+ * The DebtIssuanceModuleV2 is a module that enables users to issue and redeem SetTokens that contain default and all
37
+ * external positions, including debt positions. Module hooks are added to allow for syncing of positions, and component
38
+ * level hooks are added to ensure positions are replicated correctly. The manager can define arbitrary issuance logic
39
+ * in the manager hook, as well as specify issue and redeem fees.
40
+ *
41
+ * NOTE:
42
+ * DebtIssuanceModule contract confirms increase/decrease in balance of component held by the SetToken after every transfer in/out
43
+ * for each component during issuance/redemption. This contract replaces those strict checks with slightly looser checks which
44
+ * ensure that the SetToken remains collateralized after every transfer in/out for each component during issuance/redemption.
45
+ * This module should be used to issue/redeem SetToken whose one or more components return a balance value with +/-1 wei error.
46
+ * For example, this module can be used to issue/redeem SetTokens which has one or more aTokens as its components.
47
+ * The new checks do NOT apply to any transfers that are part of an external position. A token that has rounding issues may lead to
48
+ * reverts if it is included as an external position unless explicitly allowed in a module hook.
49
+ *
50
+ * The getRequiredComponentIssuanceUnits function on this module assumes that Default token balances will be synced on every issuance
51
+ * and redemption. If token balances are not being synced it will over-estimate the amount of tokens required to issue a Set.
52
+ */
53
+ contract DebtIssuanceModuleV2 is DebtIssuanceModule {
54
+ using Position for uint256 ;
55
+
56
+ /* ============ Constructor ============ */
57
+
58
+ constructor (IController _controller ) public DebtIssuanceModule (_controller) {}
59
+
60
+ /* ============ External Functions ============ */
61
+
62
+ /**
63
+ * Deposits components to the SetToken, replicates any external module component positions and mints
64
+ * the SetToken. If the token has a debt position all collateral will be transferred in first then debt
65
+ * will be returned to the minting address. If specified, a fee will be charged on issuance.
66
+ *
67
+ * NOTE: Overrides DebtIssuanceModule#issue external function and adds undercollateralization checks in place of the
68
+ * previous default strict balances checks. The undercollateralization checks are implemented in IssuanceValidationUtils library and they
69
+ * revert upon undercollateralization of the SetToken post component transfer.
70
+ *
71
+ * @param _setToken Instance of the SetToken to issue
72
+ * @param _quantity Quantity of SetToken to issue
73
+ * @param _to Address to mint SetToken to
74
+ */
75
+ function issue (
76
+ ISetToken _setToken ,
77
+ uint256 _quantity ,
78
+ address _to
79
+ )
80
+ external
81
+ override
82
+ nonReentrant
83
+ onlyValidAndInitializedSet (_setToken)
84
+ {
85
+ require (_quantity > 0 , "Issue quantity must be > 0 " );
86
+
87
+ address hookContract = _callManagerPreIssueHooks (_setToken, _quantity, msg .sender , _to);
88
+
89
+ _callModulePreIssueHooks (_setToken, _quantity);
90
+
91
+
92
+ uint256 initialSetSupply = _setToken.totalSupply ();
93
+
94
+ (
95
+ uint256 quantityWithFees ,
96
+ uint256 managerFee ,
97
+ uint256 protocolFee
98
+ ) = calculateTotalFees (_setToken, _quantity, true );
99
+
100
+ // Prevent stack too deep
101
+ {
102
+ (
103
+ address [] memory components ,
104
+ uint256 [] memory equityUnits ,
105
+ uint256 [] memory debtUnits
106
+ ) = _calculateRequiredComponentIssuanceUnits (_setToken, quantityWithFees, true );
107
+
108
+ uint256 finalSetSupply = initialSetSupply.add (quantityWithFees);
109
+
110
+ _resolveEquityPositions (_setToken, quantityWithFees, _to, true , components, equityUnits, initialSetSupply, finalSetSupply);
111
+ _resolveDebtPositions (_setToken, quantityWithFees, true , components, debtUnits, initialSetSupply, finalSetSupply);
112
+ _resolveFees (_setToken, managerFee, protocolFee);
113
+ }
114
+
115
+ _setToken.mint (_to, _quantity);
116
+
117
+ emit SetTokenIssued (
118
+ _setToken,
119
+ msg .sender ,
120
+ _to,
121
+ hookContract,
122
+ _quantity,
123
+ managerFee,
124
+ protocolFee
125
+ );
126
+ }
127
+
128
+ /**
129
+ * Returns components from the SetToken, unwinds any external module component positions and burns the SetToken.
130
+ * If the token has debt positions, the module transfers in the required debt amounts from the caller and uses
131
+ * those funds to repay the debts on behalf of the SetToken. All debt will be paid down first then equity positions
132
+ * will be returned to the minting address. If specified, a fee will be charged on redeem.
133
+ *
134
+ * NOTE: Overrides DebtIssuanceModule#redeem internal function and adds undercollateralization checks in place of the
135
+ * previous default strict balances checks. The undercollateralization checks are implemented in IssuanceValidationUtils library
136
+ * and they revert upon undercollateralization of the SetToken post component transfer.
137
+ *
138
+ * @param _setToken Instance of the SetToken to redeem
139
+ * @param _quantity Quantity of SetToken to redeem
140
+ * @param _to Address to send collateral to
141
+ */
142
+ function redeem (
143
+ ISetToken _setToken ,
144
+ uint256 _quantity ,
145
+ address _to
146
+ )
147
+ external
148
+ override
149
+ nonReentrant
150
+ onlyValidAndInitializedSet (_setToken)
151
+ {
152
+ require (_quantity > 0 , "Redeem quantity must be > 0 " );
153
+
154
+ _callModulePreRedeemHooks (_setToken, _quantity);
155
+
156
+ uint256 initialSetSupply = _setToken.totalSupply ();
157
+
158
+ // Place burn after pre-redeem hooks because burning tokens may lead to false accounting of synced positions
159
+ _setToken.burn (msg .sender , _quantity);
160
+
161
+ (
162
+ uint256 quantityNetFees ,
163
+ uint256 managerFee ,
164
+ uint256 protocolFee
165
+ ) = calculateTotalFees (_setToken, _quantity, false );
166
+
167
+ // Prevent stack too deep
168
+ {
169
+ (
170
+ address [] memory components ,
171
+ uint256 [] memory equityUnits ,
172
+ uint256 [] memory debtUnits
173
+ ) = _calculateRequiredComponentIssuanceUnits (_setToken, quantityNetFees, false );
174
+
175
+ uint256 finalSetSupply = initialSetSupply.sub (quantityNetFees);
176
+
177
+ _resolveDebtPositions (_setToken, quantityNetFees, false , components, debtUnits, initialSetSupply, finalSetSupply);
178
+ _resolveEquityPositions (_setToken, quantityNetFees, _to, false , components, equityUnits, initialSetSupply, finalSetSupply);
179
+ _resolveFees (_setToken, managerFee, protocolFee);
180
+ }
181
+
182
+ emit SetTokenRedeemed (
183
+ _setToken,
184
+ msg .sender ,
185
+ _to,
186
+ _quantity,
187
+ managerFee,
188
+ protocolFee
189
+ );
190
+ }
191
+
192
+ /* ============ External View Functions ============ */
193
+
194
+ /**
195
+ * Calculates the amount of each component needed to collateralize passed issue quantity plus fees of Sets as well as amount of debt
196
+ * that will be returned to caller. Default equity alues are calculated based on token balances and not position units in order to more
197
+ * closely track any accrued tokens that will be synced during issuance. External equity and debt positions will use the stored position
198
+ * units. IF TOKEN VALUES ARE NOT BEING SYNCED DURING ISSUANCE THIS FUNCTION WILL OVER ESTIMATE THE AMOUNT OF REQUIRED TOKENS.
199
+ *
200
+ * @param _setToken Instance of the SetToken to issue
201
+ * @param _quantity Amount of Sets to be issued
202
+ *
203
+ * @return address[] Array of component addresses making up the Set
204
+ * @return uint256[] Array of equity notional amounts of each component, respectively, represented as uint256
205
+ * @return uint256[] Array of debt notional amounts of each component, respectively, represented as uint256
206
+ */
207
+ function getRequiredComponentIssuanceUnits (
208
+ ISetToken _setToken ,
209
+ uint256 _quantity
210
+ )
211
+ external
212
+ view
213
+ override
214
+ returns (address [] memory , uint256 [] memory , uint256 [] memory )
215
+ {
216
+ (
217
+ uint256 totalQuantity ,,
218
+ ) = calculateTotalFees (_setToken, _quantity, true );
219
+
220
+ if (_setToken.totalSupply () == 0 ) {
221
+ return _calculateRequiredComponentIssuanceUnits (_setToken, totalQuantity, true );
222
+ } else {
223
+ (
224
+ address [] memory components ,
225
+ uint256 [] memory equityUnits ,
226
+ uint256 [] memory debtUnits
227
+ ) = _getTotalIssuanceUnitsFromBalances (_setToken);
228
+
229
+ uint256 componentsLength = components.length ;
230
+ uint256 [] memory totalEquityUnits = new uint256 [](componentsLength);
231
+ uint256 [] memory totalDebtUnits = new uint256 [](componentsLength);
232
+ for (uint256 i = 0 ; i < components.length ; i++ ) {
233
+ // Use preciseMulCeil to round up to ensure overcollateration of equity when small issue quantities are provided
234
+ // and use preciseMul to round debt calculations down to make sure we don't return too much debt to issuer
235
+ totalEquityUnits[i] = equityUnits[i].preciseMulCeil (totalQuantity);
236
+ totalDebtUnits[i] = debtUnits[i].preciseMul (totalQuantity);
237
+ }
238
+
239
+ return (components, totalEquityUnits, totalDebtUnits);
240
+ }
241
+ }
242
+
243
+ /* ============ Internal Functions ============ */
244
+
245
+ /**
246
+ * Resolve equity positions associated with SetToken. On issuance, the total equity position for an asset (including default and external
247
+ * positions) is transferred in. Then any external position hooks are called to transfer the external positions to their necessary place.
248
+ * On redemption all external positions are recalled by the external position hook, then those position plus any default position are
249
+ * transferred back to the _to address.
250
+ */
251
+ function _resolveEquityPositions (
252
+ ISetToken _setToken ,
253
+ uint256 _quantity ,
254
+ address _to ,
255
+ bool _isIssue ,
256
+ address [] memory _components ,
257
+ uint256 [] memory _componentEquityQuantities ,
258
+ uint256 _initialSetSupply ,
259
+ uint256 _finalSetSupply
260
+ )
261
+ internal
262
+ {
263
+ for (uint256 i = 0 ; i < _components.length ; i++ ) {
264
+ address component = _components[i];
265
+ uint256 componentQuantity = _componentEquityQuantities[i];
266
+ if (componentQuantity > 0 ) {
267
+ if (_isIssue) {
268
+ // Call SafeERC20#safeTransferFrom instead of ExplicitERC20#transferFrom
269
+ SafeERC20.safeTransferFrom (
270
+ IERC20 (component),
271
+ msg .sender ,
272
+ address (_setToken),
273
+ componentQuantity
274
+ );
275
+
276
+ IssuanceValidationUtils.validateCollateralizationPostTransferInPreHook (_setToken, component, _initialSetSupply, componentQuantity);
277
+
278
+ _executeExternalPositionHooks (_setToken, _quantity, IERC20 (component), true , true );
279
+ } else {
280
+ _executeExternalPositionHooks (_setToken, _quantity, IERC20 (component), false , true );
281
+
282
+ // Call Invoke#invokeTransfer instead of Invoke#strictInvokeTransfer
283
+ _setToken.invokeTransfer (component, _to, componentQuantity);
284
+
285
+ IssuanceValidationUtils.validateCollateralizationPostTransferOut (_setToken, component, _finalSetSupply);
286
+ }
287
+ }
288
+ }
289
+ }
290
+
291
+ /**
292
+ * Resolve debt positions associated with SetToken. On issuance, debt positions are entered into by calling the external position hook. The
293
+ * resulting debt is then returned to the calling address. On redemption, the module transfers in the required debt amount from the caller
294
+ * and uses those funds to repay the debt on behalf of the SetToken.
295
+ */
296
+ function _resolveDebtPositions (
297
+ ISetToken _setToken ,
298
+ uint256 _quantity ,
299
+ bool _isIssue ,
300
+ address [] memory _components ,
301
+ uint256 [] memory _componentDebtQuantities ,
302
+ uint256 _initialSetSupply ,
303
+ uint256 _finalSetSupply
304
+ )
305
+ internal
306
+ {
307
+ for (uint256 i = 0 ; i < _components.length ; i++ ) {
308
+ address component = _components[i];
309
+ uint256 componentQuantity = _componentDebtQuantities[i];
310
+ if (componentQuantity > 0 ) {
311
+ if (_isIssue) {
312
+ _executeExternalPositionHooks (_setToken, _quantity, IERC20 (component), true , false );
313
+
314
+ // Call Invoke#invokeTransfer instead of Invoke#strictInvokeTransfer
315
+ _setToken.invokeTransfer (component, msg .sender , componentQuantity);
316
+
317
+ IssuanceValidationUtils.validateCollateralizationPostTransferOut (_setToken, component, _finalSetSupply);
318
+ } else {
319
+ // Call SafeERC20#safeTransferFrom instead of ExplicitERC20#transferFrom
320
+ SafeERC20.safeTransferFrom (
321
+ IERC20 (component),
322
+ msg .sender ,
323
+ address (_setToken),
324
+ componentQuantity
325
+ );
326
+
327
+ IssuanceValidationUtils.validateCollateralizationPostTransferInPreHook (_setToken, component, _initialSetSupply, componentQuantity);
328
+
329
+ _executeExternalPositionHooks (_setToken, _quantity, IERC20 (component), false , false );
330
+ }
331
+ }
332
+ }
333
+ }
334
+ /**
335
+ * Reimplementation of _getTotalIssuanceUnits but instead derives Default equity positions from token balances on Set instead of from
336
+ * position units. This function is ONLY to be used in getRequiredComponentIssuanceUnits in order to return more accurate required
337
+ * token amounts to issuers when positions are being synced on issuance.
338
+ *
339
+ * @param _setToken Instance of the SetToken to issue
340
+ *
341
+ * @return address[] Array of component addresses making up the Set
342
+ * @return uint256[] Array of equity unit amounts of each component, respectively, represented as uint256
343
+ * @return uint256[] Array of debt unit amounts of each component, respectively, represented as uint256
344
+ */
345
+ function _getTotalIssuanceUnitsFromBalances (
346
+ ISetToken _setToken
347
+ )
348
+ internal
349
+ view
350
+ returns (address [] memory , uint256 [] memory , uint256 [] memory )
351
+ {
352
+ address [] memory components = _setToken.getComponents ();
353
+ uint256 componentsLength = components.length ;
354
+
355
+ uint256 [] memory equityUnits = new uint256 [](componentsLength);
356
+ uint256 [] memory debtUnits = new uint256 [](componentsLength);
357
+
358
+ uint256 totalSupply = _setToken.totalSupply ();
359
+
360
+ for (uint256 i = 0 ; i < components.length ; i++ ) {
361
+ address component = components[i];
362
+ int256 cumulativeEquity = totalSupply
363
+ .getDefaultPositionUnit (IERC20 (component).balanceOf (address (_setToken)))
364
+ .toInt256 ();
365
+ int256 cumulativeDebt = 0 ;
366
+ address [] memory externalPositions = _setToken.getExternalPositionModules (component);
367
+
368
+ if (externalPositions.length > 0 ) {
369
+ for (uint256 j = 0 ; j < externalPositions.length ; j++ ) {
370
+ int256 externalPositionUnit = _setToken.getExternalPositionRealUnit (component, externalPositions[j]);
371
+
372
+ // If positionUnit <= 0 it will be "added" to debt position
373
+ if (externalPositionUnit > 0 ) {
374
+ cumulativeEquity = cumulativeEquity.add (externalPositionUnit);
375
+ } else {
376
+ cumulativeDebt = cumulativeDebt.add (externalPositionUnit);
377
+ }
378
+ }
379
+ }
380
+
381
+ equityUnits[i] = cumulativeEquity.toUint256 ();
382
+ debtUnits[i] = cumulativeDebt.mul (- 1 ).toUint256 ();
383
+ }
384
+
385
+ return (components, equityUnits, debtUnits);
386
+ }
387
+ }
0 commit comments