@@ -101,6 +101,9 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended {
101
101
/// @inheritdoc IWrappedMToken
102
102
uint128 public disableIndex;
103
103
104
+ /// @inheritdoc IWrappedMToken
105
+ int240 public roundingError;
106
+
104
107
mapping (address account = > address claimRecipient ) internal _claimRecipients;
105
108
106
109
/* ============ Constructor ============ */
@@ -168,11 +171,11 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended {
168
171
169
172
/// @inheritdoc IWrappedMToken
170
173
function claimExcess () external returns (uint240 claimed_ ) {
171
- int248 excess_ = excess ();
174
+ int240 excess_ = excess ();
172
175
173
176
if (excess_ <= 0 ) revert NoExcess ();
174
177
175
- emit ExcessClaimed (claimed_ = uint240 (uint248 ( excess_) ));
178
+ emit ExcessClaimed (claimed_ = uint240 (excess_));
176
179
177
180
// NOTE: The behavior of `IMTokenLike.transfer` is known, so its return can be ignored.
178
181
IMTokenLike (mToken).transfer (excessDestination, claimed_);
@@ -292,7 +295,12 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended {
292
295
function currentIndex () public view returns (uint128 index_ ) {
293
296
uint128 disableIndex_ = disableIndex == 0 ? IndexingMath.EXP_SCALED_ONE : disableIndex;
294
297
295
- return enableMIndex == 0 ? disableIndex_ : (disableIndex_ * _currentMIndex ()) / enableMIndex;
298
+ unchecked {
299
+ return
300
+ enableMIndex == 0
301
+ ? disableIndex_
302
+ : UIntMath.safe128 ((uint256 (disableIndex_) * _currentMIndex ()) / enableMIndex);
303
+ }
296
304
}
297
305
298
306
/// @inheritdoc IWrappedMToken
@@ -306,24 +314,20 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended {
306
314
}
307
315
308
316
/// @inheritdoc IWrappedMToken
309
- function excess () public view returns (int248 excess_ ) {
317
+ function excess () public view returns (int240 excess_ ) {
310
318
unchecked {
311
319
uint240 earmarked_ = totalNonEarningSupply + projectedEarningSupply ();
312
320
uint240 balance_ = _mBalanceOf (address (this ));
313
321
314
- // The entire M balance is excess if the total projected supply (factoring rounding errors) is 0.
315
- return
316
- earmarked_ == 0 ? int248 (uint248 (balance_)) : int248 (uint248 (balance_)) - int248 (uint248 (earmarked_));
322
+ // Decreases claimable excess by the `roundingError` for extra level of safety and solvency.
323
+ return int240 (balance_) - int240 (earmarked_) - roundingError;
317
324
}
318
325
}
319
326
320
327
/// @inheritdoc IWrappedMToken
321
328
function totalAccruedYield () external view returns (uint240 yield_ ) {
322
- uint240 projectedEarningSupply_ = projectedEarningSupply ();
323
- uint240 earningSupply_ = totalEarningSupply;
324
-
325
329
unchecked {
326
- return projectedEarningSupply_ <= earningSupply_ ? 0 : projectedEarningSupply_ - earningSupply_ ;
330
+ return projectedEarningSupply () - totalEarningSupply ;
327
331
}
328
332
}
329
333
@@ -414,15 +418,19 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended {
414
418
*/
415
419
function _addEarningAmount (address account_ , uint240 amount_ , uint128 currentIndex_ ) internal {
416
420
Account storage accountInfo_ = _accounts[account_];
417
- uint112 principal_ = IndexingMath.getPrincipalAmountRoundedDown (amount_, currentIndex_);
421
+
422
+ // NOTE: Tracks two principal amounts: rounded up and rounded down.
423
+ // Slightly overestimates the principal of total earning supply to provide extra safety in `excess` calculations.
424
+ uint112 principalUp_ = IndexingMath.getPrincipalAmountRoundedUp (amount_, currentIndex_);
425
+ uint112 principalDown_ = IndexingMath.getPrincipalAmountRoundedDown (amount_, currentIndex_);
418
426
419
427
// NOTE: Can be `unchecked` because the max amount of wrappable M is never greater than `type(uint240).max`.
420
428
unchecked {
421
429
accountInfo_.balance += amount_;
422
- accountInfo_.earningPrincipal = UIntMath.safe112 (uint256 (accountInfo_.earningPrincipal) + principal_ );
430
+ accountInfo_.earningPrincipal = UIntMath.safe112 (uint256 (accountInfo_.earningPrincipal) + principalDown_ );
423
431
}
424
432
425
- _addTotalEarningSupply (amount_, principal_ );
433
+ _addTotalEarningSupply (amount_, principalUp_ );
426
434
}
427
435
428
436
/**
@@ -439,18 +447,18 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended {
439
447
440
448
uint112 earningPrincipal_ = accountInfo_.earningPrincipal;
441
449
442
- // `min112` prevents `earningPrincipal` underflow.
443
- uint112 principal_ = UIntMath.min112 (
444
- IndexingMath.getPrincipalAmountRoundedUp (amount_, currentIndex_),
445
- earningPrincipal_
446
- );
450
+ // NOTE: Tracks two principal amounts: rounded up and rounded down.
451
+ // Slightly overestimates the principal of total earning supply to provide extra safety in `excess` calculations.
452
+ uint112 principalUp_ = IndexingMath.getPrincipalAmountRoundedUp (amount_, currentIndex_);
453
+ uint112 principalDown_ = IndexingMath.getPrincipalAmountRoundedDown (amount_, currentIndex_);
447
454
448
455
unchecked {
449
456
accountInfo_.balance = balance_ - amount_;
450
- accountInfo_.earningPrincipal = earningPrincipal_ - principal_;
457
+ // `min112` prevents `earningPrincipal` underflow.
458
+ accountInfo_.earningPrincipal = earningPrincipal_ - UIntMath.min112 (principalUp_, earningPrincipal_);
451
459
}
452
460
453
- _subtractTotalEarningSupply (amount_, principal_ );
461
+ _subtractTotalEarningSupply (amount_, principalDown_ );
454
462
}
455
463
456
464
/**
@@ -660,9 +668,24 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended {
660
668
* @param amount_ The amount of M deposited.
661
669
*/
662
670
function _wrap (address account_ , address recipient_ , uint240 amount_ ) internal {
671
+ uint240 startingBalance_ = _mBalanceOf (address (this ));
672
+
663
673
// NOTE: The behavior of `IMTokenLike.transferFrom` is known, so its return can be ignored.
664
674
IMTokenLike (mToken).transferFrom (account_, address (this ), amount_);
665
675
676
+ // NOTE: Computes the actual increase in the $M balance of the `WrappedM` contract and tracks potential $M rounding adjustments.
677
+ // Option 1: $M transfer from an $M earner to another $M earner (`WrappedM` in earning state) → rounds up → rounds up,
678
+ // 0, 1, or XX extra wei may be locked in `WrappedM` compared to the minted amount of Wrapped $M.
679
+ // Result: `roundingError` remains the same or decreases.
680
+ //
681
+ // Option 2: $M transfer from an $M non-earner to an $M earner (`WrappedM` in earning state) → precise $M transfer → rounds down,
682
+ // 0, -1, or -XX wei may be deducted from $M locked in `WrappedM` compared to the minted amount of Wrapped $M.
683
+ // Result: `roundingError` remains the same or increases.
684
+ uint240 endingBalance_ = _mBalanceOf (address (this ));
685
+ uint240 mIncrease_ = endingBalance_ - startingBalance_;
686
+ roundingError += int240 (amount_) - int240 (mIncrease_);
687
+
688
+ // Mints precise amount of Wrapped $M to `recipient_`.
666
689
_mint (recipient_, amount_);
667
690
}
668
691
@@ -675,8 +698,19 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended {
675
698
function _unwrap (address account_ , address recipient_ , uint240 amount_ ) internal {
676
699
_burn (account_, amount_);
677
700
701
+ uint240 startingBalance_ = _mBalanceOf (address (this ));
702
+
678
703
// NOTE: The behavior of `IMTokenLike.transfer` is known, so its return can be ignored.
679
704
IMTokenLike (mToken).transfer (recipient_, amount_);
705
+
706
+ // NOTE: Computes the actual decrease in the $M balance of the `WrappedM` contract.
707
+ // Option 1: $M transfer from an $M earner (`WrappedM` in earning state) to another $M earner → rounds up.
708
+ // Option 2: $M transfer from an $M earner (`WrappedM` in earning state) to an $M non-earner → precise $M transfer.
709
+ // In both cases, 0, 1, or XX extra wei may be deducted from the `WrappedM` contract's $M balance compared to the burned amount of Wrapped $M.
710
+ // Result: `roundingError` remains the same or increases.
711
+ uint240 endingBalance_ = _mBalanceOf (address (this ));
712
+ uint240 mDecrease_ = startingBalance_ - endingBalance_;
713
+ roundingError += int240 (mDecrease_) - int240 (amount_);
680
714
}
681
715
682
716
/**
@@ -694,14 +728,17 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended {
694
728
if (accountInfo_.isEarning) return ;
695
729
696
730
uint240 balance_ = accountInfo_.balance;
697
- uint112 earningPrincipal_ = IndexingMath.getPrincipalAmountRoundedDown (balance_, currentIndex_);
731
+
732
+ // NOTE: Tracks two principal amounts: rounded up and rounded down.
733
+ // Slightly overestimates the principal of total earning supply to provide extra safety in `excess` calculations.
734
+ uint112 principalUp_ = IndexingMath.getPrincipalAmountRoundedUp (balance_, currentIndex_);
735
+ uint112 principalDown_ = IndexingMath.getPrincipalAmountRoundedDown (balance_, currentIndex_);
698
736
699
737
accountInfo_.isEarning = true ;
700
- accountInfo_.earningPrincipal = earningPrincipal_ ;
738
+ accountInfo_.earningPrincipal = principalDown_ ;
701
739
accountInfo_.hasEarnerDetails = admin_ != address (0 ); // Has earner details if an admin exists for this account.
702
740
703
- _addTotalEarningSupply (balance_, earningPrincipal_);
704
-
741
+ _addTotalEarningSupply (balance_, principalUp_);
705
742
unchecked {
706
743
totalNonEarningSupply -= balance_;
707
744
}
0 commit comments