Skip to content
Draft
Show file tree
Hide file tree
Changes from 5 commits
Commits
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: 4 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,7 @@
[submodule "lib/common"]
path = lib/common
url = git@github.com:m0-foundation/common.git
[submodule "lib/openzeppelin-contracts"]
path = lib/openzeppelin-contracts
url = git@github.com:OpenZeppelin/openzeppelin-contracts.git
branch = release-v5.3
2 changes: 1 addition & 1 deletion lib/common
Submodule common updated 45 files
+1 −1 .github/workflows/coverage.yml
+1 −1 .github/workflows/test.yml
+1 −1 .prettierrc.json
+1 −1 .solhint.json
+1 −1 lib/forge-std
+1 −1 src/ERC20Extended.sol
+1 −1 src/ERC3009.sol
+1 −1 src/ERC712Extended.sol
+61 −0 src/Migratable.sol
+48 −0 src/Proxy.sol
+1 −1 src/StatefulERC712.sol
+1 −1 src/interfaces/IERC1271.sol
+1 −1 src/interfaces/IERC20.sol
+3 −3 src/interfaces/IERC20Extended.sol
+1 −1 src/interfaces/IERC3009.sol
+2 −2 src/interfaces/IERC712.sol
+1 −1 src/interfaces/IERC712Extended.sol
+44 −0 src/interfaces/IMigratable.sol
+1 −1 src/interfaces/IStatefulERC712.sol
+11 −11 src/libs/Bytes32String.sol
+110 −0 src/libs/ContinuousIndexingMath.sol
+41 −17 src/libs/ContractHelper.sol
+117 −0 src/libs/IndexingMath.sol
+10 −10 src/libs/SignatureChecker.sol
+69 −55 src/libs/UIntMath.sol
+3 −3 test/Bytes32String.sol
+244 −0 test/ContinuousIndexingMath.t.sol
+1 −1 test/ContractHelper.t.sol
+41 −31 test/ERC20Extended.t.sol
+4 −4 test/ERC3009.t.sol
+5 −5 test/ERC712Extended.t.sol
+167 −0 test/IndexingMath.t.sol
+13 −13 test/SignatureChecker.t.sol
+22 −12 test/UIntMath.t.sol
+8 −8 test/invariant/ERC20ExtendedInvariant.t.sol
+1 −1 test/utils/Bytes32StringHarness.sol
+34 −0 test/utils/ContinuousIndexingMathHarness.sol
+1 −1 test/utils/ContractHelperHarness.sol
+1 −1 test/utils/ERC1271WalletMock.sol
+1 −1 test/utils/ERC20ExtendedHarness.sol
+1 −1 test/utils/ERC712ExtendedHarness.sol
+26 −0 test/utils/IndexingMathHarness.sol
+1 −1 test/utils/SignatureCheckerHarness.sol
+1 −1 test/utils/TestUtils.t.sol
+9 −1 test/utils/UIntMathHarness.sol
1 change: 1 addition & 0 deletions lib/openzeppelin-contracts
Submodule openzeppelin-contracts added at e4f702
7 changes: 5 additions & 2 deletions script/Deploy.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,18 @@ contract Deploy is Script, DeployBase {

function run() external {
(address deployer_, ) = deriveRememberKey(vm.envString("MNEMONIC"), 0);
address migrationAdmin_ = vm.envAddress("MIGRATION_ADMIN");

console2.log("Deployer:", deployer_);
console2.log("Migration Admin:", migrationAdmin_);

vm.startBroadcast(deployer_);

address mToken_ = deploy(_REGISTRAR);
(address implementation_, address proxy_) = deploy(_REGISTRAR, migrationAdmin_);

vm.stopBroadcast();

console2.log("M Token address:", mToken_);
console2.log("M Token Implementation address:", implementation_);
console2.log("M Token Proxy address:", proxy_);
}
}
38 changes: 29 additions & 9 deletions script/DeployBase.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,50 @@

pragma solidity 0.8.26;

import { ERC1967Proxy } from "../lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import { ContractHelper } from "../lib/common/src/libs/ContractHelper.sol";

import { MToken } from "../src/MToken.sol";

