Skip to content

Commit

Permalink
feat: wrap/unwrap amount fix
Browse files Browse the repository at this point in the history
  • Loading branch information
deluca-mike committed Jan 21, 2025
1 parent 2db6f81 commit 9176f58
Show file tree
Hide file tree
Showing 7 changed files with 139 additions and 69 deletions.
2 changes: 1 addition & 1 deletion lib/common
61 changes: 45 additions & 16 deletions src/WrappedMToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -542,16 +542,13 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended {
* @return wrapped_ The amount of wM minted.
*/
function _wrap(address account_, address recipient_, uint240 amount_) internal returns (uint240 wrapped_) {
uint240 startingBalance_ = _mBalanceOf(address(this));

// NOTE: The behavior of `IMTokenLike.transferFrom` is known, so its return can be ignored.
IMTokenLike(mToken).transferFrom(account_, address(this), amount_);

// NOTE: When this WrappedMToken contract is earning, any amount of M sent to it is converted to a principal
// amount at the MToken contract, which when represented as a present amount, may be a rounding error
// amount less than `amount_`. In order to capture the real increase in M, the difference between the
// starting and ending M balance is minted as WrappedM token.
_mint(recipient_, wrapped_ = _mBalanceOf(address(this)) - startingBalance_);
// amount less than `amount_`. This will reduce excess by the rounding error.
_mint(recipient_, wrapped_ = amount_);
}

/**
Expand All @@ -562,18 +559,13 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended {
* @return unwrapped_ The amount of M withdrawn.
*/
function _unwrap(address account_, address recipient_, uint240 amount_) internal returns (uint240 unwrapped_) {
_burn(account_, amount_);

uint240 startingBalance_ = _mBalanceOf(address(this));

// NOTE: The behavior of `IMTokenLike.transfer` is known, so its return can be ignored.
IMTokenLike(mToken).transfer(recipient_, _getSafeTransferableM(amount_, currentIndex()));

// NOTE: When this WrappedMToken contract is earning, any amount of M sent from it is converted to a principal
// amount at the MToken contract, which when represented as a present amount, may be a rounding error
// amount more than `amount_`. In order to capture the real decrease in M, the difference between the
// ending and starting M balance is returned.
return startingBalance_ - _mBalanceOf(address(this));
// amount more than `amount_`. The real decrease in M may be larger than `amount_`.
_burn(account_, unwrapped_ = amount_);

// NOTE: The behavior of `IMTokenLike.transfer` is known, so its return can be ignored.
IMTokenLike(mToken).transfer(recipient_, _getSufficientTransferableM(recipient_, amount_));
}

/**
Expand Down Expand Up @@ -673,6 +665,15 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended {
return IRegistrarLike(registrar).get(key_);
}

/**
* @dev Returns whether `account_` is earning M Token.
* @param account_ The account being queried.
* @return isEarning_ Whether the account is earning M Token.
*/
function _isEarningM(address account_) internal view returns (bool isEarning_) {
return IMTokenLike(mToken).isEarning(account_);
}

/**
* @dev Compute the adjusted amount of M that can safely be transferred out given the current index.
* @param amount_ Some amount to be transferred out of this contract.
Expand All @@ -682,14 +683,42 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended {
function _getSafeTransferableM(uint240 amount_, uint128 currentIndex_) internal view returns (uint240 safeAmount_) {
// If this contract is earning, adjust `amount_` to ensure it's M balance decrement is limited to `amount_`.
return
IMTokenLike(mToken).isEarning(address(this))
_isEarningM(address(this))
? IndexingMath.getPresentAmountRoundedDown(
IndexingMath.getPrincipalAmountRoundedDown(amount_, currentIndex_),
currentIndex_
)
: amount_;
}

/**
* @dev Compute the adjusted amount of M that must be transferred so the recipient receives at least that amount.
* @param recipient_ The address of some recipient.
* @param amount_ Some amount to be transferred out of the wrapper.
* @return sufficientAmount_ The adjusted amount that must be transferred.
*/
function _getSufficientTransferableM(
address recipient_,
uint240 amount_
) internal view returns (uint240 sufficientAmount_) {
// If the recipient is not earning or if the wrapper is earning, not need to adjust `amount_`.
// See: https://github.com/m0-foundation/protocol/blob/main/src/MToken.sol#L385
if (!_isEarningM(recipient_) || _isEarningM(address(this))) return amount_;

uint128 currentMIndex_ = _currentMIndex();
uint112 principal = uint112(IMTokenLike(mToken).principalBalanceOf(recipient_));
uint240 balance = IndexingMath.getPresentAmountRoundedDown(principal, currentMIndex_);

// Adjust `amount_` to ensure the recipient's M balance increments by at least `amount_`.
unchecked {
return
IndexingMath.getPresentAmountRoundedUp(
IndexingMath.getPrincipalAmountRoundedUp(balance + amount_, currentMIndex_) - principal,
currentMIndex_
);
}
}

/// @dev Returns the address of the contract to use as a migrator, if any.
function _getMigrator() internal view override returns (address migrator_) {
return
Expand Down
7 changes: 7 additions & 0 deletions src/interfaces/IMTokenLike.sol
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,11 @@ interface IMTokenLike {

/// @notice The current index that would be written to storage if `updateIndex` is called.
function currentIndex() external view returns (uint128 currentIndex);

/**
* @notice The principal of an earner M token balance.
* @param account The account to get the principal balance of.
* @return principal The principal balance of the account.
*/
function principalBalanceOf(address account) external view returns (uint240 principal);
}
92 changes: 46 additions & 46 deletions test/integration/Protocol.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -75,14 +75,14 @@ contract ProtocolIntegrationTests is TestBase {
assertEq(_mToken.totalEarningSupply(), _totalEarningSupplyOfM += 99_999999);

// Assert Alice (Earner)
assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalance = 99_999999);
assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalance = 100_000000);
assertEq(_wrappedMToken.accruedYieldOf(_alice), 0);

// Assert Globals
assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += 99_999999);
assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += 100_000000);
assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply);
assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield);
assertEq(_wrappedMToken.excess(), _excess);
assertEq(_wrappedMToken.excess(), _excess -= 1);

