From f60132800047bacd9321306b438304e55e51d2a4 Mon Sep 17 00:00:00 2001 From: Michael De Luca Date: Mon, 27 May 2024 09:41:28 -0400 Subject: [PATCH] feat: Simple Wrapper --- .gitmodules | 3 ++ .prettierrc | 7 +-- .solhint.json | 28 +++++++--- foundry.toml | 2 +- lib/common | 1 + script/Foo.s.sol | 17 ------ src/Foo.sol | 9 ---- src/WrappedM.sol | 97 ++++++++++++++++++++++++++++++++++ src/interfaces/IMTokenLike.sol | 11 ++++ src/interfaces/IWrappedM.sol | 25 +++++++++ test/Foo.t.sol | 41 -------------- 11 files changed, 164 insertions(+), 77 deletions(-) create mode 160000 lib/common delete mode 100644 script/Foo.s.sol delete mode 100644 src/Foo.sol create mode 100644 src/WrappedM.sol create mode 100644 src/interfaces/IMTokenLike.sol create mode 100644 src/interfaces/IWrappedM.sol delete mode 100644 test/Foo.t.sol diff --git a/.gitmodules b/.gitmodules index 482a2f9..83abd65 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,3 +2,6 @@ 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 diff --git a/.prettierrc b/.prettierrc index a0c7ac8..954e185 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,11 +1,13 @@ { - "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, @@ -13,4 +15,3 @@ } ] } - diff --git a/.solhint.json b/.solhint.json index af97b6e..3c7c11c 100644 --- a/.solhint.json +++ b/.solhint.json @@ -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", @@ -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", diff --git a/foundry.toml b/foundry.toml index ee59ba1..a302004 100644 --- a/foundry.toml +++ b/foundry.toml @@ -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] diff --git a/lib/common b/lib/common new file mode 160000 index 0000000..e809402 --- /dev/null +++ b/lib/common @@ -0,0 +1 @@ +Subproject commit e809402c4cc21f1fa8291f17ee0aee859f3b0d29 diff --git a/script/Foo.s.sol b/script/Foo.s.sol deleted file mode 100644 index f11a782..0000000 --- a/script/Foo.s.sol +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 - -pragma solidity 0.8.25; - -import { Script } from "forge-std/Script.sol"; -import { Foo } from "../src/Foo.sol"; - -/// @dev See the Solidity Scripting tutorial: https://book.getfoundry.sh/tutorials/solidity-scripting -contract FooScript is Script { - Foo internal foo; - - function run() public { - vm.startBroadcast(); - foo = new Foo(); - vm.stopBroadcast(); - } -} diff --git a/src/Foo.sol b/src/Foo.sol deleted file mode 100644 index be90d28..0000000 --- a/src/Foo.sol +++ /dev/null @@ -1,9 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 - -pragma solidity 0.8.25; - -contract Foo { - function getFoo() external pure returns (string memory) { - return "Foo"; - } -} diff --git a/src/WrappedM.sol b/src/WrappedM.sol new file mode 100644 index 0000000..bd322b3 --- /dev/null +++ b/src/WrappedM.sol @@ -0,0 +1,97 @@ +// 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 { IWrappedM } from "./interfaces/IWrappedM.sol"; + +contract WrappedM is IWrappedM, ERC20Extended { + /* ============ Variables ============ */ + + uint56 internal constant _EXP_SCALED_ONE = 1e12; + + address public immutable mToken; + + uint256 public totalSupply; + + mapping(address account => uint256 balance) public balanceOf; + + /* ============ Modifiers ============ */ + + modifier onlyEarner() { + if (!IMTokenLike(mToken).isEarning(msg.sender)) revert NotEarner(); + + _; + } + + /* ============ Constructor ============ */ + + constructor(address mToken_) ERC20Extended("WrappedM by M^0", "wM", 6) { + mToken = mToken_; + } + + /* ============ Interactive Functions ============ */ + + function deposit(address account_, uint256 amount_) external onlyEarner returns (uint256 shares_) { + shares_ = _getPrincipalAmountRoundedDown(UIntMath.safe240(amount_), IMTokenLike(mToken).currentIndex()); + + emit Transfer(address(0), account_, shares_); + + balanceOf[account_] += shares_; + totalSupply += shares_; + + IERC20(mToken).transferFrom(msg.sender, address(this), amount_); + } + + function withdraw(address account_, uint256 shares_) external { + emit Transfer(account_, address(0), shares_); + + uint256 amount_ = _getPresentAmountRoundedDown(UIntMath.safe112(shares_), IMTokenLike(mToken).currentIndex()); + + balanceOf[account_] += shares_; + totalSupply += shares_; + + IERC20(mToken).transferFrom(address(this), msg.sender, 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 ============ */ + + 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_); + } +} diff --git a/src/interfaces/IMTokenLike.sol b/src/interfaces/IMTokenLike.sol new file mode 100644 index 0000000..e77f7e0 --- /dev/null +++ b/src/interfaces/IMTokenLike.sol @@ -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); +} diff --git a/src/interfaces/IWrappedM.sol b/src/interfaces/IWrappedM.sol new file mode 100644 index 0000000..5ca3b47 --- /dev/null +++ b/src/interfaces/IWrappedM.sol @@ -0,0 +1,25 @@ +// SPDX-License-Identifier: GPL-3.0 + +pragma solidity 0.8.23; + +import { IERC20Extended } from "../../lib/common/src/interfaces/IERC20Extended.sol"; + +interface IWrappedM is IERC20Extended { + /* ============ Events ============ */ + + /* ============ Custom Errors ============ */ + + error NotEarner(); + + error DivisionByZero(); + + /* ============ Interactive Functions ============ */ + + function deposit(address account, uint256 amount) external returns (uint256 shares); + + function withdraw(address account, uint256 shares) external; + + /* ============ View/Pure Functions ============ */ + + function mToken() external view returns (address); +} diff --git a/test/Foo.t.sol b/test/Foo.t.sol deleted file mode 100644 index 8e6f3e8..0000000 --- a/test/Foo.t.sol +++ /dev/null @@ -1,41 +0,0 @@ -// SPDX-License-Identifier: GPL-3.0 - -pragma solidity 0.8.25; - -import "forge-std/Test.sol"; - -import { Foo } from "../src/Foo.sol"; - -interface IERC20 { - function balanceOf(address account) external view returns (uint256); -} - -/// @dev See the "Writing Tests" section in the Foundry Book if this is your first time with Forge. -/// https://book.getfoundry.sh/forge/writing-tests -contract FooTest is Test { - uint256 public mainnetFork; - - Foo public fooContract = new Foo(); - - function setUp() public { - mainnetFork = vm.createFork(vm.rpcUrl("mainnet"), 16_428_000); - } - - /// @dev Simple test. Run Forge with `-vvvv` to see stack traces. - function test() external { - string memory foo = fooContract.getFoo(); - - assertEq(foo, "Foo"); - } - - /// @dev Test that runs against a fork of Ethereum Mainnet. You need to set `MAINNET_RPC_URL` in your `.env` - function testFork() external { - vm.selectFork(mainnetFork); - - address usdc = 0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48; - address holder = 0x7713974908Be4BEd47172370115e8b1219F4A5f0; - uint256 actualBalance = IERC20(usdc).balanceOf(holder); - uint256 expectedBalance = 196_307_713.810457e6; - assertEq(actualBalance, expectedBalance); - } -}