Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Mint/burn ethereum #44

Merged
merged 27 commits into from
Oct 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
de4f8da
feat: mint/burn ethereum
MerlinEgalite Oct 22, 2024
679c7c2
chore: small letter for token
QGarchery Oct 22, 2024
7536a3e
docs: author Morpho Association
QGarchery Oct 22, 2024
1a05089
chore: missed small letter
QGarchery Oct 22, 2024
0e663e5
chore: get back to OZ v5.0.1
QGarchery Oct 22, 2024
0c996bc
fix: test bundler address(1)
QGarchery Oct 22, 2024
bd826d5
chore: use STORAGE LAYOUT
QGarchery Oct 22, 2024
e303f86
chore: rename dao to owner
QGarchery Oct 22, 2024
8089691
fix: init permit
QGarchery Oct 22, 2024
971ba5c
fix: visibility of public functions
QGarchery Oct 22, 2024
ced305c
docs: natspec on optimism getters
QGarchery Oct 22, 2024
25771e9
chore: use internal instead of private
QGarchery Oct 22, 2024
f385bee
chore: rename test file according to convention
QGarchery Oct 22, 2024
d09c21a
fix: use correct license
QGarchery Oct 22, 2024
ed5ec9b
chore: consistent solidity version
QGarchery Oct 22, 2024
04238df
chore: use relative paths
QGarchery Oct 22, 2024
472666b
feat: enforce line wrapping in the config
QGarchery Oct 22, 2024
42afb3e
chore: forge fmt
QGarchery Oct 22, 2024
f1f6dc7
fix: missed absolute dependency
QGarchery Oct 22, 2024
4549e02
fix: missed inconsistent pragma
QGarchery Oct 22, 2024
5d7f08b
fix: optimism token inherit from bridge interface
QGarchery Oct 22, 2024
3fa15cb
Merge branch 'feat/mint-burn-ethereum' into fix/optimism
QGarchery Oct 22, 2024
5910fa5
refactor: move mint and burn to Token
QGarchery Oct 22, 2024
6dd488b
test: testMint and testBurn
MathisGD Oct 22, 2024
6644279
test: 2 more mint tests
MathisGD Oct 22, 2024
5042b50
chore: fmt
MathisGD Oct 22, 2024
294f345
Merge pull request #45 from morpho-org/fix/optimism
MerlinEgalite Oct 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ This repository contains the latest version of the Morpho protocol’s ERC20 tok

## Upgradability

The Morpho Token leverages the eip-1967 to enable upgrade of the logic. This will allow new features to be added in the future.
The Morpho token leverages the eip-1967 to enable upgrade of the logic. This will allow new features to be added in the future.

## Delegation

The Morpho Token enables onchain voting power delegation. The contract keeps track of all the addresses current voting power, which allows onchain votes thanks to storage proofs (on specific voting contracts).
The Morpho token enables onchain voting power delegation. The contract keeps track of all the addresses current voting power, which allows onchain votes thanks to storage proofs (on specific voting contracts).

## Migration

Expand Down
2 changes: 2 additions & 0 deletions foundry.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ libs = ["lib"]
[profile.default.fuzz]
runs = 32

[profile.default.fmt]
wrap_comments = true