assertGe(_wrapperBalanceOfM, _totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield + _excess);

Expand Down Expand Up @@ -142,14 +142,14 @@ contract ProtocolIntegrationTests is TestBase {
assertEq(_mToken.totalEarningSupply(), _totalEarningSupplyOfM += 199_999999);

// Assert Bob (Earner)
assertEq(_wrappedMToken.balanceOf(_bob), _bobBalance = 199_999999);
assertEq(_wrappedMToken.balanceOf(_bob), _bobBalance = 200_000000);
assertEq(_wrappedMToken.accruedYieldOf(_bob), 0);

// Assert Globals
assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += 199_999999);
assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += 200_000000);
assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply);
assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield);
assertEq(_wrappedMToken.excess(), _excess);
assertEq(_wrappedMToken.excess(), _excess -= 1);

assertGe(_wrapperBalanceOfM, _totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield + _excess);

Expand All @@ -164,14 +164,14 @@ contract ProtocolIntegrationTests is TestBase {
assertEq(_mToken.totalEarningSupply(), _totalEarningSupplyOfM += 149_999999);

// Assert Dave (Non-Earner)
assertEq(_wrappedMToken.balanceOf(_dave), _daveBalance = 149_999999);
assertEq(_wrappedMToken.balanceOf(_dave), _daveBalance = 150_000000);
assertEq(_wrappedMToken.accruedYieldOf(_dave), 0);

// Assert Globals
assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply);
assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply += 149_999999);
assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply += 150_000000);
assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield);
assertEq(_wrappedMToken.excess(), _excess);
assertEq(_wrappedMToken.excess(), _excess -= 1);

assertGe(_wrapperBalanceOfM, _totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield + _excess);

Expand Down Expand Up @@ -233,7 +233,7 @@ contract ProtocolIntegrationTests is TestBase {
assertEq(_mToken.balanceOf(address(_wrappedMToken)), _wrapperBalanceOfM += 99_999999);

// Assert Alice (Earner)
assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalance = 99_999999);
assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalance = 100_000000);
assertEq(_wrappedMToken.accruedYieldOf(_alice), 0);

_giveM(_carol, 100_000000);
Expand All @@ -246,10 +246,10 @@ contract ProtocolIntegrationTests is TestBase {
assertEq(_wrappedMToken.accruedYieldOf(_carol), 0);

// Assert Globals
assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += _aliceBalance);
assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += 100_000000);
assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply += 100_000000);
assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield);
assertEq(_wrappedMToken.excess(), _excess);
assertEq(_wrappedMToken.excess(), _excess -= 1);

