Skip to content

Commit

Permalink
feat: NFT yield
Browse files Browse the repository at this point in the history
  • Loading branch information
deluca-mike committed May 30, 2024
1 parent 939ca6e commit 82e61af
Show file tree
Hide file tree
Showing 14 changed files with 358 additions and 77 deletions.
6 changes: 6 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,9 @@
path = lib/forge-std
url = https://github.com/foundry-rs/forge-std
branch = v1
[submodule "lib/common"]
path = lib/common
url = git@github.com:MZero-Labs/common.git
[submodule "lib/openzeppelin-contracts"]
path = lib/openzeppelin-contracts
url = git@github.com:OpenZeppelin/openzeppelin-contracts.git
7 changes: 4 additions & 3 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
{
"plugins": ["prettier-plugin-solidity"],
"plugins": [
"prettier-plugin-solidity"
],
"overrides": [
{
"files": "*.sol",
"options": {
"bracketSpacing": true,
"compiler": "0.8.25",
"compiler": "0.8.23",
"parser": "solidity-parse",
"printWidth": 120,
"tabWidth": 4,
}
}
]
}

28 changes: 22 additions & 6 deletions .solhint.json
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
{
"extends": ["solhint:recommended"],
"plugins": ["prettier"],
"extends": [
"solhint:recommended"
],
"plugins": [
"prettier"
],
"rules": {
"prettier/prettier": "error",
"code-complexity": ["warn", 10],
"compiler-version": ["error", "0.8.25"],
"code-complexity": [
"warn",
10
],
"compiler-version": [
"error",
"0.8.23"
],
"comprehensive-interface": "off",
"const-name-snakecase": "off",
"func-name-mixedcase": "off",
Expand All @@ -14,10 +24,16 @@
"ignoreConstructors": true
}
],
"function-max-lines": ["warn", 100],
"function-max-lines": [
"warn",
100
],
"immutable-vars-naming": "off",
"imports-on-top": "error",
"max-line-length": ["warn", 120],
"max-line-length": [
"warn",
120
],
"no-empty-blocks": "off",
"no-inline-assembly": "off",
"not-rely-on-time": "off",
Expand Down
2 changes: 1 addition & 1 deletion foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ gas_reports = ["*"]
gas_reports_ignore = []
ignored_error_codes = []
optimizer = false
solc_version = "0.8.25"
solc_version = "0.8.23"
verbosity = 3

[profile.production]
Expand Down
1 change: 1 addition & 0 deletions lib/common
Submodule common added at e80940
1 change: 1 addition & 0 deletions lib/openzeppelin-contracts
Submodule openzeppelin-contracts added at dd1e89
17 changes: 0 additions & 17 deletions script/Foo.s.sol

This file was deleted.

9 changes: 0 additions & 9 deletions src/Foo.sol

This file was deleted.

83 changes: 83 additions & 0 deletions src/WrappedM.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
// SPDX-License-Identifier: GPL-3.0

pragma solidity 0.8.23;

import { IERC20 } from "../lib/common/src/interfaces/IERC20.sol";

import { ERC20Extended } from "../lib/common/src/ERC20Extended.sol";

import { IMTokenLike } from "./interfaces/IMTokenLike.sol";
import { IWrappedMYield } from "./interfaces/IWrappedMYield.sol";
import { IWrappedM } from "./interfaces/IWrappedM.sol";

contract WrappedM is IWrappedM, ERC20Extended {
/* ============ Variables ============ */

address public immutable mToken;
address public immutable wrappedMYield;

uint256 public totalSupply;

mapping(address account => uint256 balance) public balanceOf;

/* ============ Modifiers ============ */

modifier onlyEarner() {
if (!IMTokenLike(mToken).isEarning(msg.sender)) revert NotEarner();

_;
}

modifier onlyWrappedMYield() {
if (msg.sender != wrappedMYield) revert NotWrappedMYield();

_;
}

/* ============ Constructor ============ */

constructor(address mToken_, address mYield_) ERC20Extended("Wrapped M by M^0", "wM", 6) {
mToken = mToken_;
wrappedMYield = mYield_;
}

/* ============ Interactive Functions ============ */

function deposit(address account_, uint256 amount_) external onlyEarner returns (uint256 wrappedMYieldTokenId_) {
emit Transfer(address(0), account_, amount_);

balanceOf[account_] += amount_;
totalSupply += amount_;

wrappedMYieldTokenId_ = IWrappedMYield(wrappedMYield).mint(account_, amount_);

IERC20(mToken).transferFrom(msg.sender, address(this), amount_);
}

function withdraw(
address account_,
uint256 wrappedMYieldTokenId_
) external returns (uint256 amount_, uint256 yield_) {
( amount_, yield_ ) = IWrappedMYield(wrappedMYield).burn(msg.sender, wrappedMYieldTokenId_);

emit Transfer(account_, address(0), amount_);

balanceOf[account_] -= amount_;
totalSupply -= amount_;

IERC20(mToken).transfer(account_, amount_ + yield_);
}

/* ============ View/Pure Functions ============ */

/* ============ Internal Interactive Functions ============ */

function _transfer(address sender_, address recipient_, uint256 amount_) internal override {
emit Transfer(sender_, recipient_, amount_);

balanceOf[sender_] -= amount_;
balanceOf[recipient_] += amount_;
}

/* ============ Internal View/Pure Functions ============ */
}
165 changes: 165 additions & 0 deletions src/WrappedMYield.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
// SPDX-License-Identifier: GPL-3.0

pragma solidity 0.8.23;

import { UIntMath } from "../lib/common/src/libs/UIntMath.sol";