contract DeployBase {
/**
* @dev Deploys the M Token contract.
* @param registrar_ The address of the Registrar contract.
* @return mToken_ The address of the deployed M Token contract.
* @param registrar_ The address of the Registrar contract.
* @param migrationAdmin_ The address of a migration admin.
* @return implementation_ The address of the deployed M Token implementation.
* @return proxy_ The address of the deployed M Token proxy.
*/
function deploy(address registrar_) public virtual returns (address mToken_) {
// M token needs `registrar_` addresses.
return address(new MToken(registrar_));
function deploy(address registrar_, address migrationAdmin_) public virtual returns (address implementation_, address proxy_) {
implementation_ = address(new MToken(registrar_, migrationAdmin_));
proxy_ = address(new ERC1967Proxy(implementation_, abi.encodeCall(MToken.initialize, ())));
}

function _getExpectedMToken(address deployer_, uint256 deployerNonce_) internal pure returns (address) {
function _getExpectedMTokenImplementation(
address deployer_,
uint256 deployerNonce_
) internal pure returns (address) {
return ContractHelper.getContractFrom(deployer_, deployerNonce_);
}

function getExpectedMToken(address deployer_, uint256 deployerNonce_) public pure virtual returns (address) {
return _getExpectedMToken(deployer_, deployerNonce_);
function getExpectedMTokenImplementation(
address deployer_,
uint256 deployerNonce_
) public pure virtual returns (address) {
return _getExpectedMTokenImplementation(deployer_, deployerNonce_);
}

function _getExpectedMTokenProxy(address deployer_, uint256 deployerNonce_) internal pure returns (address) {
return ContractHelper.getContractFrom(deployer_, deployerNonce_ + 1);
}

function getExpectedMTokenProxy(
address deployer_,
uint256 deployerNonce_
) public pure virtual returns (address) {
return _getExpectedMTokenProxy(deployer_, deployerNonce_);
}

function getDeployerNonceAfterMTokenDeployment(uint256 deployerNonce_) public pure virtual returns (uint256) {
return deployerNonce_ + 1;
return deployerNonce_ + 2;
}
}
40 changes: 36 additions & 4 deletions src/MToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pragma solidity 0.8.26;

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

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

Expand All @@ -20,7 +21,7 @@ import { ContinuousIndexingMath } from "./libs/ContinuousIndexingMath.sol";
* @author M^0 Labs
* @notice ERC20 M Token living on other chains.
*/
contract MToken is IMToken, ContinuousIndexing, ERC20Extended {
contract MToken is IMToken, ContinuousIndexing, ERC20Extended, Migratable {
/* ============ Structs ============ */

/**
Expand All @@ -41,6 +42,9 @@ contract MToken is IMToken, ContinuousIndexing, ERC20Extended {
/// @inheritdoc IMToken
address public immutable registrar;

/// @inheritdoc IMToken
address public immutable migrationAdmin;

/// @inheritdoc IMToken
uint240 public totalNonEarningSupply;

Expand All @@ -62,11 +66,24 @@ contract MToken is IMToken, ContinuousIndexing, ERC20Extended {

/**
* @notice Constructs the M Token contract.
* @param registrar_ The address of the Registrar contract.
* @dev Sets immutable storage.
* @param registrar_ The address of the Registrar contract.
* @param portal_ The address of the Portal contract.
* @param migrationAdmin_ The address of a migration admin.
*/
constructor(address registrar_) ContinuousIndexing() ERC20Extended("M by M^0", "M", 6) {
constructor(address registrar_, address portal_, address migrationAdmin_) ContinuousIndexing() ERC20Extended("M by M^0", "M", 6) {
_disableInitializers();

if ((registrar = registrar_) == address(0)) revert ZeroRegistrar();
if ((portal = RegistrarReader.getPortal(registrar_)) == address(0)) revert ZeroPortal();
if ((portal = portal_) == address(0)) revert ZeroPortal();
if ((migrationAdmin = migrationAdmin_) == address(0)) revert ZeroMigrationAdmin();
}

/* ============ Initializer ============ */

/// @inheritdoc IMToken
function initialize() external initializer {
_initialize();
}

/* ============ Interactive Functions ============ */
Expand Down Expand Up @@ -112,6 +129,15 @@ contract MToken is IMToken, ContinuousIndexing, ERC20Extended {
_stopEarning(account_);
}

/**
* @dev Performs the contract migration by calling `migrator_`.
* @param migrator_ The address of a migrator contract.
*/
function migrate(address migrator_) external {
if (msg.sender != migrationAdmin) revert UnauthorizedMigration();
_migrate(migrator_);
}

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

/// @inheritdoc IMToken
Expand Down Expand Up @@ -438,4 +464,10 @@ contract MToken is IMToken, ContinuousIndexing, ERC20Extended {
function _revertIfNotPortal() internal view {
if (msg.sender != portal) revert NotPortal();
}

/// @inheritdoc Migratable
function _getMigrator() internal pure override returns (address migrator_) {
// NOTE: in this version only the admin-controlled migration via `migrate()` function is supported
return address(0);
}
}
10 changes: 6 additions & 4 deletions src/abstract/ContinuousIndexing.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

pragma solidity 0.8.26;

import { Initializable } from "../../lib/openzeppelin-contracts/contracts/proxy/utils/Initializable.sol";

import { IContinuousIndexing } from "../interfaces/IContinuousIndexing.sol";

import { ContinuousIndexingMath } from "../libs/ContinuousIndexingMath.sol";
Expand All @@ -10,7 +12,7 @@ import { ContinuousIndexingMath } from "../libs/ContinuousIndexingMath.sol";
* @title Abstract Continuous Indexing Contract to handle index updates in inheriting contracts.
* @author M^0 Labs
*/
abstract contract ContinuousIndexing is IContinuousIndexing {
abstract contract ContinuousIndexing is IContinuousIndexing, Initializable {
/* ============ Variables ============ */

/// @inheritdoc IContinuousIndexing
Expand All @@ -19,10 +21,10 @@ abstract contract ContinuousIndexing is IContinuousIndexing {
/// @inheritdoc IContinuousIndexing
uint40 public latestUpdateTimestamp;

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

/// @notice Constructs the ContinuousIndexing contract.
constructor() {
/// @notice Initializes Proxy's storage.
function _initialize() internal onlyInitializing {
latestIndex = ContinuousIndexingMath.EXP_SCALED_ONE;
latestUpdateTimestamp = uint40(block.timestamp);
}
Expand Down
18 changes: 18 additions & 0 deletions src/interfaces/IMToken.sol
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,18 @@ interface IMToken is IContinuousIndexing, IERC20Extended {
/// @notice Emitted when principal of total supply (earning and non-earning) will overflow a `type(uint112).max`.
error OverflowsPrincipalOfTotalSupply();

/// @notice Emitted when the migrate function is called by a account other than the migration admin.
error UnauthorizedMigration();

/// @notice Emitted in constructor if the Portal address in the Registrar is 0x0.
error ZeroPortal();

/// @notice Emitted in constructor if the Registrar address is 0x0.
error ZeroRegistrar();

/// @notice Emitted in constructor if Migration Admin is 0x0.
error ZeroMigrationAdmin();

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

/**
Expand Down Expand Up @@ -102,6 +108,15 @@ interface IMToken is IContinuousIndexing, IERC20Extended {
*/
function stopEarning(address account) external;

/// @notice Initializes the Proxy's storage.
function initialize() external;

/**
* @notice Performs an arbitrarily defined migration.
* @param migrator The address of a migrator contract.
*/
function migrate(address migrator) external;

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

/// @notice The address of the M Portal contract.
Expand All @@ -110,6 +125,9 @@ interface IMToken is IContinuousIndexing, IERC20Extended {
/// @notice The address of the Registrar contract.
function registrar() external view returns (address);

/// @notice The account that can call the `migrate(address migrator)` function.
function migrationAdmin() external view returns (address migrationAdmin);

/**
* @notice The principal of an earner M token balance.
* @param account The account to get the principal balance of.
Expand Down
49 changes: 33 additions & 16 deletions test/Deploy.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,48 @@

pragma solidity 0.8.26;

import { Test, console2 } from "../lib/forge-std/src/Test.sol";
import { Test } from "../lib/forge-std/src/Test.sol";

import { IMToken } from "../src/interfaces/IMToken.sol";
import { IRegistrar } from "../src/interfaces/IRegistrar.sol";

import { DeployBase } from "../script/DeployBase.sol";

import { MockRegistrar } from "./utils/Mocks.sol";

contract Deploy is Test, DeployBase {
MockRegistrar internal _registrar;
address internal constant _EXPECTED_PROXY = 0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b;
address internal constant _DEPLOYER = 0xF2f1ACbe0BA726fEE8d75f3E32900526874740BB;

address internal _portal = makeAddr("portal");
address internal immutable _MIGRATION_ADMIN = makeAddr("migration admin");
address internal immutable _REGISTRAR = makeAddr("registrar");
address internal immutable _PORTAL = makeAddr("portal");

function setUp() external {
_registrar = new MockRegistrar();
_registrar.setPortal(_portal);
}
uint64 internal constant _DEPLOYER_PROXY_NONCE = 8;

function test_deploy() external {
address mToken_ = deploy(address(_registrar));

assertEq(mToken_, getExpectedMToken(address(this), 2));

// MToken assertions
assertEq(IMToken(mToken_).portal(), _portal);
assertEq(IMToken(mToken_).registrar(), address(_registrar));
vm.mockCall(
_REGISTRAR,
abi.encodeWithSelector(IRegistrar.portal.selector),
abi.encode(_PORTAL)
);

// Set nonce to 1 before `_DEPLOYER_PROXY_NONCE` since implementation is deployed before proxy.
vm.setNonce(_DEPLOYER, _DEPLOYER_PROXY_NONCE - 1);

vm.startPrank(_DEPLOYER);
(address implementation_, address proxy_) = deploy(_REGISTRAR, _MIGRATION_ADMIN);
vm.stopPrank();

// M Token Implementation assertions
assertEq(implementation_, getExpectedMTokenImplementation(_DEPLOYER, 7));
assertEq(IMToken(implementation_).migrationAdmin(), _MIGRATION_ADMIN);
assertEq(IMToken(implementation_).registrar(), _REGISTRAR);
assertEq(IMToken(implementation_).portal(), _PORTAL);

// M Token Proxy assertions
assertEq(proxy_, getExpectedMTokenProxy(_DEPLOYER, 7));
assertEq(proxy_, _EXPECTED_PROXY);
assertEq(IMToken(proxy_).migrationAdmin(), _MIGRATION_ADMIN);
assertEq(IMToken(proxy_).registrar(), _REGISTRAR);
assertEq(IMToken(proxy_).portal(), _PORTAL);
}
}
16 changes: 13 additions & 3 deletions test/MToken.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ pragma solidity 0.8.26;

import { IERC20Extended } from "../lib/common/src/interfaces/IERC20Extended.sol";
import { UIntMath } from "../lib/common/src/libs/UIntMath.sol";
import { ERC1967Proxy } from "../lib/openzeppelin-contracts/contracts/proxy/ERC1967/ERC1967Proxy.sol";

import { IContinuousIndexing } from "../src/interfaces/IContinuousIndexing.sol";
import { IMToken } from "../src/interfaces/IMToken.sol";
Expand All @@ -26,20 +27,24 @@ contract MTokenTests is TestUtils {

address internal _portal = makeAddr("portal");

address internal _migrationAdmin = makeAddr("migrationAdmin");
address[] internal _accounts = [_alice, _bob, _charlie, _david];

uint256 internal _start = vm.getBlockTimestamp();

uint128 internal _expectedCurrentIndex;

MockRegistrar internal _registrar;

MTokenHarness internal _implementation;
MTokenHarness internal _mToken;

function setUp() external {
_registrar = new MockRegistrar();
_registrar.setPortal(_portal);

_mToken = new MTokenHarness(address(_registrar));
_implementation = new MTokenHarness(address(_registrar), _migrationAdmin);
_mToken = MTokenHarness(address(new ERC1967Proxy(address(_implementation), abi.encodeCall(IMToken.initialize, ()))));

_mToken.setLatestIndex(_expectedCurrentIndex = 1_100000068703);
}
Expand All @@ -55,14 +60,19 @@ contract MTokenTests is TestUtils {
/* ============ constructor ============ */
function test_constructor_zeroRegistrar() external {
vm.expectRevert(IMToken.ZeroRegistrar.selector);
new MTokenHarness(address(0));
new MTokenHarness(address(0), _migrationAdmin);
}

function test_constructor_zeroMigrationAdmin() external {
vm.expectRevert(IMToken.ZeroMigrationAdmin.selector);
new MTokenHarness(address(_registrar), address(0));
}

function test_constructor_zeroPortal() external {
_registrar.setPortal(address(0));

vm.expectRevert(IMToken.ZeroPortal.selector);
new MTokenHarness(address(_registrar));
new MTokenHarness(address(_registrar), _migrationAdmin);
}

/* ============ mint ============ */
Expand Down
Loading
Loading