assertGe(_wrapperBalanceOfM, _totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield + _excess);

Expand Down Expand Up @@ -338,7 +338,7 @@ contract ProtocolIntegrationTests is TestBase {

// Assert Alice (Earner)
assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalance);
assertEq(_wrappedMToken.accruedYieldOf(_alice), _aliceAccruedYield += 57376);
assertEq(_wrappedMToken.accruedYieldOf(_alice), _aliceAccruedYield += 57377);

// Assert Bob (Earner)
assertEq(_wrappedMToken.balanceOf(_bob), _bobBalance);
Expand Down Expand Up @@ -366,16 +366,16 @@ contract ProtocolIntegrationTests is TestBase {
_giveM(_carol, 100_000000);
_wrap(_carol, _carol, 100_000000);

assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalance += 99_999999);
assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalance += 100_000000);
assertEq(_wrappedMToken.balanceOf(_carol), _carolBalance += 100_000000);

assertEq(_mToken.balanceOf(address(_wrappedMToken)), _wrapperBalanceOfM += 199_999999);

// Assert Globals
assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += 99_999999);
assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply += 100_000000);
assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply += 100_000000);
assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield);
assertEq(_wrappedMToken.excess(), _excess);
assertEq(_wrappedMToken.excess(), _excess -= 1);

assertGe(_wrapperBalanceOfM, _totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield + _excess);

Expand Down Expand Up @@ -430,7 +430,7 @@ contract ProtocolIntegrationTests is TestBase {
assertEq(_wrappedMToken.accruedYieldOf(_alice), _aliceAccruedYield -= 3_614473);

// Assert Globals
assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply -= 99_999999);
assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply -= 100_000000);
assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply += _aliceBalance);
assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= 3_614473);
assertEq(_wrappedMToken.excess(), _excess);
Expand Down Expand Up @@ -469,64 +469,64 @@ contract ProtocolIntegrationTests is TestBase {

_unwrap(_alice, _alice, _aliceBalance);

// Assert Alice (Non-Earner)
assertEq(_mToken.balanceOf(_alice), 103_614471);
assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalance -= 103_614472);
assertEq(_wrappedMToken.accruedYieldOf(_alice), 0);

// Assert Globals
assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply);
assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply -= 103_614472);
assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply -= _aliceBalance);
assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield);
assertEq(_wrappedMToken.excess(), _excess += 1);
assertEq(_wrappedMToken.excess(), _excess);

// Assert Alice (Non-Earner)
assertEq(_mToken.balanceOf(_alice), _aliceBalance);
assertEq(_wrappedMToken.balanceOf(_alice), _aliceBalance -= _aliceBalance);
assertEq(_wrappedMToken.accruedYieldOf(_alice), 0);

assertGe(_wrapperBalanceOfM, _totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield + _excess);

// Accrued yield of Bob is claimed when unwrapping
_unwrap(_bob, _bob, _bobBalance + _bobAccruedYield);

// Assert Bob (Earner)
assertEq(_mToken.balanceOf(_bob), 100_000000 + 3_614474 - 1);
assertEq(_wrappedMToken.balanceOf(_bob), _bobBalance -= 100_000000);
assertEq(_wrappedMToken.accruedYieldOf(_bob), _bobAccruedYield -= 3_614474);

// Assert Globals
assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply -= 100_000000);
assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply -= _bobBalance);
assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply);
assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= 3_614474);
assertEq(_wrappedMToken.excess(), _excess += 1);
assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= _bobAccruedYield);
assertEq(_wrappedMToken.excess(), _excess -= 1);

// Assert Bob (Earner)
assertEq(_mToken.balanceOf(_bob), _bobBalance + _bobAccruedYield);
assertEq(_wrappedMToken.balanceOf(_bob), _bobBalance -= _bobBalance);
assertEq(_wrappedMToken.accruedYieldOf(_bob), _bobAccruedYield -= _bobAccruedYield);

assertGe(_wrapperBalanceOfM, _totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield + _excess);

// Accrued yield of Carol is claimed when unwrapping
_unwrap(_carol, _carol, _carolBalance + _carolAccruedYield);