[profile.default.rpc_endpoints]
ethereum = "https://eth-mainnet.g.alchemy.com/v2/${ALCHEMY_KEY}"
Expand Down
2 changes: 1 addition & 1 deletion lib/openzeppelin-contracts-upgradeable
18 changes: 15 additions & 3 deletions src/MorphoTokenEthereum.sol
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {Token} from "./Token.sol";
/// @title MorphoTokenEthereum
/// @author Morpho Association
/// @custom:contact [email protected]
/// @notice The Morpho Token contract for Ethereum.
/// @notice The Morpho token contract for Ethereum.
contract MorphoTokenEthereum is Token {
/* CONSTANTS */

Expand All @@ -21,12 +21,12 @@ contract MorphoTokenEthereum is Token {
/// @notice Reverts if the address is the zero address.
error ZeroAddress();

/* PUBLIC */
/* EXTERNAL */

/// @notice Initializes the contract.
/// @param owner The new owner.
/// @param wrapper The wrapper contract address to migrate legacy MORPHO tokens to the new one.
function initialize(address owner, address wrapper) public initializer {
function initialize(address owner, address wrapper) external initializer {
require(owner != address(0), ZeroAddress());

__ERC20_init(NAME, SYMBOL);
Expand All @@ -35,4 +35,16 @@ contract MorphoTokenEthereum is Token {
_transferOwnership(owner);
_mint(wrapper, 1_000_000_000e18); // Mint 1B to the wrapper contract.
}

/// @dev Mints tokens.
function mint(address to, uint256 amount) external onlyOwner {
_mint(to, amount);
emit Mint(to, amount);
}

/// @dev Burns sender's tokens.
function burn(uint256 amount) external {
_burn(_msgSender(), amount);
emit Burn(_msgSender(), amount);
}
}
41 changes: 15 additions & 26 deletions src/MorphoTokenOptimism.sol
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// SPDX-License-Identifier: MIT
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity 0.8.27;

import {IOptimismMintableERC20} from "./interfaces/IOptimismMintableERC20.sol";
Expand All @@ -11,7 +11,7 @@ import {Token} from "./Token.sol";
/// @author Morpho Association
/// @custom:contact [email protected]
/// @notice The Morpho token contract for Optimism networks.
contract MorphoTokenOptimism is Token {
contract MorphoTokenOptimism is Token, IOptimismMintableERC20 {
/* CONSTANTS */

/// @dev The name of the token.
Expand All @@ -21,25 +21,17 @@ contract MorphoTokenOptimism is Token {
string internal constant SYMBOL = "MORPHO";

// keccak256(abi.encode(uint256(keccak256("morpho.storage.OptimismMintableERC20")) - 1)) & ~bytes32(uint256(0xff))
bytes32 private constant OptimismMintableERC20StorageLocation =
bytes32 internal constant OptimismMintableERC20StorageLocation =
0x6fd4c0a11d0843c68c809f0a5f29b102d54bc08a251c384d9ad17600bfa05d00;

/* STRUCTS */
/* STORAGE LAYOUT */

/// @custom:storage-location erc7201:morpho.storage.OptimismMintableERC20
struct OptimismMintableERC20Storage {
address _remoteToken;
address _bridge;
}

/* EVENTS */

/// @dev Emitted whenever tokens are minted for an account.
event Mint(address indexed account, uint256 amount);

/// @dev Emitted whenever tokens are burned from an account.
event Burn(address indexed account, uint256 amount);

/* ERRORS */

/// @notice Reverts if the address is the zero address.
Expand All @@ -57,28 +49,27 @@ contract MorphoTokenOptimism is Token {
_;
}

/* PUBLIC */
/* EXTERNAL */

/// @notice Initializes the contract.
/// @param dao The DAO address.
/// @param remoteToken_ The address of the Morpho Token on Ethereum.
/// @param owner The new owner.
/// @param remoteToken_ The address of the Morpho token on Ethereum.
/// @param bridge_ The address of the StandardBridge contract.
function initialize(address dao, address remoteToken_, address bridge_) public initializer {
require(dao != address(0), ZeroAddress());
function initialize(address owner, address remoteToken_, address bridge_) external initializer {
require(owner != address(0), ZeroAddress());
require(remoteToken_ != address(0), ZeroAddress());
require(bridge_ != address(0), ZeroAddress());

__ERC20_init(NAME, SYMBOL);
__ERC20Permit_init(NAME);

OptimismMintableERC20Storage storage $ = _getOptimismMintableERC20Storage();
$._remoteToken = remoteToken_;
$._bridge = bridge_;

_transferOwnership(dao); // Transfer ownership to the DAO.
_transferOwnership(owner);
}

/* EXTERNAL */

/// @dev Allows the StandardBridge on this network to mint tokens.
function mint(address to, uint256 amount) external onlyBridge {
_mint(to, amount);
Expand All @@ -100,24 +91,22 @@ contract MorphoTokenOptimism is Token {
return _interfaceId == interfaceERC165 || _interfaceId == interfaceOptimismMintableERC20;
}

/// @custom:legacy
/// @dev Legacy getter for REMOTE_TOKEN.
/// @dev Returns the address of the Morpho token on Ethereum.
function remoteToken() external view returns (address) {
OptimismMintableERC20Storage storage $ = _getOptimismMintableERC20Storage();
return $._remoteToken;
}

/// @custom:legacy
/// @dev Legacy getter for BRIDGE.
/// @dev Returns the address of the StandardBridge contract.
function bridge() external view returns (address) {
OptimismMintableERC20Storage storage $ = _getOptimismMintableERC20Storage();
return $._bridge;
}

/* PRIVATE */
/* INTERNAL */

/// @dev Returns the OptimismMintableERC20Storage struct.
function _getOptimismMintableERC20Storage() private pure returns (OptimismMintableERC20Storage storage $) {
function _getOptimismMintableERC20Storage() internal pure returns (OptimismMintableERC20Storage storage $) {
assembly {
$.slot := OptimismMintableERC20StorageLocation
}
Expand Down
12 changes: 9 additions & 3 deletions src/Token.sol
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,11 @@ abstract contract Token is
{
/* CONSTANTS */

bytes32 private constant DELEGATION_TYPEHASH =
bytes32 internal constant DELEGATION_TYPEHASH =
keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)");

// keccak256(abi.encode(uint256(keccak256("morpho.storage.ERC20Delegates")) - 1)) & ~bytes32(uint256(0xff))
bytes32 private constant ERC20DelegatesStorageLocation =
bytes32 internal constant ERC20DelegatesStorageLocation =
0x1dc92b2c6e971ab6e08dfd7dcec0e9496d223ced663ba2a06543451548549500;

/* STORAGE LAYOUT */
Expand Down Expand Up @@ -63,6 +63,12 @@ abstract contract Token is
/// @dev Emitted when a delegatee's delegated voting power changes.
event DelegatedVotingPowerChanged(address indexed delegatee, uint256 oldVotes, uint256 newVotes);

/// @dev Emitted whenever tokens are minted for an account.
event Mint(address indexed account, uint256 amount);

/// @dev Emitted whenever tokens are burned from an account.
event Burn(address indexed account, uint256 amount);

/* CONSTRUCTOR */

/// @dev Disables initializers for the implementation contract.
Expand Down Expand Up @@ -119,7 +125,7 @@ abstract contract Token is
/// @dev Delegates the balance of the `delegator` to `newDelegatee`.
function _delegate(address delegator, address newDelegatee) internal {
ERC20DelegatesStorage storage $ = _getERC20DelegatesStorage();
address oldDelegatee = delegatee(delegator);
address oldDelegatee = $._delegatee[delegator];
$._delegatee[delegator] = newDelegatee;

emit DelegateeChanged(delegator, oldDelegatee, newDelegatee);
Expand Down
11 changes: 6 additions & 5 deletions src/Wrapper.sol
Original file line number Diff line number Diff line change
Expand Up @@ -36,10 +36,10 @@ contract Wrapper {
NEW_MORPHO = morphoToken;
}

/* PUBLIC */
/* EXTERNAL */

/// @dev Compliant to `ERC20Wrapper` contract from OZ for convenience.
function depositFor(address account, uint256 value) public returns (bool) {
function depositFor(address account, uint256 value) external returns (bool) {
require(account != address(0), ZeroAddress());
require(account != address(this), SelfAddress());

Expand All @@ -49,7 +49,7 @@ contract Wrapper {
}

/// @dev Compliant to `ERC20Wrapper` contract from OZ for convenience.
function withdrawTo(address account, uint256 value) public returns (bool) {
function withdrawTo(address account, uint256 value) external returns (bool) {
require(account != address(0), ZeroAddress());
require(account != address(this), SelfAddress());

Expand All @@ -58,8 +58,9 @@ contract Wrapper {
return true;
}

/// @dev To ease wrapping via the bundler contract: https://github.com/morpho-org/morpho-blue-bundlers/blob/main/src/ERC20WrapperBundler.sol
function underlying() public pure returns (address) {
/// @dev To ease wrapping via the bundler contract:
/// https://github.com/morpho-org/morpho-blue-bundlers/blob/main/src/ERC20WrapperBundler.sol
function underlying() external pure returns (address) {
return LEGACY_MORPHO;
}
}
2 changes: 1 addition & 1 deletion src/interfaces/IERC20DelegatesUpgradeable.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
pragma solidity >=0.5.0;

/// @title IDelegates
/// @author Morpho Labs
/// @author Morpho Association
/// @custom:contact [email protected]
interface IERC20DelegatesUpgradeable {
function delegatedVotingPower(address account) external view returns (uint256);
Expand Down
6 changes: 3 additions & 3 deletions src/interfaces/IOptimismMintableERC20.sol
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity >=0.5.0;

import {IERC165} from
"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/introspection/IERC165.sol";
"../../lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/utils/introspection/IERC165.sol";

/// @title IOptimismMintableERC20
/// @notice This interface is available on the OptimismMintableERC20 contract.
Expand Down
2 changes: 2 additions & 0 deletions test/MorphoTokenMigrationTest.sol
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,8 @@ contract MorphoTokenEthereumMigrationTest is BaseTest {

function testMigration(address migrater, uint256 amount) public {
vm.assume(migrater != address(0));
// Unset initiator is address(1), so it can't use the bundler.
vm.assume(migrater != address(1));
vm.assume(migrater != MORPHO_DAO);
amount = bound(amount, MIN_TEST_AMOUNT, 1_000_000_000e18);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
// SPDX-License-Identifier: GPL-2.0-or-later
pragma solidity ^0.8.0;

import {Test, console} from "lib/forge-std/src/Test.sol";
import {Test, console} from "../lib/forge-std/src/Test.sol";
import {MorphoTokenOptimism} from "../src/MorphoTokenOptimism.sol";
import {ERC1967Proxy} from
"lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import {UUPSUpgradeable} from "lib/openzeppelin-contracts-upgradeable/contracts/proxy/utils/UUPSUpgradeable.sol";
"../lib/openzeppelin-contracts-upgradeable/lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import {UUPSUpgradeable} from "../lib/openzeppelin-contracts-upgradeable/contracts/proxy/utils/UUPSUpgradeable.sol";

contract MorphoTokenOptimismTest is Test {
address internal constant MORPHO_DAO = 0xcBa28b38103307Ec8dA98377ffF9816C164f9AFa;
Expand Down
49 changes: 49 additions & 0 deletions test/MorphoTokenTest.sol
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could probably rename this file to MorphoTokenEthereum.sol

Original file line number Diff line number Diff line change
Expand Up @@ -234,4 +234,53 @@ contract MorphoTokenEthereumTest is BaseTest {
keccak256(abi.encode(uint256(keccak256("morpho.storage.ERC20Delegates")) - 1)) & ~bytes32(uint256(0xff));
assertEq(expected, 0x1dc92b2c6e971ab6e08dfd7dcec0e9496d223ced663ba2a06543451548549500);
}

function testMint(address to, uint256 amount) public {
vm.assume(to != address(0));
amount = bound(amount, MIN_TEST_AMOUNT, MAX_TEST_AMOUNT);

uint256 initialTotalSupply = newMorpho.totalSupply();

vm.prank(MORPHO_DAO);
newMorpho.mint(to, amount);

assertEq(newMorpho.totalSupply(), initialTotalSupply + amount);
assertEq(newMorpho.balanceOf(to), amount);
}

function testMintOverflow(address to, uint256 amount) public {
vm.assume(to != address(0));
amount = bound(amount, type(uint256).max - newMorpho.totalSupply() + 1, type(uint256).max);

vm.prank(MORPHO_DAO);
vm.expectRevert();
newMorpho.mint(to, amount);
}

function testMintAccess(address account, address to, uint256 amount) public {
vm.assume(to != address(0));
vm.assume(account != MORPHO_DAO);
amount = bound(amount, MIN_TEST_AMOUNT, MAX_TEST_AMOUNT);

vm.expectRevert();
vm.prank(account);
newMorpho.mint(to, amount);
}

function testBurn(address from, uint256 amountMinted, uint256 amountBurned) public {
vm.assume(from != address(0));
amountMinted = bound(amountMinted, MIN_TEST_AMOUNT, MAX_TEST_AMOUNT);
amountBurned = bound(amountBurned, MIN_TEST_AMOUNT, amountMinted);

uint256 initialTotalSupply = newMorpho.totalSupply();

vm.prank(MORPHO_DAO);
newMorpho.mint(from, amountMinted);

vm.prank(from);
newMorpho.burn(amountBurned);

assertEq(newMorpho.totalSupply(), initialTotalSupply + amountMinted - amountBurned);
assertEq(newMorpho.balanceOf(from), amountMinted - amountBurned);
}
}
14 changes: 6 additions & 8 deletions test/helpers/SigUtils.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,18 @@ library SigUtils {
uint256 deadline;
}

bytes32 private constant DELEGATION_TYPEHASH =
bytes32 internal constant DELEGATION_TYPEHASH =
keccak256("Delegation(address delegatee,uint256 nonce,uint256 expiry)");

bytes32 private constant PERMIT_TYPEHASH =
bytes32 internal constant PERMIT_TYPEHASH =
keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)");

bytes32 private constant TYPE_HASH =
bytes32 internal constant TYPE_HASH =
keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)");

// bytes32 private constant DOMAIN_SEPARATOR = 0xebe7cdc854ed987c1fb2e9e58acbe8b1afdc4375c51e160b9a8de75014baa36b;

/// @dev Computes the hash of the EIP-712 encoded data.
function getDelegationTypedDataHash(Delegation memory delegation, address contractAddress)
public
internal
view
returns (bytes32)
{
Expand All @@ -42,7 +40,7 @@ library SigUtils {
);
}

function getPermitTypedDataHash(Permit memory permit, address contractAddress) public view returns (bytes32) {
function getPermitTypedDataHash(Permit memory permit, address contractAddress) internal view returns (bytes32) {
(, string memory name, string memory version,,,,) = IERC5267(contractAddress).eip712Domain();
return keccak256(
bytes.concat("\x19\x01", domainSeparator(contractAddress, name, version), permitHashStruct(permit))
Expand All @@ -60,7 +58,7 @@ library SigUtils {
}

function domainSeparator(address contractAddress, string memory name, string memory version)
public
internal
view
returns (bytes32)
{
Expand Down