import { ERC721 } from "../lib/openzeppelin-contracts/contracts/token/ERC721/ERC721.sol";

import { IMTokenLike } from "./interfaces/IMTokenLike.sol";
import { IWrappedMYield } from "./interfaces/IWrappedMYield.sol";
import { IWrappedM } from "./interfaces/IWrappedM.sol";

contract WrappedMYield is IWrappedMYield, ERC721 {
struct YieldBasis {
uint112 amount;
uint128 index;
}

/* ============ Variables ============ */

uint56 internal constant _EXP_SCALED_ONE = 1e12;

address public immutable mToken;
address public immutable wrappedM;

uint256 internal _tokenCount;

mapping(uint256 tokenId => YieldBasis yieldBasis) internal _yieldBases;

/* ============ Modifiers ============ */

modifier onlyWrappedM() {
if (msg.sender != wrappedM) revert NotWrappedM();

_;
}

/* ============ Constructor ============ */

constructor(address mToken_, address wrappedM_) ERC721("Wrapped M Yield by M^0", "wyM") {
mToken = mToken_;
wrappedM = wrappedM_;
}

/* ============ Interactive Functions ============ */

function mint(address account_, uint256 amount_) external onlyWrappedM returns (uint256 tokenId_) {
return _mint(account_, UIntMath.safe240(amount_), IMTokenLike(mToken).currentIndex());
}

function burn(
address account_,
uint256 tokenId_
) external onlyWrappedM returns (uint256 amount_, uint256 yield_) {
return _burn(account_, tokenId_, IMTokenLike(mToken).currentIndex());
}

function reshape(
address account_,
uint256[] calldata tokenIds_,
uint256[] calldata amounts_
) external returns (uint256[] memory newTokenIds_) {
uint128 currentIndex_ = IMTokenLike(mToken).currentIndex();

uint240 totalAmount_;
uint240 totalYield_;

for (uint256 index_; index_ < tokenIds_.length; ++index_) {
(uint240 amount_, uint240 yield_) = _burn(msg.sender, tokenIds_[index_], currentIndex_);

totalAmount_ += amount_;
totalYield_ += yield_;
}

uint128 blendedIndex_ = _getIndex(totalAmount_, _getPrincipalAmount(totalAmount_ + totalYield_, currentIndex_));

newTokenIds_ = new uint256[](amounts_.length + 1);

for (uint256 index_; index_ < amounts_.length; ++index_) {
uint240 amount_ = (index_ == amounts_.length - 1)
? totalAmount_
: UIntMath.safe240(amounts_[index_]);

newTokenIds_[index_] = _mint(account_, amount_, blendedIndex_);

totalAmount_ -= amount_;
}
}

/* ============ View/Pure Functions ============ */

function getYieldBasis(uint256 tokenId_) public view returns (uint112 amount_, uint128 index_) {
YieldBasis storage yieldBasis_ = _yieldBases[tokenId_];

return (yieldBasis_.amount, yieldBasis_.index);
}

/* ============ Internal Interactive Functions ============ */

function _mint(address account_, uint240 amount_, uint128 currentIndex_) internal returns (uint256 tokenId_) {
_yieldBases[tokenId_ = ++_tokenCount] = YieldBasis({
amount: _getPrincipalAmount(UIntMath.safe240(amount_), currentIndex_),
index: currentIndex_
});

_mint(account_, tokenId_);
}

function _burn(
address account_,
uint256 tokenId_,
uint128 currentIndex_
) internal returns (uint240 amount_, uint240 yield_) {
_revertIfNotOwner(account_, tokenId_);

_burn(tokenId_);

(uint112 basisAmount_, uint128 basisIndex_) = getYieldBasis(tokenId_);

amount_ = _getPresentAmount(basisAmount_, basisIndex_);
yield_ = _getPresentAmount(basisAmount_, currentIndex_) - amount_;

delete _yieldBases[tokenId_];
}

/* ============ Internal View/Pure Functions ============ */

function _revertIfNotOwner(address account_, uint256 tokenId_) internal view {
if (ownerOf(tokenId_) != account_) revert NotOwner();
}

function _multiplyDown(uint112 x_, uint128 y_) internal pure returns (uint240) {
unchecked {
return uint240((uint256(x_) * y_) / _EXP_SCALED_ONE);
}
}

function _divideDown128(uint240 x_, uint128 y_) internal pure returns (uint112) {
if (y_ == 0) revert DivisionByZero();

unchecked {
return UIntMath.safe112((uint256(x_) * _EXP_SCALED_ONE) / y_);
}
}

function _divideDown112(uint240 x_, uint112 y_) internal pure returns (uint128) {
if (y_ == 0) revert DivisionByZero();

unchecked {
return UIntMath.safe128((uint256(x_) * _EXP_SCALED_ONE) / y_);
}
}

function _getIndex(uint240 presentAmount_, uint112 principalAmount) internal pure returns (uint128) {
return _divideDown112(presentAmount_, principalAmount);
}

function _getPresentAmount(uint112 principalAmount_, uint128 index_) internal pure returns (uint240) {
return _multiplyDown(principalAmount_, index_);
}

function _getPrincipalAmount(uint240 presentAmount_, uint128 index_) internal pure returns (uint112) {
return _divideDown128(presentAmount_, index_);
}
}
11 changes: 11 additions & 0 deletions src/interfaces/IMTokenLike.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// SPDX-License-Identifier: GPL-3.0

pragma solidity 0.8.23;

interface IMTokenLike {
/* ============ View/Pure Functions ============ */

function currentIndex() external view returns (uint128);

function isEarning(address account) external view returns (bool);
}
Loading

0 comments on commit 82e61af

Please sign in to comment.