// Assert Carol (Earner)
assertEq(_mToken.balanceOf(_carol), 100_000000 + 2_395361 - 1);
assertEq(_wrappedMToken.balanceOf(_carol), _carolBalance -= 100_000000);
assertEq(_wrappedMToken.accruedYieldOf(_carol), _carolAccruedYield -= 2_395361);

// Assert Globals
assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply -= 100_000000);
assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply -= _carolBalance);
assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply);
assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= 2_395361);
assertEq(_wrappedMToken.excess(), _excess);
assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield -= _carolAccruedYield);
assertEq(_wrappedMToken.excess(), _excess -= 1);

// Assert Carol (Earner)
assertEq(_mToken.balanceOf(_carol), _carolBalance + _carolAccruedYield);
assertEq(_wrappedMToken.balanceOf(_carol), _carolBalance -= _carolBalance);
assertEq(_wrappedMToken.accruedYieldOf(_carol), _carolAccruedYield -= _carolAccruedYield);

assertGe(_wrapperBalanceOfM, _totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield + _excess);

_unwrap(_dave, _dave, _daveBalance);

// Assert Dave (Non-Earner)
assertEq(_mToken.balanceOf(_dave), 100_000000 - 1);
assertEq(_wrappedMToken.balanceOf(_dave), _daveBalance -= 100_000000);
assertEq(_wrappedMToken.accruedYieldOf(_dave), 0);

// Assert Globals
assertEq(_wrappedMToken.totalEarningSupply(), _totalEarningSupply);
assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply -= 100_000000);
assertEq(_wrappedMToken.totalNonEarningSupply(), _totalNonEarningSupply -= _daveBalance);
assertEq(_wrappedMToken.totalAccruedYield(), _totalAccruedYield);
assertEq(_wrappedMToken.excess(), _excess);

// Assert Dave (Non-Earner)
assertEq(_mToken.balanceOf(_dave), _daveBalance);
assertEq(_wrappedMToken.balanceOf(_dave), _daveBalance -= _daveBalance);
assertEq(_wrappedMToken.accruedYieldOf(_dave), 0);

assertGe(_wrapperBalanceOfM, _totalEarningSupply + _totalNonEarningSupply + _totalAccruedYield + _excess);

uint256 vaultStartingBalance_ = _mToken.balanceOf(_excessDestination);
Expand Down
8 changes: 4 additions & 4 deletions test/unit/Stories.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ contract StoryTests is Test {
assertEq(_wrappedMToken.totalNonEarningSupply(), 50_000000);
assertEq(_wrappedMToken.totalSupply(), 450_000000);
assertEq(_wrappedMToken.totalAccruedYield(), 183_333340);
assertEq(_wrappedMToken.excess(), 600_000000);
assertEq(_wrappedMToken.excess(), 599_999995);

vm.prank(_bob);
_wrappedMToken.unwrap(_bob, 333_333330);
Expand All @@ -323,7 +323,7 @@ contract StoryTests is Test {
assertEq(_wrappedMToken.totalNonEarningSupply(), 50_000000);
assertEq(_wrappedMToken.totalSupply(), 250_000000);
assertEq(_wrappedMToken.totalAccruedYield(), 50_000010);
assertEq(_wrappedMToken.excess(), 600_000000);
assertEq(_wrappedMToken.excess(), 599_999995);

vm.prank(_carol);
_wrappedMToken.unwrap(_carol, 250_000000);
Expand All @@ -337,7 +337,7 @@ contract StoryTests is Test {
assertEq(_wrappedMToken.totalNonEarningSupply(), 50_000000);
assertEq(_wrappedMToken.totalSupply(), 50_000000);
assertEq(_wrappedMToken.totalAccruedYield(), 0);
assertEq(_wrappedMToken.excess(), 600_000010);
assertEq(_wrappedMToken.excess(), 600_000005);

vm.prank(_dave);
_wrappedMToken.unwrap(_dave, 50_000000);
Expand All @@ -351,7 +351,7 @@ contract StoryTests is Test {
assertEq(_wrappedMToken.totalNonEarningSupply(), 0);
assertEq(_wrappedMToken.totalSupply(), 0);
assertEq(_wrappedMToken.totalAccruedYield(), 0);
assertEq(_wrappedMToken.excess(), 600_000010);
assertEq(_wrappedMToken.excess(), 600_000005);

_wrappedMToken.claimExcess();

Expand Down
Loading

0 comments on commit 9176f58

Please sign in to comment.