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 29, 2024
1 parent 37237de commit 9e55cec
Show file tree
Hide file tree
Showing 10 changed files with 248 additions and 106 deletions.
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@
[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
1 change: 1 addition & 0 deletions lib/openzeppelin-contracts
Submodule openzeppelin-contracts added at a241f0
17 changes: 0 additions & 17 deletions script/Foo.s.sol

This file was deleted.

155 changes: 155 additions & 0 deletions src/MTokenYield.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
// 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 { IMTokenYield } from "./interfaces/IMTokenYield.sol";
import { IWrappedM } from "./interfaces/IWrappedM.sol";

contract MTokenYield is IMTokenYield, ERC721 {
// TODO: Might be a way to make this a uint112 and uint128 for one slot.
struct YieldBase {
uint240 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 => YieldBase yieldBase) internal _yieldBases;

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

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

_;
}

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

constructor(address mToken_, address wrappedM_) ERC721("MYield by M^0", "ysM") {
mToken = mToken_;
wrappedM = wrappedM_;
}

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

function mint(address account_, uint256 amount_) external onlyWrappedM returns (uint256 tokenId_) {
tokenId_ = ++_tokenCount;

_yieldBases[tokenId_] = YieldBase({
amount: UIntMath.safe240(amount_),
index: IMTokenLike(mToken).currentIndex()
});

_mint(account_, tokenId_);
}

function burn(
address account_,
uint256 tokenId_
) external onlyWrappedM returns (uint256 baseAmount_, uint256 yield_) {
if (ownerOf(tokenId_) != account_) revert NotOwner();

_burn(tokenId_);

YieldBase storage yieldBase_ = _yieldBases[tokenId_];

baseAmount_ = yieldBase_.amount;

yield_ = _multiplyDown(yieldBase_.amount, yieldBase_.index - IMTokenLike(mToken).currentIndex());

delete _yieldBases[tokenId_];
}

function claim(address account_, uint256 tokenId_) external returns (uint256 yield_) {
if (ownerOf(tokenId_) != msg.sender) revert NotOwner();

YieldBase storage yieldBase_ = _yieldBases[tokenId_];

uint128 currentIndex_ = IMTokenLike(mToken).currentIndex();

yield_ = _multiplyDown(yieldBase_.amount, yieldBase_.index - currentIndex_);

yieldBase_.index = currentIndex_;

IWrappedM(wrappedM).extract(account_, yield_);
}

function reshape(
address account_,
uint256[] calldata tokenIds_,
uint256[] calldata amounts_
) external returns (uint256[] memory newTokenIds_, uint256 yield_) {
if (tokenIds_.length != amounts_.length) revert LengthMismatch();

uint128 currentIndex_ = IMTokenLike(mToken).currentIndex();

uint240 total_;

for (uint256 index_; index_ < tokenIds_.length; ++index_) {
uint256 tokenId_ = tokenIds_[index_];

if (ownerOf(tokenId_) != msg.sender) revert NotOwner();

_burn(tokenId_);

YieldBase storage yieldBase_ = _yieldBases[tokenId_];

total_ += yieldBase_.amount;
yield_ += _multiplyDown(yieldBase_.amount, yieldBase_.index - currentIndex_);

delete _yieldBases[tokenId_];
}

newTokenIds_ = new uint256[](tokenIds_.length);

for (uint256 index_; index_ < newTokenIds_.length; ++index_) {
uint256 tokenId_ = newTokenIds_[index_] = ++_tokenCount;
uint240 amount_ = UIntMath.safe240(amounts_[index_]);

_yieldBases[tokenId_] = YieldBase({
amount: amount_,
index: currentIndex_
});

total_ -= amount_;

_mint(account_, tokenId_);
}

if (total_ > 0) revert ExcessAmount();

IWrappedM(wrappedM).extract(account_, yield_);
}

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

function getYieldBase(uint256 tokenId_) external view returns (uint240 amount_, uint128 index_) {
YieldBase storage yieldBase_ = _yieldBases[tokenId_];

amount_ = yieldBase_.amount;
index_ = yieldBase_.index;
}

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

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

function _multiplyDown(uint240 x_, uint128 index_) internal pure returns (uint240) {
unchecked {
return uint240((uint256(x_) * index_) / _EXP_SCALED_ONE);
}
}
}
59 changes: 23 additions & 36 deletions src/WrappedM.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,14 @@ import { IERC20 } from "../lib/common/src/interfaces/IERC20.sol";
import { ERC20Extended } from "../lib/common/src/ERC20Extended.sol";

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

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

uint56 internal constant _EXP_SCALED_ONE = 1e12;

address public immutable mToken;
address public immutable mYield;

uint256 public totalSupply;

Expand All @@ -30,34 +30,43 @@ contract WrappedM is IWrappedM, ERC20Extended {
_;
}

modifier onlyMYield() {
if (msg.sender != mYield) revert NotMYield();

_;
}

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

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

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

function deposit(address account_, uint256 amount_) external onlyEarner returns (uint256 shares_) {
shares_ = _getPrincipalAmountRoundedDown(UIntMath.safe240(amount_), IMTokenLike(mToken).currentIndex());
function deposit(address account_, uint256 amount_) external onlyEarner returns (uint256 mYieldTokenId_) {
emit Transfer(address(0), account_, amount_);

emit Transfer(address(0), account_, shares_);
balanceOf[account_] += amount_;
totalSupply += amount_;

balanceOf[account_] += shares_;
totalSupply += shares_;
mYieldTokenId_ = IMTokenYield(mYield).mint(account_, amount_);

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

function withdraw(address account_, uint256 shares_) external {
emit Transfer(account_, address(0), shares_);
function withdraw(address account_, uint256 mYieldTokenId_) external returns (uint256 baseAmount_, uint256 yield_) {
(baseAmount_, yield_) = IMTokenYield(mYield).burn(msg.sender, mYieldTokenId_);

uint256 amount_ = _getPresentAmountRoundedDown(UIntMath.safe112(shares_), IMTokenLike(mToken).currentIndex());
balanceOf[account_] -= baseAmount_;
totalSupply -= baseAmount_;

balanceOf[account_] += shares_;
totalSupply += shares_;
IERC20(mToken).transfer(account_, baseAmount_ + yield_);
}

IERC20(mToken).transferFrom(address(this), msg.sender, amount_);
function extract(address account_, uint256 amount_) external onlyMYield {
IERC20(mToken).transfer(account_, amount_);
}

/* ============ View/Pure Functions ============ */
Expand All @@ -72,26 +81,4 @@ contract WrappedM is IWrappedM, ERC20Extended {
}

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

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

function _divideDown(uint240 x_, uint128 index_) internal pure returns (uint112 z) {
if (index_ == 0) revert DivisionByZero();

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

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

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

pragma solidity 0.8.23;

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

interface IMTokenYield is IERC721Metadata {
/* ============ Events ============ */

/* ============ Custom Errors ============ */

error NotWrappedM();

error NotOwner();

error LengthMismatch();

error ExcessAmount();

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

function mint(address account, uint256 amount) external returns (uint256 tokenId);

function burn(address account, uint256 tokenId) external returns (uint256 baseAmount, uint256 yield);

function claim(address account_, uint256 tokenId) external returns (uint256 yield);

function reshape(
address account,
uint256[] calldata tokenIds,
uint256[] calldata amounts
) external returns (uint256[] memory newTokenIds, uint256 yield);

/* ============ View/Pure Functions ============ */
}
Loading

0 comments on commit 9e55cec

Please sign in to comment.