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 9be838f
Show file tree
Hide file tree
Showing 14 changed files with 355 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 a241f0
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.

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

pragma solidity 0.8.23;

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

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 mYieldTokenId_) {
emit Transfer(address(0), account_, amount_);

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

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

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

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

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

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

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

function extract(address account_, uint256 amount_) external onlyWrappedMYield {
IERC20(mToken).transfer(account_, amount_);
}

/* ============ 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 ============ */
}
155 changes: 155 additions & 0 deletions src/WrappedMYield.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 { IWrappedMYield } from "./interfaces/IWrappedMYield.sol";
import { IWrappedM } from "./interfaces/IWrappedM.sol";

contract WrappedMYield is IWrappedMYield, 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("Wrapped M Yield by M^0", "wyM") {
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);
}
}
}
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 9be838f

Please sign in to comment.