1111
1212pragma solidity ^ 0.8.16 ;
1313
14+ import {Math} from "@openzeppelin/contracts/utils/math/Math.sol " ;
1415import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol " ;
1516import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol " ;
1617import {Upgradeable} from "./proxy/Upgradeable.sol " ;
@@ -52,8 +53,8 @@ contract StructuredPortfolio is IStructuredPortfolio, LoansManager, Upgradeable
5253 IProtocolConfig _protocolConfig ,
5354 PortfolioParams memory portfolioParams ,
5455 TrancheInitData[] memory tranchesInitData ,
55- ExpectedEquityRate calldata _expectedEquityRate
56- ) external initializer {
56+ ExpectedEquityRate memory _expectedEquityRate
57+ ) public initializer {
5758 _initialize (_fixedInterestOnlyLoans, underlyingToken);
5859 __Upgradeable_init (_protocolConfig.protocolAdmin (), _protocolConfig.pauserAddress ());
5960 _grantRole (MANAGER_ROLE, manager);
@@ -101,48 +102,37 @@ contract StructuredPortfolio is IStructuredPortfolio, LoansManager, Upgradeable
101102 }
102103
103104 function updateCheckpoints () public whenNotPaused {
104- require (status == Status.Live, "SP: Portfolio is not live " );
105-
105+ require (status != Status.CapitalFormation, "SP: No checkpoints before start " );
106+ (uint256 [] memory _totalAssetsAfter , uint256 [] memory pendingFees ) = _calculateWaterfall (virtualTokenBalance + loansValue ());
107+ LoansDeficitCheckpoint[] memory deficits = _calculateLoansDeficit (_totalAssetsAfter, pendingFees);
108+ for (uint256 i = 0 ; i < _totalAssetsAfter.length ; i++ ) {
109+ tranches[i].updateCheckpointFromPortfolio (_totalAssetsAfter[i], deficits[i].deficit);
110+ }
106111 if (someLoansDefaulted) {
107- _updateCheckpointsAndLoansDeficit ();
108- } else {
109- _updateCheckpoints ();
112+ for ( uint256 i = 1 ; i < deficits. length ; i ++ ) {
113+ tranchesData[i].loansDeficitCheckpoint = deficits[i];
114+ }
110115 }
111116 }
112117
113- function _updateCheckpointsAndLoansDeficit () internal {
114- uint256 [] memory _totalAssetsBefore = new uint256 [](tranches.length );
115- for (uint256 i = 0 ; i < tranches.length ; i++ ) {
116- _totalAssetsBefore[i] = tranches[i].getCheckpoint ().totalAssets;
118+ function _calculateLoansDeficit (uint256 [] memory realTotalAssets , uint256 [] memory pendingFees )
119+ internal
120+ view
121+ returns (LoansDeficitCheckpoint[] memory )
122+ {
123+ LoansDeficitCheckpoint[] memory deficits = new LoansDeficitCheckpoint [](realTotalAssets.length );
124+ if (! someLoansDefaulted) {
125+ return deficits;
117126 }
118-
119- uint256 [] memory _totalAssetsAfter = _updateCheckpoints ();
120-
121127 uint256 timestamp = _limitedBlockTimestamp ();
122- for (uint256 i = 1 ; i < _totalAssetsAfter.length ; i++ ) {
123- uint256 currentDeficit = _defaultedLoansDeficit (tranchesData[i], timestamp);
124-
125- int256 deficitDelta = SafeCast.toInt256 (_totalAssetsBefore[i]) - SafeCast.toInt256 (_totalAssetsAfter[i]);
126- uint256 newDeficit = _getNewLoansDeficit (currentDeficit, deficitDelta);
127-
128- tranchesData[i].loansDeficitCheckpoint = LoansDeficitCheckpoint ({deficit: newDeficit, timestamp: timestamp});
129- }
130- }
131-
132- function _updateCheckpoints () internal returns (uint256 [] memory ) {
133- uint256 [] memory _totalAssetsAfter = calculateWaterfall ();
134- for (uint256 i = 0 ; i < _totalAssetsAfter.length ; i++ ) {
135- tranches[i].updateCheckpointFromPortfolio (_totalAssetsAfter[i]);
136- }
137- return _totalAssetsAfter;
138- }
139-
140- function _getNewLoansDeficit (uint256 currentDeficit , int256 delta ) internal pure returns (uint256 ) {
141- require (delta > type (int256 ).min, "SP: Delta out of range " );
142- if (delta < 0 && currentDeficit < uint256 (- delta)) {
143- return 0 ;
128+ for (uint256 i = 1 ; i < realTotalAssets.length ; i++ ) {
129+ Checkpoint memory checkpoint = tranches[i].getCheckpoint ();
130+ uint256 assumedTotalAssets = _assumedTrancheValue (i, timestamp);
131+ uint256 assumedTotalAssetsAfterFees = _saturatingSub (assumedTotalAssets, Math.max (pendingFees[i], checkpoint.unpaidFees));
132+ uint256 newDeficit = _saturatingSub (assumedTotalAssetsAfterFees, realTotalAssets[i]);
133+ deficits[i] = LoansDeficitCheckpoint ({deficit: newDeficit, timestamp: timestamp});
144134 }
145- return _addSigned (currentDeficit, delta) ;
135+ return deficits ;
146136 }
147137
148138 function increaseVirtualTokenBalance (uint256 increment ) external {
@@ -157,16 +147,18 @@ contract StructuredPortfolio is IStructuredPortfolio, LoansManager, Upgradeable
157147 uint256 tranchesCount = tranches.length ;
158148 for (uint256 i = 0 ; i < tranchesCount; i++ ) {
159149 if (msg .sender == address (tranches[i])) {
160- virtualTokenBalance = _addSigned ( virtualTokenBalance, delta);
150+ virtualTokenBalance = delta < 0 ? virtualTokenBalance - uint256 ( - delta) : virtualTokenBalance + uint256 ( delta);
161151 return ;
162152 }
163153 }
164154 revert ("SP: Not a tranche " );
165155 }
166156
167- function totalAssets () public view returns (uint256 ) {
157+ function totalAssets () external view returns (uint256 ) {
168158 if (status == Status.Live) {
169- return liquidAssets () + loansValue ();
159+ uint256 _totalPendingFees = totalPendingFees ();
160+ uint256 totalAssetsBeforeFees = virtualTokenBalance + loansValue ();
161+ return _saturatingSub (totalAssetsBeforeFees, _totalPendingFees);
170162 }
171163 return _sum (_tranchesTotalAssets ());
172164 }
@@ -244,6 +236,53 @@ contract StructuredPortfolio is IStructuredPortfolio, LoansManager, Upgradeable
244236 _checkTranchesRatios (_tranchesTotalAssets ());
245237 }
246238
239+ function maxTrancheValueComplyingWithRatio (uint256 trancheIdx ) external view returns (uint256 ) {
240+ if (status != Status.Live || trancheIdx == 0 ) {
241+ return type (uint256 ).max;
242+ }
243+
244+ uint256 [] memory waterfallValues = calculateWaterfall ();
245+
246+ uint256 subordinateValue = 0 ;
247+ for (uint256 i = 0 ; i < trancheIdx; i++ ) {
248+ subordinateValue += waterfallValues[i];
249+ }
250+
251+ uint256 minSubordinateRatio = tranchesData[trancheIdx].minSubordinateRatio;
252+ if (minSubordinateRatio == 0 ) {
253+ return type (uint256 ).max;
254+ }
255+
256+ return (subordinateValue * BASIS_PRECISION) / minSubordinateRatio;
257+ }
258+
259+ function minTrancheValueComplyingWithRatio (uint256 trancheIdx ) external view returns (uint256 ) {
260+ if (status != Status.Live) {
261+ return 0 ;
262+ }
263+
264+ uint256 [] memory trancheValues = calculateWaterfall ();
265+ uint256 tranchesCount = trancheValues.length ;
266+ if (trancheIdx == tranchesCount - 1 ) {
267+ return 0 ;
268+ }
269+
270+ uint256 subordinateValueWithoutTranche = 0 ;
271+ uint256 maxThreshold = 0 ;
272+ for (uint256 i = 0 ; i < tranchesCount - 1 ; i++ ) {
273+ uint256 trancheValue = trancheValues[i];
274+ if (i != trancheIdx) {
275+ subordinateValueWithoutTranche += trancheValue;
276+ }
277+ if (i >= trancheIdx) {
278+ uint256 lowerBound = (trancheValues[i + 1 ] * tranchesData[i + 1 ].minSubordinateRatio) / BASIS_PRECISION;
279+ uint256 minTrancheValue = _saturatingSub (lowerBound, subordinateValueWithoutTranche);
280+ maxThreshold = Math.max (minTrancheValue, maxThreshold);
281+ }
282+ }
283+ return maxThreshold;
284+ }
285+
247286 function _tranchesTotalAssets () internal view returns (uint256 [] memory ) {
248287 if (status == Status.Live) {
249288 return calculateWaterfall ();
@@ -290,36 +329,24 @@ contract StructuredPortfolio is IStructuredPortfolio, LoansManager, Upgradeable
290329 }
291330
292331 _changePortfolioStatus (Status.Closed);
332+ updateCheckpoints ();
293333
294334 if (! isAfterEndDate) {
295335 endDate = block .timestamp ;
296336 }
297337 }
298338
299339 function _closeTranches () internal {
340+ updateCheckpoints ();
300341 uint256 limitedBlockTimestamp = _limitedBlockTimestamp ();
342+ (uint256 [] memory waterfall , ) = _calculateWaterfall (virtualTokenBalance);
301343
302- uint256 [] memory waterfallTranchesAssets = calculateWaterfall ();
303- uint256 [] memory distributedAssets = new uint256 [](waterfallTranchesAssets.length );
304- uint256 assetsLeft = virtualTokenBalance;
305-
306- for (uint256 i = waterfallTranchesAssets.length - 1 ; i > 0 ; i-- ) {
307- tranchesData[i].maxValueOnClose = _assumedTrancheValue (i, limitedBlockTimestamp);
308-
309- uint256 trancheDistributedAssets = _min (waterfallTranchesAssets[i], assetsLeft);
310- distributedAssets[i] = trancheDistributedAssets;
311- tranches[i].updateCheckpointFromPortfolio (trancheDistributedAssets);
312-
313- assetsLeft -= trancheDistributedAssets;
314- }
315-
316- distributedAssets[0 ] = _min (waterfallTranchesAssets[0 ], assetsLeft);
317- tranches[0 ].updateCheckpointFromPortfolio (distributedAssets[0 ]);
318-
319- for (uint256 i = 0 ; i < distributedAssets.length ; i++ ) {
320- uint256 trancheDistributedAssets = distributedAssets[i];
321- tranchesData[i].distributedAssets = trancheDistributedAssets;
322- _transfer (tranches[i], trancheDistributedAssets);
344+ for (uint256 i = 0 ; i < waterfall.length ; i++ ) {
345+ if (i != 0 ) {
346+ tranchesData[i].maxValueOnClose = _assumedTrancheValue (i, limitedBlockTimestamp);
347+ }
348+ tranchesData[i].distributedAssets = waterfall[i];
349+ _transfer (tranches[i], waterfall[i]);
323350 }
324351 }
325352
@@ -340,21 +367,34 @@ contract StructuredPortfolio is IStructuredPortfolio, LoansManager, Upgradeable
340367 }
341368
342369 function calculateWaterfall () public view returns (uint256 [] memory ) {
343- uint256 [] memory waterfall = calculateWaterfallWithoutFees ();
370+ (uint256 [] memory waterfall , ) = _calculateWaterfall (virtualTokenBalance + loansValue ());
371+ return waterfall;
372+ }
373+
374+ function _calculateWaterfall (uint256 assetsLeft ) internal view returns (uint256 [] memory , uint256 [] memory ) {
375+ uint256 [] memory waterfall = _calculateWaterfallWithoutFees (assetsLeft);
376+ uint256 [] memory fees = new uint256 [](tranches.length );
344377 for (uint256 i = 0 ; i < waterfall.length ; i++ ) {
345378 uint256 pendingFees = tranches[i].totalPendingFeesForAssets (waterfall[i]);
346379 waterfall[i] = _saturatingSub (waterfall[i], pendingFees);
380+ fees[i] = pendingFees;
347381 }
348- return waterfall;
382+ return ( waterfall, fees) ;
349383 }
350384
351385 function calculateWaterfallWithoutFees () public view returns (uint256 [] memory ) {
386+ return _calculateWaterfallWithoutFees (virtualTokenBalance + loansValue ());
387+ }
388+
389+ function _calculateWaterfallWithoutFees (uint256 assetsLeft ) internal view returns (uint256 [] memory ) {
352390 uint256 [] memory waterfall = new uint256 [](tranches.length );
353391 if (status != Status.Live) {
392+ for (uint256 i = 0 ; i < waterfall.length ; i++ ) {
393+ waterfall[i] = tranches[i].totalAssetsBeforeFees ();
394+ }
354395 return waterfall;
355396 }
356397
357- uint256 assetsLeft = virtualTokenBalance + loansValue ();
358398 uint256 limitedBlockTimestamp = _limitedBlockTimestamp ();
359399
360400 for (uint256 i = waterfall.length - 1 ; i > 0 ; i-- ) {
@@ -377,28 +417,20 @@ contract StructuredPortfolio is IStructuredPortfolio, LoansManager, Upgradeable
377417 function _assumedTrancheValue (uint256 trancheIdx , uint256 timestamp ) internal view returns (uint256 ) {
378418 Checkpoint memory checkpoint = tranches[trancheIdx].getCheckpoint ();
379419 TrancheData memory trancheData = tranchesData[trancheIdx];
420+ uint256 targetApy = trancheData.targetApy;
380421
381- uint256 assumedTotalAssets = _assumedTotalAssets (checkpoint, trancheData, timestamp);
382- uint256 defaultedLoansDeficit = _defaultedLoansDeficit (trancheData, timestamp);
422+ uint256 timePassedSinceCheckpoint = _saturatingSub (timestamp, checkpoint.timestamp);
423+ uint256 assumedTotalAssets = _withInterest (checkpoint.totalAssets, targetApy, timePassedSinceCheckpoint) +
424+ checkpoint.unpaidFees;
383425
384- return assumedTotalAssets + defaultedLoansDeficit;
385- }
386-
387- function _assumedTotalAssets (
388- Checkpoint memory checkpoint ,
389- TrancheData memory trancheData ,
390- uint256 timestamp
391- ) internal pure returns (uint256 ) {
392- uint256 timePassed = _saturatingSub (timestamp, checkpoint.timestamp);
393- return _withInterest (checkpoint.totalAssets, trancheData.targetApy, timePassed);
394- }
395-
396- function _defaultedLoansDeficit (TrancheData memory trancheData , uint256 timestamp ) internal pure returns (uint256 ) {
397- if (trancheData.loansDeficitCheckpoint.deficit == 0 ) {
398- return 0 ;
426+ uint256 defaultedLoansDeficit;
427+ uint256 checkpointDeficit = trancheData.loansDeficitCheckpoint.deficit;
428+ if (checkpointDeficit != 0 ) {
429+ uint256 timePassed = _saturatingSub (timestamp, trancheData.loansDeficitCheckpoint.timestamp);
430+ defaultedLoansDeficit = _withInterest (checkpointDeficit, targetApy, timePassed);
399431 }
400- uint256 timePassed = _saturatingSub (timestamp, trancheData.loansDeficitCheckpoint.timestamp);
401- return _withInterest (trancheData.loansDeficitCheckpoint.deficit, trancheData.targetApy, timePassed) ;
432+
433+ return assumedTotalAssets + defaultedLoansDeficit ;
402434 }
403435
404436 function _withInterest (
@@ -456,7 +488,7 @@ contract StructuredPortfolio is IStructuredPortfolio, LoansManager, Upgradeable
456488 continue ;
457489 }
458490
459- uint256 trancheShare = _min (trancheFreeCapacity, undistributedAssets);
491+ uint256 trancheShare = Math. min (trancheFreeCapacity, undistributedAssets);
460492 undistributedAssets -= trancheShare;
461493 _repayInClosed (i, trancheShare);
462494 }
@@ -504,18 +536,10 @@ contract StructuredPortfolio is IStructuredPortfolio, LoansManager, Upgradeable
504536 emit PortfolioStatusChanged (newStatus);
505537 }
506538
507- function _min (uint256 x , uint256 y ) internal pure returns (uint256 ) {
508- return x > y ? y : x;
509- }
510-
511539 function _saturatingSub (uint256 x , uint256 y ) internal pure returns (uint256 ) {
512540 return x > y ? x - y : 0 ;
513541 }
514542
515- function _addSigned (uint256 x , int256 y ) internal pure returns (uint256 ) {
516- return y < 0 ? x - uint256 (- y) : x + uint256 (y);
517- }
518-
519543 function _sum (uint256 [] memory components ) internal pure returns (uint256 ) {
520544 uint256 sum;
521545 for (uint256 i = 0 ; i < components.length ; i++ ) {
@@ -525,7 +549,7 @@ contract StructuredPortfolio is IStructuredPortfolio, LoansManager, Upgradeable
525549 }
526550
527551 function _limitedBlockTimestamp () internal view returns (uint256 ) {
528- return _min (block .timestamp , endDate);
552+ return Math. min (block .timestamp , endDate);
529553 }
530554
531555 function _requireManagerRole () internal view {
0 commit comments