Skip to content

Commit fdf3628

Browse files
authored
Merge 0a2ed8c into b742ab8
2 parents b742ab8 + 0a2ed8c commit fdf3628

File tree

6 files changed

+247
-101
lines changed

6 files changed

+247
-101
lines changed

src/WrappedMToken.sol

Lines changed: 62 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,9 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended {
101101
/// @inheritdoc IWrappedMToken
102102
uint128 public disableIndex;
103103

104+
/// @inheritdoc IWrappedMToken
105+
int240 public roundingError;
106+
104107
mapping(address account => address claimRecipient) internal _claimRecipients;
105108

106109
/* ============ Constructor ============ */
@@ -168,11 +171,11 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended {
168171

169172
/// @inheritdoc IWrappedMToken
170173
function claimExcess() external returns (uint240 claimed_) {
171-
int248 excess_ = excess();
174+
int240 excess_ = excess();
172175

173176
if (excess_ <= 0) revert NoExcess();
174177

175-
emit ExcessClaimed(claimed_ = uint240(uint248(excess_)));
178+
emit ExcessClaimed(claimed_ = uint240(excess_));
176179

177180
// NOTE: The behavior of `IMTokenLike.transfer` is known, so its return can be ignored.
178181
IMTokenLike(mToken).transfer(excessDestination, claimed_);
@@ -292,7 +295,12 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended {
292295
function currentIndex() public view returns (uint128 index_) {
293296
uint128 disableIndex_ = disableIndex == 0 ? IndexingMath.EXP_SCALED_ONE : disableIndex;
294297

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+
}
296304
}
297305

298306
/// @inheritdoc IWrappedMToken
@@ -306,24 +314,20 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended {
306314
}
307315

308316
/// @inheritdoc IWrappedMToken
309-
function excess() public view returns (int248 excess_) {
317+
function excess() public view returns (int240 excess_) {
310318
unchecked {
311319
uint240 earmarked_ = totalNonEarningSupply + projectedEarningSupply();
312320
uint240 balance_ = _mBalanceOf(address(this));
313321

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;
317324
}
318325
}
319326

320327
/// @inheritdoc IWrappedMToken
321328
function totalAccruedYield() external view returns (uint240 yield_) {
322-
uint240 projectedEarningSupply_ = projectedEarningSupply();
323-
uint240 earningSupply_ = totalEarningSupply;
324-
325329
unchecked {
326-
return projectedEarningSupply_ <= earningSupply_ ? 0 : projectedEarningSupply_ - earningSupply_;
330+
return projectedEarningSupply() - totalEarningSupply;
327331
}
328332
}
329333

@@ -414,15 +418,19 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended {
414418
*/
415419
function _addEarningAmount(address account_, uint240 amount_, uint128 currentIndex_) internal {
416420
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_);
418426

419427
// NOTE: Can be `unchecked` because the max amount of wrappable M is never greater than `type(uint240).max`.
420428
unchecked {
421429
accountInfo_.balance += amount_;
422-
accountInfo_.earningPrincipal = UIntMath.safe112(uint256(accountInfo_.earningPrincipal) + principal_);
430+
accountInfo_.earningPrincipal = UIntMath.safe112(uint256(accountInfo_.earningPrincipal) + principalDown_);
423431
}
424432

425-
_addTotalEarningSupply(amount_, principal_);
433+
_addTotalEarningSupply(amount_, principalUp_);
426434
}
427435

428436
/**
@@ -439,18 +447,18 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended {
439447

440448
uint112 earningPrincipal_ = accountInfo_.earningPrincipal;
441449

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_);
447454

448455
unchecked {
449456
accountInfo_.balance = balance_ - amount_;
450-
accountInfo_.earningPrincipal = earningPrincipal_ - principal_;
457+
// `min112` prevents `earningPrincipal` underflow.
458+
accountInfo_.earningPrincipal = earningPrincipal_ - UIntMath.min112(principalUp_, earningPrincipal_);
451459
}
452460

453-
_subtractTotalEarningSupply(amount_, principal_);
461+
_subtractTotalEarningSupply(amount_, principalDown_);
454462
}
455463

456464
/**
@@ -660,9 +668,24 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended {
660668
* @param amount_ The amount of M deposited.
661669
*/
662670
function _wrap(address account_, address recipient_, uint240 amount_) internal {
671+
uint240 startingBalance_ = _mBalanceOf(address(this));
672+
663673
// NOTE: The behavior of `IMTokenLike.transferFrom` is known, so its return can be ignored.
664674
IMTokenLike(mToken).transferFrom(account_, address(this), amount_);
665675

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_`.
666689
_mint(recipient_, amount_);
667690
}
668691

@@ -675,8 +698,19 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended {
675698
function _unwrap(address account_, address recipient_, uint240 amount_) internal {
676699
_burn(account_, amount_);
677700

701+
uint240 startingBalance_ = _mBalanceOf(address(this));
702+
678703
// NOTE: The behavior of `IMTokenLike.transfer` is known, so its return can be ignored.
679704
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_);
680714
}
681715

682716
/**
@@ -694,14 +728,17 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended {
694728
if (accountInfo_.isEarning) return;
695729

696730
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_);
698736

699737
accountInfo_.isEarning = true;
700-
accountInfo_.earningPrincipal = earningPrincipal_;
738+
accountInfo_.earningPrincipal = principalDown_;
701739
accountInfo_.hasEarnerDetails = admin_ != address(0); // Has earner details if an admin exists for this account.
702740

703-
_addTotalEarningSupply(balance_, earningPrincipal_);
704-
741+
_addTotalEarningSupply(balance_, principalUp_);
705742
unchecked {
706743
totalNonEarningSupply -= balance_;
707744
}

src/interfaces/IWrappedMToken.sol

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,7 +258,7 @@ interface IWrappedMToken is IMigratable, IERC20Extended {
258258
function enableMIndex() external view returns (uint128 enableMIndex);
259259

260260
/// @notice This contract's current excess M that is not earmarked for account balances or accrued yield.
261-
function excess() external view returns (int248 excess);
261+
function excess() external view returns (int240 excess);
262262

263263
/// @notice The wrapper's index when earning was most recently disabled.
264264
function disableIndex() external view returns (uint128 disableIndex);
@@ -302,4 +302,7 @@ interface IWrappedMToken is IMigratable, IERC20Extended {
302302

303303
/// @notice The address of the destination where excess is claimed to.
304304
function excessDestination() external view returns (address excessDestination);
305+
306+
/// @notice The rounding error that may occur due to imprecise $M transfers in and out of WrappedM contract.
307+
function roundingError() external view returns (int240 roundingError);
305308
}

0 commit comments

Comments
 (0)