Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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 69 files
+1 −1 .github/workflows/coverage.yml
+1 −1 .github/workflows/test.yml
+1 −0 .gitignore
+4 −0 .gitmodules
+3 −5 .prettierrc.json
+8 −27 .solhint.json
+2 −0 foundry.toml
+1 −1 lib/forge-std
+1 −0 lib/openzeppelin-contracts-upgradeable
+1,326 −0 package-lock.json
+5 −5 package.json
+65 −0 script/deploy/DeployHelpers.sol
+59 −0 script/deploy/interfaces/ICreateXLike.sol
+3 −3 src/ERC20Extended.sol
+205 −0 src/ERC20ExtendedUpgradeable.sol
+1 −1 src/ERC3009.sol
+379 −0 src/ERC3009Upgradeable.sol
+1 −1 src/ERC712Extended.sol
+212 −0 src/ERC712ExtendedUpgradeable.sol
+61 −0 src/Migratable.sol
+48 −0 src/Proxy.sol
+1 −1 src/StatefulERC712.sol
+56 −0 src/StatefulERC712Upgradeable.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
+82 −55 src/libs/UIntMath.sol
+3 −3 test/Bytes32String.sol
+244 −0 test/ContinuousIndexingMath.t.sol
+1 −1 test/ContractHelper.t.sol
+7 −479 test/ERC20Extended.t.sol
+23 −0 test/ERC20ExtendedUpgradeable.t.sol
+7 −816 test/ERC3009.t.sol
+23 −0 test/ERC3009Upgradeable.t.sol
+7 −241 test/ERC712Extended.t.sol
+23 −0 test/ERC712ExtendedUpgradeable.t.sol
+167 −0 test/IndexingMath.t.sol
+13 −13 test/SignatureChecker.t.sol
+29 −12 test/UIntMath.t.sol
+496 −0 test/base/BaseERC20Extended.t.sol
+826 −0 test/base/BaseERC3009.t.sol
+250 −0 test/base/BaseERC712Extended.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
+4 −2 test/utils/ERC20ExtendedHarness.sol
+100 −0 test/utils/ERC20ExtendedUpgradeableHarness.sol
+4 −2 test/utils/ERC712ExtendedHarness.sol
+57 −0 test/utils/ERC712ExtendedUpgradeableHarness.sol
+47 −0 test/utils/IERC20ExtendedHarness.sol
+9 −0 test/utils/IERC20ExtendedUpgradeableHarness.sol
+26 −0 test/utils/IERC712ExtendedHarness.sol
+9 −0 test/utils/IERC712ExtendedUpgradeableHarness.sol
+26 −0 test/utils/IndexingMathHarness.sol
+1 −1 test/utils/SignatureCheckerHarness.sol
+1 −1 test/utils/TestUtils.t.sol
+13 −1 test/utils/UIntMathHarness.sol
1 change: 1 addition & 0 deletions lib/openzeppelin-contracts
Submodule openzeppelin-contracts added at e4f702
8 changes: 6 additions & 2 deletions script/Deploy.s.sol
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,22 @@ import { DeployBase } from "./DeployBase.sol";
contract Deploy is Script, DeployBase {
// NOTE: Ensure this is the correct Registrar testnet/mainnet address.
address internal constant _REGISTRAR = 0x0000000000000000000000000000000000000000;
address internal constant _PORTAL = 0x0000000000000000000000000000000000000000;

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, _PORTAL, migrationAdmin_);

vm.stopBroadcast();

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

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 portal_ The address of the Portal 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 portal_, address migrationAdmin_) public virtual returns (address implementation_, address proxy_) {
implementation_ = address(new MToken(registrar_, portal_, 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 M0", "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
43 changes: 27 additions & 16 deletions test/Deploy.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,42 @@

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));
// 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, _PORTAL, _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);
}
}
18 changes: 13 additions & 5 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), _portal, _migrationAdmin);
_mToken = MTokenHarness(address(new ERC1967Proxy(address(_implementation), abi.encodeCall(IMToken.initialize, ()))));

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

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

function test_constructor_zeroPortal() external {
vm.expectRevert(IMToken.ZeroPortal.selector);
new MTokenHarness(address(_registrar));
new MTokenHarness(address(_registrar), address(0), _migrationAdmin);
}

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