Skip to content

Commit c8cf27c

Browse files
committed
feat: add upgradability
1 parent aa9f90b commit c8cf27c

File tree

9 files changed

+208
-37
lines changed

9 files changed

+208
-37
lines changed

script/Deploy.s.sol

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,15 +12,18 @@ contract Deploy is Script, DeployBase {
1212

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

1617
console2.log("Deployer:", deployer_);
18+
console2.log("Migration Admin:", migrationAdmin_);
1719

1820
vm.startBroadcast(deployer_);
1921

20-
address mToken_ = deploy(_REGISTRAR);
22+
(address implementation_, address proxy_) = deploy(_REGISTRAR, migrationAdmin_);
2123

2224
vm.stopBroadcast();
2325

24-
console2.log("M Token address:", mToken_);
26+
console2.log("M Token Implementation address:", implementation_);
27+
console2.log("M Token Proxy address:", proxy_);
2528
}
2629
}

script/DeployBase.sol

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,30 +2,50 @@
22

33
pragma solidity 0.8.26;
44

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

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

910
contract DeployBase {
1011
/**
1112
* @dev Deploys the M Token contract.
12-
* @param registrar_ The address of the Registrar contract.
13-
* @return mToken_ The address of the deployed M Token contract.
13+
* @param registrar_ The address of the Registrar contract.
14+
* @param migrationAdmin_ The address of a migration admin.
15+
* @return implementation_ The address of the deployed M Token implementation.
16+
* @return proxy_ The address of the deployed M Token proxy.
1417
*/
15-
function deploy(address registrar_) public virtual returns (address mToken_) {
16-
// M token needs `registrar_` addresses.
17-
return address(new MToken(registrar_));
18+
function deploy(address registrar_, address migrationAdmin_) public virtual returns (address implementation_, address proxy_) {
19+
implementation_ = address(new MToken(registrar_, migrationAdmin_));
20+
proxy_ = address(new ERC1967Proxy(implementation_, abi.encodeCall(MToken.initialize, ())));
1821
}
1922

20-
function _getExpectedMToken(address deployer_, uint256 deployerNonce_) internal pure returns (address) {
23+
function _getExpectedMTokenImplementation(
24+
address deployer_,
25+
uint256 deployerNonce_
26+
) internal pure returns (address) {
2127
return ContractHelper.getContractFrom(deployer_, deployerNonce_);
2228
}
2329

24-
function getExpectedMToken(address deployer_, uint256 deployerNonce_) public pure virtual returns (address) {
25-
return _getExpectedMToken(deployer_, deployerNonce_);
30+
function getExpectedMTokenImplementation(
31+
address deployer_,
32+
uint256 deployerNonce_
33+
) public pure virtual returns (address) {
34+
return _getExpectedMTokenImplementation(deployer_, deployerNonce_);
35+
}
36+
37+
function _getExpectedMTokenProxy(address deployer_, uint256 deployerNonce_) internal pure returns (address) {
38+
return ContractHelper.getContractFrom(deployer_, deployerNonce_ + 1);
39+
}
40+
41+
function getExpectedMTokenProxy(
42+
address deployer_,
43+
uint256 deployerNonce_
44+
) public pure virtual returns (address) {
45+
return _getExpectedMTokenProxy(deployer_, deployerNonce_);
2646
}
2747

2848
function getDeployerNonceAfterMTokenDeployment(uint256 deployerNonce_) public pure virtual returns (uint256) {
29-
return deployerNonce_ + 1;
49+
return deployerNonce_ + 2;
3050
}
3151
}

src/MToken.sol

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ pragma solidity 0.8.26;
44

55
import { ERC20Extended } from "../lib/common/src/ERC20Extended.sol";
66
import { UIntMath } from "../lib/common/src/libs/UIntMath.sol";
7+
import { Migratable } from "../lib/common/src/Migratable.sol";
78

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

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

2627
/**
@@ -41,6 +42,9 @@ contract MToken is IMToken, ContinuousIndexing, ERC20Extended {
4142
/// @inheritdoc IMToken
4243
address public immutable registrar;
4344

45+
/// @inheritdoc IMToken
46+
address public immutable migrationAdmin;
47+
4448
/// @inheritdoc IMToken
4549
uint240 public totalNonEarningSupply;
4650

@@ -62,11 +66,21 @@ contract MToken is IMToken, ContinuousIndexing, ERC20Extended {
6266

6367
/**
6468
* @notice Constructs the M Token contract.
69+
* @dev Sets immutable storage.
6570
* @param registrar_ The address of the Registrar contract.
71+
* @param migrationAdmin_ The address of a migration admin.
6672
*/
67-
constructor(address registrar_) ContinuousIndexing() ERC20Extended("M by M^0", "M", 6) {
73+
constructor(address registrar_, address migrationAdmin_) ContinuousIndexing() ERC20Extended("M by M^0", "M", 6) {
6874
if ((registrar = registrar_) == address(0)) revert ZeroRegistrar();
6975
if ((portal = RegistrarReader.getPortal(registrar_)) == address(0)) revert ZeroPortal();
76+
if ((migrationAdmin = migrationAdmin_) == address(0)) revert ZeroMigrationAdmin();
77+
}
78+
79+
/* ============ Initializer ============ */
80+
81+
/// @inheritdoc IMToken
82+
function initialize() external initializer {
83+
_initialize();
7084
}
7185

7286
/* ============ Interactive Functions ============ */
@@ -112,6 +126,15 @@ contract MToken is IMToken, ContinuousIndexing, ERC20Extended {
112126
_stopEarning(account_);
113127
}
114128

129+
/**
130+
* @dev Performs the contract migration by calling `migrator_`.
131+
* @param migrator_ The address of a migrator contract.
132+
*/
133+
function migrate(address migrator_) external {
134+
if (msg.sender != migrationAdmin) revert UnauthorizedMigration();
135+
_migrate(migrator_);
136+
}
137+
115138
/* ============ View/Pure Functions ============ */
116139

117140
/// @inheritdoc IMToken
@@ -438,4 +461,10 @@ contract MToken is IMToken, ContinuousIndexing, ERC20Extended {
438461
function _revertIfNotPortal() internal view {
439462
if (msg.sender != portal) revert NotPortal();
440463
}
464+
465+
/// @inheritdoc Migratable
466+
function _getMigrator() internal pure override returns (address migrator_) {
467+
// NOTE: in this version only the admin-controlled migration via `migrate()` function is supported
468+
return address(0);
469+
}
441470
}

src/abstract/ContinuousIndexing.sol

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
pragma solidity 0.8.26;
44

5+
import { Initializable } from "../../lib/openzeppelin-contracts/contracts/proxy/utils/Initializable.sol";
6+
57
import { IContinuousIndexing } from "../interfaces/IContinuousIndexing.sol";
68

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

1618
/// @inheritdoc IContinuousIndexing
@@ -19,10 +21,10 @@ abstract contract ContinuousIndexing is IContinuousIndexing {
1921
/// @inheritdoc IContinuousIndexing
2022
uint40 public latestUpdateTimestamp;
2123

22-
/* ============ Constructor ============ */
24+
/* ============ Initializer ============ */
2325

24-
/// @notice Constructs the ContinuousIndexing contract.
25-
constructor() {
26+
/// @notice Initializes Proxy's storage.
27+
function _initialize() internal onlyInitializing {
2628
latestIndex = ContinuousIndexingMath.EXP_SCALED_ONE;
2729
latestUpdateTimestamp = uint40(block.timestamp);
2830
}

src/interfaces/IMToken.sol

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,12 +50,18 @@ interface IMToken is IContinuousIndexing, IERC20Extended {
5050
/// @notice Emitted when principal of total supply (earning and non-earning) will overflow a `type(uint112).max`.
5151
error OverflowsPrincipalOfTotalSupply();
5252

53+
/// @notice Emitted when the migrate function is called by a account other than the migration admin.
54+
error UnauthorizedMigration();
55+
5356
/// @notice Emitted in constructor if the Portal address in the Registrar is 0x0.
5457
error ZeroPortal();
5558

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

62+
/// @notice Emitted in constructor if Migration Admin is 0x0.
63+
error ZeroMigrationAdmin();
64+
5965
/* ============ Interactive Functions ============ */
6066

6167
/**
@@ -102,6 +108,15 @@ interface IMToken is IContinuousIndexing, IERC20Extended {
102108
*/
103109
function stopEarning(address account) external;
104110

111+
/// @notice Initializes the Proxy's storage.
112+
function initialize() external;
113+
114+
/**
115+
* @notice Performs an arbitrarily defined migration.
116+
* @param migrator The address of a migrator contract.
117+
*/
118+
function migrate(address migrator) external;
119+
105120
/* ============ View/Pure Functions ============ */
106121

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

128+
/// @notice The account that can call the `migrate(address migrator)` function.
129+
function migrationAdmin() external view returns (address migrationAdmin);
130+
113131
/**
114132
* @notice The principal of an earner M token balance.
115133
* @param account The account to get the principal balance of.

test/Deploy.t.sol

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,31 +2,48 @@
22

33
pragma solidity 0.8.26;
44

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

77
import { IMToken } from "../src/interfaces/IMToken.sol";
8+
import { IRegistrar } from "../src/interfaces/IRegistrar.sol";
89

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

11-
import { MockRegistrar } from "./utils/Mocks.sol";
12-
1312
contract Deploy is Test, DeployBase {
14-
MockRegistrar internal _registrar;
13+
address internal constant _EXPECTED_PROXY = 0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b;
14+
address internal constant _DEPLOYER = 0xF2f1ACbe0BA726fEE8d75f3E32900526874740BB;
1515

16-
address internal _portal = makeAddr("portal");
16+
address internal immutable _MIGRATION_ADMIN = makeAddr("migration admin");
17+
address internal immutable _REGISTRAR = makeAddr("registrar");
18+
address internal immutable _PORTAL = makeAddr("portal");
1719

18-
function setUp() external {
19-
_registrar = new MockRegistrar();
20-
_registrar.setPortal(_portal);
21-
}
20+
uint64 internal constant _DEPLOYER_PROXY_NONCE = 8;
2221

2322
function test_deploy() external {
24-
address mToken_ = deploy(address(_registrar));
25-
26-
assertEq(mToken_, getExpectedMToken(address(this), 2));
27-
28-
// MToken assertions
29-
assertEq(IMToken(mToken_).portal(), _portal);
30-
assertEq(IMToken(mToken_).registrar(), address(_registrar));
23+
vm.mockCall(
24+
_REGISTRAR,
25+
abi.encodeWithSelector(IRegistrar.portal.selector),
26+
abi.encode(_PORTAL)
27+
);
28+
29+
// Set nonce to 1 before `_DEPLOYER_PROXY_NONCE` since implementation is deployed before proxy.
30+
vm.setNonce(_DEPLOYER, _DEPLOYER_PROXY_NONCE - 1);
31+
32+
vm.startPrank(_DEPLOYER);
33+
(address implementation_, address proxy_) = deploy(_REGISTRAR, _MIGRATION_ADMIN);
34+
vm.stopPrank();
35+
36+
// M Token Implementation assertions
37+
assertEq(implementation_, getExpectedMTokenImplementation(_DEPLOYER, 7));
38+
assertEq(IMToken(implementation_).migrationAdmin(), _MIGRATION_ADMIN);
39+
assertEq(IMToken(implementation_).registrar(), _REGISTRAR);
40+
assertEq(IMToken(implementation_).portal(), _PORTAL);
41+
42+
// M Token Proxy assertions
43+
assertEq(proxy_, getExpectedMTokenProxy(_DEPLOYER, 7));
44+
assertEq(proxy_, _EXPECTED_PROXY);
45+
assertEq(IMToken(proxy_).migrationAdmin(), _MIGRATION_ADMIN);
46+
assertEq(IMToken(proxy_).registrar(), _REGISTRAR);
47+
assertEq(IMToken(proxy_).portal(), _PORTAL);
3148
}
3249
}

test/MToken.t.sol

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ pragma solidity 0.8.26;
44

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

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

2728
address internal _portal = makeAddr("portal");
2829

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

3133
uint256 internal _start = vm.getBlockTimestamp();
3234

3335
uint128 internal _expectedCurrentIndex;
3436

3537
MockRegistrar internal _registrar;
38+
39+
MTokenHarness internal _implementation;
3640
MTokenHarness internal _mToken;
3741

3842
function setUp() external {
3943
_registrar = new MockRegistrar();
4044
_registrar.setPortal(_portal);
4145

42-
_mToken = new MTokenHarness(address(_registrar));
46+
_implementation = new MTokenHarness(address(_registrar), _migrationAdmin);
47+
_mToken = MTokenHarness(address(new ERC1967Proxy(address(_implementation), abi.encodeCall(IMToken.initialize, ()))));
4348

4449
_mToken.setLatestIndex(_expectedCurrentIndex = 1_100000068703);
4550
}
@@ -55,14 +60,19 @@ contract MTokenTests is TestUtils {
5560
/* ============ constructor ============ */
5661
function test_constructor_zeroRegistrar() external {
5762
vm.expectRevert(IMToken.ZeroRegistrar.selector);
58-
new MTokenHarness(address(0));
63+
new MTokenHarness(address(0), _migrationAdmin);
64+
}
65+
66+
function test_constructor_zeroMigrationAdmin() external {
67+
vm.expectRevert(IMToken.ZeroMigrationAdmin.selector);
68+
new MTokenHarness(address(_registrar), address(0));
5969
}
6070

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

6474
vm.expectRevert(IMToken.ZeroPortal.selector);
65-
new MTokenHarness(address(_registrar));
75+
new MTokenHarness(address(_registrar), _migrationAdmin);
6676
}
6777

6878
/* ============ mint ============ */

0 commit comments

Comments
 (0)