• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

IndexCoop / index-protocol / e40481c6-2e74-4945-86d5-3797c691eb2a

11 Nov 2024 03:23PM UTC coverage: 96.137%. Remained the same
e40481c6-2e74-4945-86d5-3797c691eb2a

push

circleci

web-flow
feat(MorphoLeverageModule): Add exitCollateralPosition method and redeposit leftover collateral on delever (#53)

* feat: Add exitCollateralPosition method and redeposit leftover collateral on delever

* fix test

* fix test

2283 of 2438 branches covered (93.64%)

Branch coverage included in aggregate %.

15 of 16 new or added lines in 1 file covered. (93.75%)

13 existing lines in 1 file now uncovered.

3466 of 3542 relevant lines covered (97.85%)

246.2 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

78.39
/contracts/protocol/modules/v1/MorphoLeverageModule.sol
1
/*
2
    Copyright 2024 Index Coop
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 { Ownable } from "@openzeppelin/contracts/access/Ownable.sol";
24
import { ReentrancyGuard } from "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
25

26
import { IController } from "../../../interfaces/IController.sol";
27
import { IDebtIssuanceModule } from "../../../interfaces/IDebtIssuanceModule.sol";
28
import { IExchangeAdapter } from "../../../interfaces/IExchangeAdapter.sol";
29
import { IModuleIssuanceHook } from "../../../interfaces/IModuleIssuanceHook.sol";
30
import { ISetToken } from "../../../interfaces/ISetToken.sol";
31
import { MarketParams, Market, IMorpho, Position } from "../../../interfaces/external/morpho/IMorpho.sol";
32
import { Morpho } from "../../../protocol/integration/lib/Morpho.sol";
33
import { MorphoMarketParams } from "../../../protocol/integration/lib/MorphoMarketParams.sol";
34
import { MorphoSharesMath } from "../../../protocol/integration/lib/MorphoSharesMath.sol";
35
import { MorphoBalancesLib } from "../../../protocol/integration/lib/MorphoBalancesLib.sol";
36
import { ModuleBase } from "../../lib/ModuleBase.sol";
37

38
/**
39
 * @title Morpho Leverage Module
40
 * @author Index Coop
41
 * @notice Smart contract that enables leverage trading using Morpho Blue as the lending protocol.
42
 */
43
contract MorphoLeverageModule is ModuleBase, ReentrancyGuard, Ownable, IModuleIssuanceHook {
44
    using Morpho for ISetToken;
45
    using MorphoMarketParams for MarketParams;
46
    using MorphoSharesMath for uint128;
47
    using MorphoBalancesLib for  IMorpho;
48

49
    /* ============ Structs ============ */
50

51
    struct ActionInfo {
52
        ISetToken setToken;                      // SetToken instance
53
        IExchangeAdapter exchangeAdapter;        // Exchange adapter instance
54
        uint256 setTotalSupply;                  // Total supply of SetToken
55
        uint256 notionalSendQuantity;            // Total notional quantity sent to exchange
56
        uint256 minNotionalReceiveQuantity;      // Min total notional received from exchange
57
        IERC20 collateralAsset;                  // Address of collateral asset
58
        IERC20 borrowAsset;                      // Address of borrow asset
59
        uint256 preTradeReceiveTokenBalance;     // Balance of pre-trade receive token balance
60
    }
61

62
    /* ============ Events ============ */
63

64
    /**
65
     * @dev Emitted on lever()
66
     * @param _setToken             Instance of the SetToken being levered
67
     * @param _borrowAsset          Asset being borrowed for leverage
68
     * @param _collateralAsset      Collateral asset being levered
69
     * @param _exchangeAdapter      Exchange adapter used for trading
70
     * @param _totalBorrowAmount    Total amount of `_borrowAsset` borrowed
71
     * @param _totalReceiveAmount   Total amount of `_collateralAsset` received by selling `_borrowAsset`
72
     * @param _protocolFee          Protocol fee charged
73
     */
74
    event LeverageIncreased(
75
        ISetToken indexed _setToken,
76
        IERC20 indexed _borrowAsset,
77
        IERC20 indexed _collateralAsset,
78
        IExchangeAdapter _exchangeAdapter,
79
        uint256 _totalBorrowAmount,
80
        uint256 _totalReceiveAmount,
81
        uint256 _protocolFee
82
    );
83

84
    /**
85
     * @dev Emitted on delever() and deleverToZeroBorrowBalance()
86
     * @param _setToken             Instance of the SetToken being delevered
87
     * @param _collateralAsset      Asset sold to decrease leverage
88
     * @param _repayAsset           Asset being bought to repay to Morpho
89
     * @param _exchangeAdapter      Exchange adapter used for trading
90
     * @param _totalRedeemAmount    Total amount of `_collateralAsset` being sold
91
     * @param _totalRepayAmount     Total amount of `_repayAsset` being repaid
92
     * @param _protocolFee          Protocol fee charged
93
     */
94
    event LeverageDecreased(
95
        ISetToken indexed _setToken,
96
        IERC20 indexed _collateralAsset,
97
        IERC20 indexed _repayAsset,
98
        IExchangeAdapter _exchangeAdapter,
99
        uint256 _totalRedeemAmount,
100
        uint256 _totalRepayAmount,
101
        uint256 _protocolFee
102
    );
103

104
    /**
105
     * @dev Emitted on updateAllowedSetToken()
106
     * @param _setToken SetToken being whose allowance to initialize this module is being updated
107
     * @param _added    true if added false if removed
108
     */
109
    event SetTokenStatusUpdated(
110
        ISetToken indexed _setToken,
111
        bool indexed _added
112
    );
113

114
    /**
115
     * @dev Emitted on updateAnySetAllowed()
116
     * @param _anySetAllowed    true if any set is allowed to initialize this module, false otherwise
117
     */
118
    event AnySetAllowedUpdated(
119
        bool indexed _anySetAllowed
120
    );
121

122
    /**
123
     * @dev Emitted on updateAnySetAllowed()
124
     * @param _setToken SetToken whose Morpho Market params are updated
125
     * @param _marketId Morpho Market Id corresponding to the Market Params that have been set 
126
     */
127
    event MorphoMarketUpdated(
128
        ISetToken indexed _setToken,
129
        bytes32 _marketId
130
    );
131

132
    /* ============ Constants ============ */
133

134

135
    // String identifying the DebtIssuanceModule in the IntegrationRegistry. Note: Governance must add DefaultIssuanceModule as
136
    // the string as the integration name
137
    string constant internal DEFAULT_ISSUANCE_MODULE_NAME = "DefaultIssuanceModule";
138

139
    // 0 index stores protocol fee % on the controller, charged in the _executeTrade function
140
    uint256 constant internal PROTOCOL_TRADE_FEE_INDEX = 0;
141

142
    IMorpho public immutable morpho;
143

144
    /* ============ State Variables ============ */
145

146

147
    // Mapping of SetToken to Morpho MarketParams defining the market on which to leverage
148
    mapping(ISetToken => MarketParams) public marketParams;
149

150
    // Mapping of SetToken to boolean indicating if SetToken is on allow list. Updateable by governance
151
    mapping(ISetToken => bool) public allowedSetTokens;
152

153
    // Boolean that returns if any SetToken can initialize this module. If false, then subject to allow list. Updateable by governance.
154
    bool public anySetAllowed;
155

156
    /* ============ Constructor ============ */
157

158
    /**
159
     * @dev Set morpho contract address
160
     * @param _controller                       Address of controller contract
161
     * @param _morpho                           Address of morpho contract
162
     */
163
    constructor(
164
        IController _controller,
165
        IMorpho _morpho
166
    )
167
        public
168
        ModuleBase(_controller)
169
    {
170
        morpho = _morpho;
171
    }
172

173
    /* ============ External Functions ============ */
174

175
    /**
176
     * @dev MANAGER ONLY: Increases leverage for a given collateral position using an enabled borrow asset.
177
     * Retrieves borrow / collateral tokens from configured Morpho Market Params
178
     * Borrows borrow token from Morpho. Performs a DEX trade, exchanging the borrow token for collateral token.
179
     * Deposits collateral token to Morpho Market
180
     * Note: Assumes that at the time the function is called, the set has already deposited sufficient collateral tokens in morpho to borrow against
181
     * @param _setToken                     Instance of the SetToken
182
     * @param _borrowQuantityUnits          Borrow quantity of asset in position units
183
     * @param _minReceiveQuantityUnits      Min receive quantity of collateral asset to receive post-trade in position units
184
     * @param _tradeAdapterName             Name of trade adapter
185
     * @param _tradeData                    Arbitrary data for trade
186
     */
187
    function lever(
188
        ISetToken _setToken,
189
        uint256 _borrowQuantityUnits,
190
        uint256 _minReceiveQuantityUnits,
191
        string memory _tradeAdapterName,
192
        bytes memory _tradeData
193
    )
194
        external
195
        nonReentrant
3!
196
        onlyManagerAndValidSet(_setToken)
3!
197
    {
198
        MarketParams memory setMarketParams = marketParams[_setToken]; 
3✔
199
        require(setMarketParams.collateralToken != address(0), "Collateral not set");
3!
200

201
        // For levering up, send quantity is derived from borrow asset and receive quantity is derived from
202
        // collateral asset
203
        ActionInfo memory leverInfo = _createAndValidateActionInfo(
3✔
204
            _setToken,
205
            IERC20(setMarketParams.loanToken),
206
            IERC20(setMarketParams.collateralToken),
207
            _borrowQuantityUnits,
208
            _minReceiveQuantityUnits,
209
            _tradeAdapterName,
210
            true
211
        );
212

213
        _borrow(leverInfo.setToken, setMarketParams, leverInfo.notionalSendQuantity);
3✔
214

215
        uint256 postTradeReceiveQuantity = _executeTrade(leverInfo, IERC20(setMarketParams.loanToken), IERC20(setMarketParams.collateralToken), _tradeData);
3✔
216

217
        uint256 protocolFee = _accrueProtocolFee(_setToken, IERC20(setMarketParams.collateralToken), postTradeReceiveQuantity);
3✔
218

219
        uint256 postTradeCollateralQuantity = postTradeReceiveQuantity.sub(protocolFee);
3✔
220

221
        _deposit(leverInfo.setToken, setMarketParams, postTradeCollateralQuantity);
3✔
222

223
        _sync(leverInfo.setToken);
3✔
224

225
        emit LeverageIncreased(
3✔
226
            _setToken,
227
            IERC20(setMarketParams.loanToken),
228
            IERC20(setMarketParams.collateralToken),
229
            leverInfo.exchangeAdapter,
230
            leverInfo.notionalSendQuantity,
231
            postTradeCollateralQuantity,
232
            protocolFee
233
        );
234
    }
235

236
    /**
237
     * @dev MANAGER ONLY: Decrease leverage for a given collateral position using an enabled borrow asset.
238
     * Determines collatera / borrow tokens based on configured morpho market params
239
     * Withdraws collateral token from Morpho Market. Performs a DEX trade, exchanging the collateral token for borrow token
240
     * Repays borrow token to Morpho
241
     * @param _setToken                 Instance of the SetToken
242
     * @param _redeemQuantityUnits      Quantity of collateral asset to delever in position units
243
     * @param _minRepayQuantityUnits    Minimum amount of repay asset to receive post trade in position units
244
     * @param _tradeAdapterName         Name of trade adapter
245
     * @param _tradeData                Arbitrary data for trade
246
     */
247
    function delever(
248
        ISetToken _setToken,
249
        uint256 _redeemQuantityUnits,
250
        uint256 _minRepayQuantityUnits,
251
        string memory _tradeAdapterName,
252
        bytes memory _tradeData
253
    )
254
        external
255
        nonReentrant
2!
256
        onlyManagerAndValidSet(_setToken)
2!
257
    {
258
        MarketParams memory setMarketParams = marketParams[_setToken]; 
2✔
259
        require(setMarketParams.collateralToken != address(0), "Collateral not set");
2!
260

261
        // Note: for delevering, send quantity is derived from collateral asset and receive quantity is derived from
262
        // repay asset
263
        ActionInfo memory deleverInfo = _createAndValidateActionInfo(
2✔
264
            _setToken,
265
            IERC20(setMarketParams.collateralToken),
266
            IERC20(setMarketParams.loanToken),
267
            _redeemQuantityUnits,
268
            _minRepayQuantityUnits,
269
            _tradeAdapterName,
270
            false
271
        );
272

273
        _withdraw(deleverInfo.setToken, setMarketParams, deleverInfo.notionalSendQuantity);
2✔
274

275
        uint256 postTradeReceiveQuantity = _executeTrade(deleverInfo, IERC20(setMarketParams.collateralToken), IERC20(setMarketParams.loanToken), _tradeData);
2✔
276

277
        uint256 protocolFee = _accrueProtocolFee(_setToken, IERC20(setMarketParams.loanToken), postTradeReceiveQuantity);
2✔
278

279
        uint256 repayQuantity = postTradeReceiveQuantity.sub(protocolFee);
2✔
280

281
        _repayBorrow(deleverInfo.setToken, setMarketParams, repayQuantity, 0);
2✔
282

283
        _updateRepayDefaultPosition(deleverInfo, IERC20(setMarketParams.loanToken));
2✔
284

285
        uint256 collateralBalance = deleverInfo.collateralAsset.balanceOf(address(deleverInfo.setToken));
2✔
286
        if(collateralBalance > 0) {
2!
NEW
287
            _deposit(_setToken, setMarketParams, collateralBalance);
×
288
        }
289

290
        _sync(deleverInfo.setToken);
2✔
291

292
        emit LeverageDecreased(
2✔
293
            _setToken,
294
            IERC20(setMarketParams.collateralToken),
295
            IERC20(setMarketParams.loanToken),
296
            deleverInfo.exchangeAdapter,
297
            deleverInfo.notionalSendQuantity,
298
            repayQuantity,
299
            protocolFee
300
        );
301
    }
302

303
    /** @dev MANAGER ONLY: Pays down the borrow token to 0 selling off a given amount of collateral asset.
304
     * Withdraws collateral token from Morpho. Performs a DEX trade, exchanging the collateral Token for borrow Token.
305
     * Minimum receive amount for the DEX trade is set to the current borrow asset balance on Morpho
306
     * Repays received borrow tokens to Morpho. Any extra received borrow asset is updated as equity
307
     * The function reverts if not enough collateral token is redeemed to buy the required minimum amount of borrow tokens.
308
     * @param _setToken             Instance of the SetToken
309
     * @param _redeemQuantityUnits  Quantity of collateral asset to delever in position units
310
     * @param _tradeAdapterName     Name of trade adapter
311
     * @param _tradeData            Arbitrary data for trade
312
     * @return uint256              Notional repay quantity
313
     */
314
    function deleverToZeroBorrowBalance(
315
        ISetToken _setToken,
316
        uint256 _redeemQuantityUnits,
317
        string memory _tradeAdapterName,
318
        bytes memory _tradeData
319
    )
320
        external
321
        nonReentrant
5!
322
        onlyManagerAndValidSet(_setToken)
5!
323
        returns (uint256)
324
    {
325
        MarketParams memory setMarketParams = marketParams[_setToken]; 
5✔
326
        require(setMarketParams.collateralToken != address(0), "Collateral not set");
5!
327
       
328
        uint256 setTotalSupply = _setToken.totalSupply();
5✔
329
        uint256 notionalRedeemQuantity = _redeemQuantityUnits.preciseMul(setTotalSupply);
5✔
330
        (,uint256 borrowBalance, uint256 borrowShares) = _getCollateralAndBorrowBalances(_setToken, setMarketParams);
5✔
331

332
        ActionInfo memory deleverInfo = _createAndValidateActionInfoNotional(
5✔
333
            _setToken,
334
            IERC20(setMarketParams.collateralToken),
335
            IERC20(setMarketParams.loanToken),
336
            notionalRedeemQuantity,
337
            borrowBalance,
338
            _tradeAdapterName,
339
            false,
340
            setTotalSupply
341
        );
342

343
        _withdraw(deleverInfo.setToken, setMarketParams, deleverInfo.notionalSendQuantity);
5✔
344

345
        _executeTrade(deleverInfo, IERC20(setMarketParams.collateralToken), IERC20(setMarketParams.loanToken), _tradeData);
5✔
346

347
        _repayBorrow(deleverInfo.setToken, setMarketParams, borrowBalance, borrowShares);
5✔
348

349
        _updateRepayDefaultPosition(deleverInfo, IERC20(setMarketParams.loanToken));
5✔
350
 
351
        uint256 collateralBalance = deleverInfo.collateralAsset.balanceOf(address(deleverInfo.setToken));
5✔
352
        if(collateralBalance > 0) {
5!
353
            _deposit(_setToken, setMarketParams, collateralBalance);
5✔
354
        }
355

356
       _sync(deleverInfo.setToken);
5✔
357

358
        emit LeverageDecreased(
5✔
359
            _setToken,
360
            IERC20(setMarketParams.collateralToken),
361
            IERC20(setMarketParams.loanToken),
362
            deleverInfo.exchangeAdapter,
363
            deleverInfo.notionalSendQuantity,
364
            borrowBalance,
365
            0   // No protocol fee
366
        );
367

368
        return borrowBalance;
5✔
369
    }
370

371
    /**
372
     * @dev CALLABLE BY ANYBODY: Sync Set positions with ALL enabled Morpho collateral and borrow positions.
373
     * @param _setToken               Instance of the SetToken
374
     */
375
    function sync(ISetToken _setToken) public nonReentrant onlyValidAndInitializedSet(_setToken) {
24!
376
        _sync(_setToken);
12✔
377
    }
378

379
    function _sync(ISetToken _setToken) internal {
380
        MarketParams memory setMarketParams = marketParams[_setToken];
25✔
381
        require(setMarketParams.collateralToken != address(0), "Collateral not set");
25!
382

383
        uint256 setTotalSupply = _setToken.totalSupply();
25✔
384
        (int256 newCollateralPositionUnit, int256 newBorrowPositionUnit) = _getCollateralAndBorrowPositions(_setToken, setMarketParams, setTotalSupply);
25✔
385

386
        int256 previousCollateralPositionUnit = _setToken.getExternalPositionRealUnit(setMarketParams.collateralToken, address(this));
25✔
387
        if (previousCollateralPositionUnit != newCollateralPositionUnit) {
25✔
388
            _updateExternalPosition(_setToken, setMarketParams.collateralToken, newCollateralPositionUnit);
17✔
389
        }
390

391
        int256 previousBorrowPositionUnit = _setToken.getExternalPositionRealUnit(setMarketParams.loanToken, address(this));
25✔
392
        // Note: Accounts for if borrowPosition does not exist on SetToken but is tracked in enabledAssets
393
        if (newBorrowPositionUnit != previousBorrowPositionUnit) {
25✔
394
            _updateExternalPosition(_setToken, setMarketParams.loanToken, newBorrowPositionUnit);
18✔
395
        }
396
    }
397

398
    /**
399
     * @dev MANAGER ONLY: Initializes this module to the SetToken. Either the SetToken needs to be on the allowed list
400
     * or anySetAllowed needs to be true. Only callable by the SetToken's manager.
401
     * Note: Managers can enable collateral and borrow assets that don't exist as positions on the SetToken
402
     * @param _setToken             Instance of the SetToken to initialize
403
     * @param _marketParams         Parameters defining the Morpho market to use for leveraging
404
     */
405
    function initialize(
406
        ISetToken _setToken,
407
        MarketParams memory _marketParams
408
    )
409
        external
410
        onlySetManager(_setToken, msg.sender)
10✔
411
        onlyValidAndPendingSet(_setToken)
9✔
412
    {
413
        if (!anySetAllowed) {
7✔
414
            require(allowedSetTokens[_setToken], "Not allowed SetToken");
6✔
415
        }
416

417
        // Initialize module before trying register
418
        _setToken.initializeModule();
6✔
419

420
        // Get debt issuance module registered to this module and require that it is initialized
421
        require(_setToken.isInitializedModule(getAndValidateAdapter(DEFAULT_ISSUANCE_MODULE_NAME)), "Issuance not initialized");
6✔
422

423
        // Try if register exists on any of the modules including the debt issuance module
424
        address[] memory modules = _setToken.getModules();
4✔
425
        for(uint256 i = 0; i < modules.length; i++) {
4✔
426
            try IDebtIssuanceModule(modules[i]).registerToIssuanceModule(_setToken) {} catch {}
8✔
427
        }
428

429
        _setMarketParams(_setToken, _marketParams);
4✔
430
    }
431

432
    /**
433
     * @dev MANAGER ONLY: Deposits full collateral token balance as collateral into the specified Morpho market
434
     * Will result in the collateral token position switching from being a default to an external position.
435
     * Note: At the time of calling this the set token should contain >0 balance of collateral tokens and no other position
436
     * @param _setToken             Instance of the SetToken for which to deposit collateral tokens into Morpho
437
     */
438
    function enterCollateralPosition(
439
        ISetToken _setToken
440
    )
441
        external
442
        onlyManagerAndValidSet(_setToken)
4!
443
    {
444
        MarketParams memory setMarketParams = marketParams[_setToken];
4✔
445
        uint256 collateralBalance = IERC20(setMarketParams.collateralToken).balanceOf(address(_setToken));
4✔
446
        require(collateralBalance > 0, "Collateral balance is 0");
4!
447
        _deposit(_setToken, setMarketParams, collateralBalance);
4✔
448
        // Remove default position for collateral token 
449
        _setToken.editDefaultPosition(setMarketParams.collateralToken, 0);
4✔
450
        sync(_setToken);
4✔
451
    }
452

453
    /**
454
     * @dev MANAGER ONLY: Withdraws full collateral position from the specified Morpho market
455
     * @param _setToken             Instance of the SetToken for which to withdraw collateral tokens from Morpho
456
     */
457
    function exitCollateralPosition(
458
        ISetToken _setToken
459
    )
460
        external
461
        onlyManagerAndValidSet(_setToken)
4!
462
    {
463
        MarketParams memory setMarketParams = marketParams[_setToken];
4✔
464
        bytes32 marketId = setMarketParams.id();
4✔
465
        Position memory position = morpho.position(marketId, address(_setToken));
4✔
466
        require(position.borrowShares == 0, "Borrow balance must be 0");
4✔
467
        _withdraw(_setToken, setMarketParams, position.collateral);
3✔
468

469
        _sync(_setToken);
3✔
470

471
        uint256 collateralNotionalBalance = IERC20(setMarketParams.collateralToken).balanceOf(address(_setToken));
3✔
472
        uint256 newCollateralPosition = collateralNotionalBalance.preciseDiv(_setToken.totalSupply());
3✔
473
        _setToken.editDefaultPosition(setMarketParams.collateralToken, newCollateralPosition);
3✔
474
    }
475

476

477
    /**
478
     * @dev MANAGER ONLY: Removes this module from the SetToken, via call by the SetToken. Any deposited collateral assets
479
     * are disabled to be used as collateral on Morpho. Morpho market params state is deleted.
480
     * Note: Function should revert is there is any debt remaining on Morpho
481
     */
482
    function removeModule()
483
        external
484
        override
485
        onlyValidAndInitializedSet(ISetToken(msg.sender))
×
486
    {
UNCOV
487
        ISetToken setToken = ISetToken(msg.sender);
×
488

UNCOV
489
        sync(setToken);
×
490

491
        delete marketParams[setToken];
492

493
        // Try if unregister exists on any of the modules
UNCOV
494
        address[] memory modules = setToken.getModules();
×
UNCOV
495
        for(uint256 i = 0; i < modules.length; i++) {
×
UNCOV
496
            try IDebtIssuanceModule(modules[i]).unregisterFromIssuanceModule(setToken) {} catch {}
×
497
        }
498
    }
499

500
    /**
501
     * @dev MANAGER ONLY: Add registration of this module on the debt issuance module for the SetToken.
502
     * Note: if the debt issuance module is not added to SetToken before this module is initialized, then this function
503
     * needs to be called if the debt issuance module is later added and initialized to prevent state inconsistencies
504
     * @param _setToken             Instance of the SetToken
505
     * @param _debtIssuanceModule   Debt issuance module address to register
506
     */
507
    function registerToModule(
508
        ISetToken _setToken,
509
        IDebtIssuanceModule _debtIssuanceModule
510
    )
511
        external
512
        onlyManagerAndValidSet(_setToken)
×
513
    {
UNCOV
514
        require(_setToken.isInitializedModule(address(_debtIssuanceModule)), "Issuance not initialized");
×
515

UNCOV
516
        _debtIssuanceModule.registerToIssuanceModule(_setToken);
×
517
    }
518

519
    /**
520
     * @dev GOVERNANCE ONLY: Enable/disable ability of a SetToken to initialize this module. Only callable by governance.
521
     * @param _setToken             Instance of the SetToken
522
     * @param _status               Bool indicating if _setToken is allowed to initialize this module
523
     */
524
    function updateAllowedSetToken(
525
        ISetToken _setToken,
526
        bool _status
527
    )
528
        external
529
        onlyOwner
2!
530
    {
531
        require(controller.isSet(address(_setToken)) || allowedSetTokens[_setToken], "Invalid SetToken");
2!
532
        allowedSetTokens[_setToken] = _status;
533
        emit SetTokenStatusUpdated(_setToken, _status);
2✔
534
    }
535

536
    /**
537
     * @dev GOVERNANCE ONLY: Toggle whether ANY SetToken is allowed to initialize this module. Only callable by governance.
538
     * @param _anySetAllowed             Bool indicating if ANY SetToken is allowed to initialize this module
539
     */
540
    function updateAnySetAllowed(bool _anySetAllowed) external onlyOwner {
1!
541
        anySetAllowed = _anySetAllowed;
542
        emit AnySetAllowedUpdated(_anySetAllowed);
1✔
543
    }
544

545
    /**
546
     * @dev MODULE ONLY: Hook called prior to issuance to sync positions on SetToken. Only callable by valid module.
547
     * @param _setToken             Instance of the SetToken
548
     */
549
    function moduleIssueHook(
550
        ISetToken _setToken,
551
        uint256 /* _setTokenQuantity */
552
    )
553
        external
554
        override
555
        onlyModule(_setToken)
4!
556
    {
557
        sync(_setToken);
4✔
558
    }
559

560
    /**
561
     * @dev MODULE ONLY: Hook called prior to redemption to sync positions on SetToken. For redemption, always use current borrowed
562
     * balance after interest accrual. Only callable by valid module.
563
     * @param _setToken             Instance of the SetToken
564
     */
565
    function moduleRedeemHook(
566
        ISetToken _setToken,
567
        uint256 /* _setTokenQuantity */
568
    )
569
        external
570
        override
571
        onlyModule(_setToken)
2!
572
    {
573
        sync(_setToken);
2✔
574
    }
575

576
    /**
577
     * @dev MODULE ONLY: Hook called prior to looping through each component on issuance. Invokes borrow in order for
578
     * module to return debt to issuer. Only callable by valid module.
579
     * @param _setToken             Instance of the SetToken
580
     * @param _setTokenQuantity     Quantity of SetToken
581
     * @param _component            Address of component
582
     */
583
    function componentIssueHook(
584
        ISetToken _setToken,
585
        uint256 _setTokenQuantity,
586
        IERC20 _component,
587
        bool _isEquity
588
    )
589
        external
590
        override
591
        onlyModule(_setToken)
14✔
592
    {
593
        // Check hook not being called for an equity position. If hook is called with equity position and outstanding borrow position 
594
        // exists the loan would be taken out twice potentially leading to liquidation
595
        MarketParams memory setMarketParams = marketParams[_setToken];
12✔
596
        if (_isEquity && setMarketParams.collateralToken == address(_component)) {
12✔
597
            int256 componentCollateral = _setToken.getExternalPositionRealUnit(address(_component), address(this));
4✔
598

599
            require(componentCollateral > 0, "Component must be negative");
4!
600

601
            uint256 notionalCollateral = componentCollateral.toUint256().preciseMul(_setTokenQuantity);
4✔
602
            _deposit(_setToken, setMarketParams, notionalCollateral);
4✔
603
        }
604
        if(!_isEquity) {
12✔
605
            require(setMarketParams.loanToken == address(_component), "Debt component mismatch");
7✔
606
            int256 componentDebt = _setToken.getExternalPositionRealUnit(address(_component), address(this));
6✔
607

608
            require(componentDebt < 0, "Component must be negative");
6!
609

610
            uint256 notionalDebt = componentDebt.mul(-1).toUint256().preciseMul(_setTokenQuantity);
6✔
611
            _borrow(_setToken, setMarketParams, notionalDebt);
6✔
612
        }
613
    }
614

615
    /**
616
     * @dev MODULE ONLY: Hook called prior to looping through each component on redemption. Invokes repay after
617
     * the issuance module transfers debt from the issuer. Only callable by valid module.
618
     * @param _setToken             Instance of the SetToken
619
     * @param _setTokenQuantity     Quantity of SetToken
620
     * @param _component            Address of component
621
     */
622
    function componentRedeemHook(
623
        ISetToken _setToken,
624
        uint256 _setTokenQuantity,
625
        IERC20 _component,
626
        bool _isEquity
627
    )
628
        external
629
        override
630
        onlyModule(_setToken)
10✔
631
    {
632
        MarketParams memory setMarketParams = marketParams[_setToken];
8✔
633
        if (_isEquity && setMarketParams.collateralToken == address(_component)) {
8✔
634
            int256 componentCollateral = _setToken.getExternalPositionRealUnit(address(_component), address(this));
2✔
635
            require(componentCollateral > 0, "Component must be negative");
2!
636
            uint256 notionalCollateral = componentCollateral.toUint256().preciseMul(_setTokenQuantity);
2✔
637
            _withdraw(_setToken, setMarketParams, notionalCollateral);
2✔
638
        }
639
        if(!_isEquity) {
8✔
640
            require(setMarketParams.loanToken == address(_component), "Debt component mismatch");
5✔
641
            int256 componentDebt = _setToken.getExternalPositionRealUnit(address(_component), address(this));
4✔
642

643
            require(componentDebt < 0, "Component must be negative");
4!
644

645
            uint256 notionalDebt = componentDebt.mul(-1).toUint256().preciseMul(_setTokenQuantity);
4✔
646
            _repayBorrow(_setToken, setMarketParams, notionalDebt, 0);
4✔
647
        }
648
    }
649

650

651
    function getMarketId(ISetToken _setToken) external view returns (bytes32) {
UNCOV
652
        MarketParams memory setMarketParams = marketParams[_setToken];
×
UNCOV
653
        require(setMarketParams.collateralToken != address(0), "Collateral not set");
×
UNCOV
654
        return setMarketParams.id();
×
655
    }
656

657
    /**
658
     * @dev Reads outstanding borrow token debt and collateral token balance from Morpho
659
     */
660
    function getCollateralAndBorrowBalances(
661
        ISetToken _setToken
662
    )
663
        external
664
        view 
665
        returns(uint256 collateralBalance, uint256 borrowBalance, uint256 borrowSharesU256)
666
    {
UNCOV
667
        MarketParams memory setMarketParams = marketParams[_setToken];
×
UNCOV
668
        require(setMarketParams.collateralToken != address(0), "Collateral not set");
×
UNCOV
669
        return _getCollateralAndBorrowBalances(_setToken, setMarketParams);
×
670
    }
671

672

673
    /* ============ Internal Functions ============ */
674

675
    /**
676
     * @dev Reads outstanding borrow token debt and collateral token balance from Morpho
677
     * and normalizes them  by the sets total supply to get the per token equivalent (position)
678
     */
679
    function _getCollateralAndBorrowPositions(
680
        ISetToken _setToken,
681
        MarketParams memory _marketParams,
682
        uint256 _setTotalSupply
683
    )
684
        internal
685
        view
686
        returns(int256 collateralPosition, int256 borrowPosition)
687
    {
688
        if (_setTotalSupply == 0) {
25!
689
            return (0, 0);
×
690
        }
691
        (uint256 collateralBalance, uint256 borrowBalance, ) = _getCollateralAndBorrowBalances(_setToken, _marketParams);
25✔
692
        collateralPosition = collateralBalance.preciseDiv(_setTotalSupply).toInt256();
693
        borrowPosition = borrowBalance.preciseDivCeil(_setTotalSupply).toInt256().mul(-1);
694
    }
695

696
    /**
697
     * @dev Reads outstanding borrow token debt and collateral token balance from Morpho
698
     */
699
    function _getCollateralAndBorrowBalances(
700
        ISetToken _setToken,
701
        MarketParams memory _marketParams
702
    )
703
        internal
704
        view 
705
        returns(uint256 collateralBalance, uint256 borrowBalance, uint256 borrowSharesU256)
706
    {
707
        bytes32 marketId = _marketParams.id();
30✔
708
        Position memory position = morpho.position(marketId, address(_setToken));
30✔
709
        (,, uint256 totalBorrowAssets, uint256 totalBorrowShares) = morpho.expectedMarketBalances(_marketParams);
30✔
710

711
        borrowBalance = position.borrowShares.toAssetsUp(totalBorrowAssets, totalBorrowShares);
712

713
        collateralBalance = uint256(position.collateral);
714
        borrowSharesU256 = uint256(position.borrowShares);
715
    }
716

717
    /**
718
     * @dev Updates external position unit for given borrow asset on SetToken
719
     */
720
    function _updateExternalPosition(
721
        ISetToken _setToken, 
722
        address _underlyingAsset,
723
        int256 _newPositionUnit
724
    )
725
        internal
726
    {
727
        _setToken.editExternalPosition(_underlyingAsset, address(this), _newPositionUnit, "");
35✔
728
    }
729

730

731
    /**
732
     * @dev Invoke deposit (as collateral) from SetToken using Morpho Blue
733
     */
734
    function _deposit(
735
        ISetToken _setToken,
736
        MarketParams memory _marketParams,
737
        uint256 _notionalQuantity
738
    )
739
        internal
740
    {
741
        _setToken.invokeApprove(_marketParams.collateralToken, address(morpho), _notionalQuantity);
16✔
742
        _setToken.invokeSupplyCollateral(
16✔
743
            morpho,
744
            _marketParams,
745
            _notionalQuantity
746
        );
747
    }
748

749
    /**
750
     * @dev Invoke withdraw from SetToken using Morpho Blue
751
     */
752
    function _withdraw(
753
        ISetToken _setToken,
754
        MarketParams memory _marketParams,
755
        uint256 _notionalQuantity
756
    )
757
        internal
758
    {
759
        _setToken.invokeWithdrawCollateral(
12✔
760
            morpho,
761
            _marketParams,
762
            _notionalQuantity
763
        );
764
    }
765

766
    /**
767
     * @dev Invoke repay from SetToken using Morpho Blue
768
     */
769
    function _repayBorrow(
770
        ISetToken _setToken,
771
        MarketParams memory _marketParams,
772
        uint256 _notionalQuantity,
773
        uint256 _shares
774
    )
775
        internal
776
    {
777
        // Only ever set shares or assets to avoid "inconsistent input" error when there is a rounding error
778
        if(_shares > 0) {
11✔
779
            _setToken.invokeApprove(_marketParams.loanToken, address(morpho), _notionalQuantity);
5✔
780
            _setToken.invokeRepay(
5✔
781
                morpho,
782
                _marketParams,
783
                0,
784
                _shares
785
            );
786
        } else {
787
            _setToken.invokeApprove(_marketParams.loanToken, address(morpho), _notionalQuantity);
6✔
788
            _setToken.invokeRepay(
6✔
789
                morpho,
790
                _marketParams,
791
                _notionalQuantity,
792
                0
793
            );
794
        }
795
    }
796

797

798
    /**
799
     * @dev Invoke borrow from the SetToken using Morpho 
800
     */
801
    function _borrow(
802
        ISetToken _setToken,
803
        MarketParams memory _marketParams,
804
        uint256 _notionalQuantity
805
    )
806
        internal
807
    {
808
        _setToken.invokeBorrow(
9✔
809
            morpho,
810
            _marketParams,
811
            _notionalQuantity
812
        );
813
    }
814

815
    /**
816
     * @dev Invokes approvals, gets trade call data from exchange adapter and invokes trade from SetToken
817
     * @return uint256     The quantity of tokens received post-trade
818
     */
819
    function _executeTrade(
820
        ActionInfo memory _actionInfo,
821
        IERC20 _sendToken,
822
        IERC20 _receiveToken,
823
        bytes memory _data
824
    )
825
        internal
826
        returns (uint256)
827
    {
828
        ISetToken setToken = _actionInfo.setToken;
10✔
829
        uint256 notionalSendQuantity = _actionInfo.notionalSendQuantity;
10✔
830

831
        setToken.invokeApprove(
10✔
832
            address(_sendToken),
833
            _actionInfo.exchangeAdapter.getSpender(),
834
            notionalSendQuantity
835
        );
836

837
        (
10✔
838
            address targetExchange,
839
            uint256 callValue,
840
            bytes memory methodData
841
        ) = _actionInfo.exchangeAdapter.getTradeCalldata(
842
            address(_sendToken),
843
            address(_receiveToken),
844
            address(setToken),
845
            notionalSendQuantity,
846
            _actionInfo.minNotionalReceiveQuantity,
847
            _data
848
        );
849

850
        setToken.invoke(targetExchange, callValue, methodData);
10✔
851

852
        uint256 receiveTokenQuantity = _receiveToken.balanceOf(address(setToken)).sub(_actionInfo.preTradeReceiveTokenBalance);
10✔
853
        require(
10!
854
            receiveTokenQuantity >= _actionInfo.minNotionalReceiveQuantity,
855
            "Slippage too high"
856
        );
857

858
        return receiveTokenQuantity;
10✔
859
    }
860

861
    /**
862
     * @dev Calculates protocol fee on module and pays protocol fee from SetToken
863
     * @return uint256          Total protocol fee paid
864
     */
865
    function _accrueProtocolFee(ISetToken _setToken, IERC20 _receiveToken, uint256 _exchangedQuantity) internal returns(uint256) {
866
        uint256 protocolFeeTotal = getModuleFee(PROTOCOL_TRADE_FEE_INDEX, _exchangedQuantity);
5✔
867

868
        payProtocolFeeFromSetToken(_setToken, address(_receiveToken), protocolFeeTotal);
5✔
869

870
        return protocolFeeTotal;
5✔
871
    }
872

873
    /**
874
     * @dev Construct the ActionInfo struct for lever and delever
875
     * @return ActionInfo       Instance of constructed ActionInfo struct
876
     */
877
    function _createAndValidateActionInfo(
878
        ISetToken _setToken,
879
        IERC20 _sendToken,
880
        IERC20 _receiveToken,
881
        uint256 _sendQuantityUnits,
882
        uint256 _minReceiveQuantityUnits,
883
        string memory _tradeAdapterName,
884
        bool _isLever
885
    )
886
        internal
887
        view
888
        returns(ActionInfo memory)
889
    {
890
        uint256 totalSupply = _setToken.totalSupply();
5✔
891

892
        return _createAndValidateActionInfoNotional(
5✔
893
            _setToken,
894
            _sendToken,
895
            _receiveToken,
896
            _sendQuantityUnits.preciseMul(totalSupply),
897
            _minReceiveQuantityUnits.preciseMul(totalSupply),
898
            _tradeAdapterName,
899
            _isLever,
900
            totalSupply
901
        );
902
    }
903

904
    /**
905
     * @dev Construct the ActionInfo struct for lever and delever accepting notional units
906
     * @return ActionInfo       Instance of constructed ActionInfo struct
907
     */
908
    function _createAndValidateActionInfoNotional(
909
        ISetToken _setToken,
910
        IERC20 _sendToken,
911
        IERC20 _receiveToken,
912
        uint256 _notionalSendQuantity,
913
        uint256 _minNotionalReceiveQuantity,
914
        string memory _tradeAdapterName,
915
        bool _isLever,
916
        uint256 _setTotalSupply
917
    )
918
        internal
919
        view
920
        returns(ActionInfo memory)
921
    {
922
        ActionInfo memory actionInfo = ActionInfo ({
10✔
923
            exchangeAdapter: IExchangeAdapter(getAndValidateAdapter(_tradeAdapterName)),
924
            setToken: _setToken,
925
            collateralAsset: _isLever ? _receiveToken : _sendToken,
926
            borrowAsset: _isLever ? _sendToken : _receiveToken,
927
            setTotalSupply: _setTotalSupply,
928
            notionalSendQuantity: _notionalSendQuantity,
929
            minNotionalReceiveQuantity: _minNotionalReceiveQuantity,
930
            preTradeReceiveTokenBalance: IERC20(_receiveToken).balanceOf(address(_setToken))
931
        });
932

933
        _validateCommon(actionInfo);
10✔
934

935
        return actionInfo;
10✔
936
    }
937

938
    /**
939
     * @dev Validates and sets given market params for given set token
940
     */
941
    function _setMarketParams(
942
        ISetToken _setToken,
943
        MarketParams memory _newMarketParams
944
    ) 
945
        internal
946
    {
947
        bytes32 marketId = _validateMarketParams(_newMarketParams);
4✔
948
        marketParams[_setToken] = _newMarketParams;
949
        emit MorphoMarketUpdated(
4✔
950
            _setToken,
951
            marketId
952
        );
953
    }
954

955
    /**
956
     * @dev Validates market params by checking that the resulting marketId exists on morpho
957
     */
958
    function _validateMarketParams(
959
        MarketParams memory _newMarketParams
960
    )
961
        internal
962
        view
963
        returns(bytes32 marketId)
964
    {
965
        marketId = _newMarketParams.id();
966
        Market memory market = morpho.market(marketId);
4✔
967
        require(market.lastUpdate != 0, "Market not created");
4!
968
    }
969

970
    /**
971
     * @dev Validate common requirements for lever and delever
972
     */
973
    function _validateCommon(ActionInfo memory _actionInfo) internal view {
974
        require(marketParams[_actionInfo.setToken].collateralToken != address(0), "Collateral not enabled");
10!
975
        require(marketParams[_actionInfo.setToken].loanToken != address(0), "Borrow not enabled");
10!
976
        require(_actionInfo.collateralAsset != _actionInfo.borrowAsset, "Collateral and borrow asset must be different");
10!
977
        require(_actionInfo.notionalSendQuantity > 0, "Quantity is 0");
10!
978
    }
979

980
    /**
981
     * @dev Updates the default (i.e. non-morpho) token balance of the repay / collateral token after delevering if necessary
982
     */
983
    function _updateRepayDefaultPosition(ActionInfo memory _actionInfo, IERC20 _repayAsset) internal {
984
        // if amount of tokens traded for exceeds debt, update default position first to save gas on editing borrow position
985
        uint256 repayAssetBalance = _repayAsset.balanceOf(address(_actionInfo.setToken));
7✔
986
        if (repayAssetBalance != _actionInfo.preTradeReceiveTokenBalance) {
7✔
987
            _actionInfo.setToken.calculateAndEditDefaultPosition(
5✔
988
                address(_repayAsset),
989
                _actionInfo.setTotalSupply,
990
                _actionInfo.preTradeReceiveTokenBalance
991
            );
992
        }
993
    }
994
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc