From 3e9989e46715f6700ae1cf449652cc5e674d45fd Mon Sep 17 00:00:00 2001 From: Michael De Luca Date: Tue, 14 Jan 2025 13:31:04 -0500 Subject: [PATCH] feat: post-deploy v2 prep - solc 0.8.26 - latest common - latest forge-std - registrar as constructor arg - excess destination as constructor arg - constants made public - token name changed - `IsApprovedEarner` and `NotApprovedEarner` errors have account as parameter - basic Migrator contract introduced - version bump - use more from common - more test coverage - fixed scripts to allow for generic deploy and mainnet upgrade --- Makefile | 7 +- script/Deploy.s.sol | 75 +++++ script/DeployBase.sol | 72 +++-- ...ction.s.sol => DeployUpgradeMainnet.s.sol} | 47 +-- src/MigratorV1.sol | 10 +- src/WrappedMToken.sol | 37 +-- src/interfaces/IWrappedMToken.sol | 15 - test/integration/Deploy.t.sol | 15 +- test/integration/Protocol.t.sol | 2 +- test/integration/Upgrade.t.sol | 55 ++++ test/unit/Migration.t.sol | 19 +- test/unit/WrappedMToken.t.sol | 296 +++++++++++++----- 12 files changed, 454 insertions(+), 196 deletions(-) create mode 100644 script/Deploy.s.sol rename script/{DeployProduction.s.sol => DeployUpgradeMainnet.s.sol} (52%) create mode 100644 test/integration/Upgrade.t.sol diff --git a/Makefile b/Makefile index f9fef41..5ebaf68 100644 --- a/Makefile +++ b/Makefile @@ -9,10 +9,13 @@ update:; forge update # Deployment helpers deploy: - FOUNDRY_PROFILE=production forge script script/DeployProduction.s.sol --skip src --skip test --rpc-url mainnet --slow --broadcast -vvv --verify + FOUNDRY_PROFILE=production MAINNET_RPC_URL=$(DEPLOY_RPC_URL) forge script script/Deploy.s.sol --skip src --skip test --slow --broadcast -vvv --verify deploy-local: - FOUNDRY_PROFILE=production forge script script/DeployProduction.s.sol --skip src --skip test --rpc-url localhost --slow --broadcast -vvv + FOUNDRY_PROFILE=production forge script script/Deploy.s.sol --skip src --skip test --rpc-url localhost --slow --broadcast -vvv + +deploy-upgrade: + FOUNDRY_PROFILE=production forge script script/DeployUpgradeMainnet.s.sol --skip src --skip test --rpc-url mainnet --slow --broadcast -vvv --verify # Run slither slither : diff --git a/script/Deploy.s.sol b/script/Deploy.s.sol new file mode 100644 index 0000000..0bdf585 --- /dev/null +++ b/script/Deploy.s.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity 0.8.26; + +import { Script, console2 } from "../lib/forge-std/src/Script.sol"; + +import { DeployBase } from "./DeployBase.sol"; + +contract DeployProduction is Script, DeployBase { + error DeployerMismatch(address expected, address actual); + + error DeployerNonceTooHigh(); + + error UnexpectedDeployerNonce(); + + error CurrentNonceMismatch(uint64 expected, uint64 actual); + + error ExpectedProxyMismatch(address expected, address actual); + + error ResultingProxyMismatch(address expected, address actual); + + function run() external { + address deployer_ = vm.rememberKey(vm.envUint("PRIVATE_KEY")); + address expectedDeployer_ = vm.envAddress("DEPLOYER"); + + uint64 deployerProxyNonce_ = uint64(vm.envUint("DEPLOYER_PROXY_NONCE")); + + address registrar_ = vm.envAddress("REGISTRAR"); + address excessDestination_ = vm.envAddress("EXCESS_DESTINATION"); + address mToken_ = vm.envAddress("M_TOKEN"); + address migrationAdmin_ = vm.envAddress("MIGRATION_ADMIN"); + address expectedProxy_ = vm.envAddress("EXPECTED_PROXY"); + + console2.log("Deployer:", deployer_); + + if (deployer_ != expectedDeployer_) revert DeployerMismatch(expectedDeployer_, deployer_); + + uint64 currentNonce_ = vm.getNonce(deployer_); + + uint64 startNonce_ = currentNonce_; + address implementation_; + address proxy_; + + while (true) { + if (startNonce_ > deployerProxyNonce_) revert DeployerNonceTooHigh(); + + (implementation_, proxy_) = mockDeploy(deployer_, startNonce_); + + if (proxy_ == expectedProxy_) break; + + ++startNonce_; + } + + vm.startBroadcast(deployer_); + + // Burn nonces until to `currentNonce_ == startNonce_`. + while (currentNonce_ < startNonce_) { + payable(deployer_).transfer(0); + ++currentNonce_; + } + + if (currentNonce_ != vm.getNonce(deployer_)) revert CurrentNonceMismatch(currentNonce_, vm.getNonce(deployer_)); + + if (currentNonce_ != startNonce_) revert UnexpectedDeployerNonce(); + + (implementation_, proxy_) = deployUpgrade(mToken_, registrar_, excessDestination_, migrationAdmin_); + + vm.stopBroadcast(); + + console2.log("Wrapped M Implementation address:", implementation_); + console2.log("Migrator address:", proxy_); + + if (proxy_ != expectedProxy_) revert ResultingProxyMismatch(expectedProxy_, proxy_); + } +} diff --git a/script/DeployBase.sol b/script/DeployBase.sol index d69f55a..c4e1325 100644 --- a/script/DeployBase.sol +++ b/script/DeployBase.sol @@ -5,15 +5,16 @@ pragma solidity 0.8.26; import { ContractHelper } from "../lib/common/src/libs/ContractHelper.sol"; import { Proxy } from "../lib/common/src/Proxy.sol"; +import { MigratorV1 } from "../src/MigratorV1.sol"; import { WrappedMToken } from "../src/WrappedMToken.sol"; contract DeployBase { /** * @dev Deploys Wrapped M Token. - * @param mToken_ The address the M Token contract. - * @param registrar_ The address the Registrar contract. + * @param mToken_ The address of the M Token contract. + * @param registrar_ The address of the Registrar contract. * @param excessDestination_ The address of the excess destination. - * @param migrationAdmin_ The address the Migration Admin. + * @param migrationAdmin_ The address of the Migration Admin. * @return implementation_ The address of the deployed Wrapped M Token implementation. * @return proxy_ The address of the deployed Wrapped M Token proxy. */ @@ -23,39 +24,68 @@ contract DeployBase { address excessDestination_, address migrationAdmin_ ) public virtual returns (address implementation_, address proxy_) { - // Wrapped M token needs `mToken_`, `registrar_`, and `migrationAdmin_` addresses. + // Wrapped M token needs `mToken_`, `registrar_`, `excessDestination_`, and `migrationAdmin_` addresses. // Proxy needs `implementation_` addresses. implementation_ = address(new WrappedMToken(mToken_, registrar_, excessDestination_, migrationAdmin_)); proxy_ = address(new Proxy(implementation_)); } - function _getExpectedWrappedMTokenImplementation( - address deployer_, - uint256 deployerNonce_ - ) internal pure returns (address) { - return ContractHelper.getContractFrom(deployer_, deployerNonce_); + /** + * @dev Deploys Wrapped M Token components needed to upgrade an existing Wrapped M proxy. + * @param mToken_ The address of the M Token contract. + * @param registrar_ The address of the Registrar contract. + * @param excessDestination_ The address of the excess destination. + * @param migrationAdmin_ The address of the Migration Admin. + * @return implementation_ The address of the deployed Wrapped M Token implementation. + * @return migrator_ The address of the deployed Migrator. + */ + function deployUpgrade( + address mToken_, + address registrar_, + address excessDestination_, + address migrationAdmin_ + ) public virtual returns (address implementation_, address migrator_) { + // Wrapped M token needs `mToken_`, `registrar_`, `excessDestination_`, and `migrationAdmin_` addresses. + // Migrator needs `implementation_` addresses. + + implementation_ = address(new WrappedMToken(mToken_, registrar_, excessDestination_, migrationAdmin_)); + migrator_ = address(new MigratorV1(implementation_)); } - function getExpectedWrappedMTokenImplementation( + /** + * @dev Mock deploys Wrapped M Token, returning the would-be addresses. + * @param deployer_ The address of the deployer. + * @param deployerNonce_ The nonce of the deployer. + * @return implementation_ The address of the would-be Wrapped M Token implementation. + * @return proxy_ The address of the would-be Wrapped M Token proxy. + */ + function mockDeploy( address deployer_, uint256 deployerNonce_ - ) public pure virtual returns (address) { - return _getExpectedWrappedMTokenImplementation(deployer_, deployerNonce_); - } + ) public view virtual returns (address implementation_, address proxy_) { + // Wrapped M token needs `mToken_`, `registrar_`, `excessDestination_`, and `migrationAdmin_` addresses. + // Proxy needs `implementation_` addresses. - function _getExpectedWrappedMTokenProxy(address deployer_, uint256 deployerNonce_) internal pure returns (address) { - return ContractHelper.getContractFrom(deployer_, deployerNonce_ + 1); + implementation_ = ContractHelper.getContractFrom(deployer_, deployerNonce_); + proxy_ = ContractHelper.getContractFrom(deployer_, deployerNonce_ + 1); } - function getExpectedWrappedMTokenProxy( + /** + * @dev Mock deploys Wrapped M Token, returning the would-be addresses. + * @param deployer_ The address of the deployer. + * @param deployerNonce_ The nonce of the deployer. + * @return implementation_ The address of the would-be Wrapped M Token implementation. + * @return migrator_ The address of the would-be Migrator. + */ + function mockDeployUpgrade( address deployer_, uint256 deployerNonce_ - ) public pure virtual returns (address) { - return _getExpectedWrappedMTokenProxy(deployer_, deployerNonce_); - } + ) public view virtual returns (address implementation_, address migrator_) { + // Wrapped M token needs `mToken_`, `registrar_`, `excessDestination_`, and `migrationAdmin_` addresses. + // Migrator needs `implementation_` addresses. - function getDeployerNonceAfterProtocolDeployment(uint256 deployerNonce_) public pure virtual returns (uint256) { - return deployerNonce_ + 2; + implementation_ = ContractHelper.getContractFrom(deployer_, deployerNonce_); + migrator_ = ContractHelper.getContractFrom(deployer_, deployerNonce_ + 1); } } diff --git a/script/DeployProduction.s.sol b/script/DeployUpgradeMainnet.s.sol similarity index 52% rename from script/DeployProduction.s.sol rename to script/DeployUpgradeMainnet.s.sol index 468e1a1..060e563 100644 --- a/script/DeployProduction.s.sol +++ b/script/DeployUpgradeMainnet.s.sol @@ -6,7 +6,7 @@ import { Script, console2 } from "../lib/forge-std/src/Script.sol"; import { DeployBase } from "./DeployBase.sol"; -contract DeployProduction is Script, DeployBase { +contract DeployUpgradeMainnet is Script, DeployBase { error DeployerMismatch(address expected, address actual); error DeployerNonceTooHigh(); @@ -15,30 +15,28 @@ contract DeployProduction is Script, DeployBase { error CurrentNonceMismatch(uint64 expected, uint64 actual); - error ExpectedProxyMismatch(address expected, address actual); + error ResultingMigratorMismatch(address expected, address actual); - error ResultingProxyMismatch(address expected, address actual); - - // NOTE: Ensure this is the correct Registrar testnet/mainnet address. address internal constant _REGISTRAR = 0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c; - // NOTE: Ensure this is the correct Excess Destination testnet/mainnet address. + // NOTE: Ensure this is the correct Excess Destination mainnet address. address internal constant _EXCESS_DESTINATION = 0xd7298f620B0F752Cf41BD818a16C756d9dCAA34f; // Vault - // NOTE: Ensure this is the correct M Token testnet/mainnet address. address internal constant _M_TOKEN = 0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b; - // NOTE: Ensure this is the correct Migration Admin testnet/mainnet address. + // NOTE: Ensure this is the correct Migration Admin mainnet address. address internal constant _MIGRATION_ADMIN = 0x431169728D75bd02f4053435b87D15c8d1FB2C72; - // NOTE: Ensure this is the correct deployer testnet/mainnet to use. + address internal constant _PROXY = 0x437cc33344a0B27A429f795ff6B469C72698B291; // Mainnet address for the Proxy. + + // NOTE: Ensure this is the correct mainnet deployer to use. address internal constant _EXPECTED_DEPLOYER = 0xF2f1ACbe0BA726fEE8d75f3E32900526874740BB; - // NOTE: Ensure this is the correct nonce to use to deploy the Proxy on testnet/mainnet. - uint64 internal constant _DEPLOYER_PROXY_NONCE = 40; + // NOTE: Ensure this is the correct nonce to use to deploy the Migrator on mainnet. + uint64 internal constant _DEPLOYER_MIGRATOR_NONCE = 40; - // NOTE: Ensure this is the correct expected testnet/mainnet address for the Proxy. - address internal constant _EXPECTED_PROXY = 0x437cc33344a0B27A429f795ff6B469C72698B291; + // NOTE: Ensure this is the correct expected mainnet address for the Migrator. + address internal constant _EXPECTED_MIGRATOR = address(0); function run() external { address deployer_ = vm.rememberKey(vm.envUint("PRIVATE_KEY")); @@ -48,17 +46,24 @@ contract DeployProduction is Script, DeployBase { if (deployer_ != _EXPECTED_DEPLOYER) revert DeployerMismatch(_EXPECTED_DEPLOYER, deployer_); uint64 currentNonce_ = vm.getNonce(deployer_); - uint64 startNonce_ = _DEPLOYER_PROXY_NONCE - 1; - if (currentNonce_ >= startNonce_) revert DeployerNonceTooHigh(); + uint64 startNonce_ = currentNonce_; + address implementation_; + address migrator_; + + while (true) { + if (startNonce_ > _DEPLOYER_MIGRATOR_NONCE) revert DeployerNonceTooHigh(); - address expectedProxy_ = getExpectedWrappedMTokenProxy(deployer_, startNonce_); + (implementation_, migrator_) = mockDeployUpgrade(deployer_, startNonce_); - if (expectedProxy_ != _EXPECTED_PROXY) revert ExpectedProxyMismatch(_EXPECTED_PROXY, expectedProxy_); + if (migrator_ == _EXPECTED_MIGRATOR) break; + + ++startNonce_; + } vm.startBroadcast(deployer_); - // Burn nonces until to 1 before `_DEPLOYER_PROXY_NONCE` since implementation is deployed before proxy. + // Burn nonces until to `currentNonce_ == startNonce_`. while (currentNonce_ < startNonce_) { payable(deployer_).transfer(0); ++currentNonce_; @@ -68,13 +73,13 @@ contract DeployProduction is Script, DeployBase { if (currentNonce_ != startNonce_) revert UnexpectedDeployerNonce(); - (address implementation_, address proxy_) = deploy(_M_TOKEN, _REGISTRAR, _EXCESS_DESTINATION, _MIGRATION_ADMIN); + (implementation_, migrator_) = deployUpgrade(_M_TOKEN, _REGISTRAR, _EXCESS_DESTINATION, _MIGRATION_ADMIN); vm.stopBroadcast(); console2.log("Wrapped M Implementation address:", implementation_); - console2.log("Wrapped M Proxy address:", proxy_); + console2.log("Migrator address:", migrator_); - if (proxy_ != _EXPECTED_PROXY) revert ResultingProxyMismatch(_EXPECTED_PROXY, proxy_); + if (migrator_ != _EXPECTED_MIGRATOR) revert ResultingMigratorMismatch(_EXPECTED_MIGRATOR, migrator_); } } diff --git a/src/MigratorV1.sol b/src/MigratorV1.sol index b148337..74cef78 100644 --- a/src/MigratorV1.sol +++ b/src/MigratorV1.sol @@ -10,17 +10,17 @@ contract MigratorV1 { /// @dev Storage slot with the address of the current factory. `keccak256('eip1967.proxy.implementation') - 1`. uint256 private constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; - address public immutable implementationV2; + address public immutable newImplementation; - constructor(address implementationV2_) { - implementationV2 = implementationV2_; + constructor(address newImplementation_) { + newImplementation = newImplementation_; } fallback() external virtual { - address implementationV2_ = implementationV2; + address newImplementation_ = newImplementation; assembly { - sstore(_IMPLEMENTATION_SLOT, implementationV2_) + sstore(_IMPLEMENTATION_SLOT, newImplementation_) } } } diff --git a/src/WrappedMToken.sol b/src/WrappedMToken.sol index b118715..096231c 100644 --- a/src/WrappedMToken.sol +++ b/src/WrappedMToken.sol @@ -102,7 +102,7 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { address registrar_, address excessDestination_, address migrationAdmin_ - ) ERC20Extended("M by M^0 (wrapped)", "wM", 6) { + ) ERC20Extended("M (Wrapped) by M^0", "wM", 6) { if ((mToken = mToken_) == address(0)) revert ZeroMToken(); if ((registrar = registrar_) == address(0)) revert ZeroRegistrar(); if ((excessDestination = excessDestination_) == address(0)) revert ZeroExcessDestination(); @@ -185,31 +185,11 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { _startEarningFor(account_, _currentMIndex()); } - /// @inheritdoc IWrappedMToken - function startEarningFor(address[] calldata accounts_) external { - if (!isEarningEnabled()) revert EarningIsDisabled(); - - uint128 currentIndex_ = _currentMIndex(); - - for (uint256 index_; index_ < accounts_.length; ++index_) { - _startEarningFor(accounts_[index_], currentIndex_); - } - } - /// @inheritdoc IWrappedMToken function stopEarningFor(address account_) external { _stopEarningFor(account_, currentIndex()); } - /// @inheritdoc IWrappedMToken - function stopEarningFor(address[] calldata accounts_) external { - uint128 currentIndex_ = currentIndex(); - - for (uint256 index_; index_ < accounts_.length; ++index_) { - _stopEarningFor(accounts_[index_], currentIndex_); - } - } - /* ============ Temporary Admin Migration ============ */ /// @inheritdoc IWrappedMToken @@ -314,7 +294,7 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { */ function _mint(address recipient_, uint240 amount_) internal { _revertIfInsufficientAmount(amount_); - _revertIfZeroAccount(recipient_); + _revertIfInvalidRecipient(recipient_); if (_accounts[recipient_].isEarning) { uint128 currentIndex_ = currentIndex(); @@ -393,8 +373,9 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { // NOTE: Can be `unchecked` because the max amount of wrappable M is never greater than `type(uint240).max`. unchecked { _accounts[account_].balance += amount_; - _addTotalEarningSupply(amount_, currentIndex_); } + + _addTotalEarningSupply(amount_, currentIndex_); } /** @@ -412,8 +393,9 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { unchecked { accountInfo_.balance = balance_ - amount_; - _subtractTotalEarningSupply(amount_, currentIndex_); } + + _subtractTotalEarningSupply(amount_, currentIndex_); } /** @@ -467,8 +449,7 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { * @param currentIndex_ The current index. */ function _transfer(address sender_, address recipient_, uint240 amount_, uint128 currentIndex_) internal { - _revertIfZeroAccount(sender_); - _revertIfZeroAccount(recipient_); + _revertIfInvalidRecipient(recipient_); // Claims for both the sender and recipient are required before transferring since add an subtract functions // assume accounts' balances are up-to-date with the current index. @@ -762,8 +743,8 @@ contract WrappedMToken is IWrappedMToken, Migratable, ERC20Extended { * @dev Reverts if `account_` is address(0). * @param account_ Address of an account. */ - function _revertIfZeroAccount(address account_) internal pure { - if (account_ == address(0)) revert ZeroAccount(); + function _revertIfInvalidRecipient(address account_) internal pure { + if (account_ == address(0)) revert InvalidRecipient(account_); } /** diff --git a/src/interfaces/IWrappedMToken.sol b/src/interfaces/IWrappedMToken.sol index 236df83..a74ca47 100644 --- a/src/interfaces/IWrappedMToken.sol +++ b/src/interfaces/IWrappedMToken.sol @@ -84,9 +84,6 @@ interface IWrappedMToken is IMigratable, IERC20Extended { /// @notice Emitted when the non-governance migrate function is called by a account other than the migration admin. error UnauthorizedMigration(); - /// @notice Emitted in an account is 0x0. - error ZeroAccount(); - /// @notice Emitted in constructor if Excess Destination is 0x0. error ZeroExcessDestination(); @@ -156,24 +153,12 @@ interface IWrappedMToken is IMigratable, IERC20Extended { */ function startEarningFor(address account) external; - /** - * @notice Starts earning for multiple accounts if individually allowed by the Registrar. - * @param accounts The accounts to start earning for. - */ - function startEarningFor(address[] calldata accounts) external; - /** * @notice Stops earning for `account` if disallowed by the Registrar. * @param account The account to stop earning for. */ function stopEarningFor(address account) external; - /** - * @notice Stops earning for multiple accounts if individually disallowed by the Registrar. - * @param accounts The account to stop earning for. - */ - function stopEarningFor(address[] calldata accounts) external; - /* ============ Temporary Admin Migration ============ */ /** diff --git a/test/integration/Deploy.t.sol b/test/integration/Deploy.t.sol index c6c172e..ced7573 100644 --- a/test/integration/Deploy.t.sol +++ b/test/integration/Deploy.t.sol @@ -5,37 +5,40 @@ pragma solidity 0.8.26; import { Test } from "../../lib/forge-std/src/Test.sol"; import { IWrappedMToken } from "../../src/interfaces/IWrappedMToken.sol"; -import { IRegistrarLike } from "../../src/interfaces/IRegistrarLike.sol"; import { DeployBase } from "../../script/DeployBase.sol"; -contract Deploy is Test, DeployBase { +contract DeployTests is Test, DeployBase { address internal constant _REGISTRAR = 0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c; address internal constant _M_TOKEN = 0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b; address internal constant _MIGRATION_ADMIN = 0x431169728D75bd02f4053435b87D15c8d1FB2C72; address internal constant _EXCESS_DESTINATION = 0xd7298f620B0F752Cf41BD818a16C756d9dCAA34f; // Vault address internal constant _DEPLOYER = 0xF2f1ACbe0BA726fEE8d75f3E32900526874740BB; - uint256 internal constant _DEPLOYER_NONCE = 50; + + uint64 internal constant _DEPLOYER_NONCE = 50; function test_deploy() external { - vm.setNonce(_DEPLOYER, uint64(_DEPLOYER_NONCE)); + vm.setNonce(_DEPLOYER, _DEPLOYER_NONCE); + + (address expectedImplementation_, address expectedProxy_) = mockDeploy(_DEPLOYER, _DEPLOYER_NONCE); vm.startPrank(_DEPLOYER); (address implementation_, address proxy_) = deploy(_M_TOKEN, _REGISTRAR, _EXCESS_DESTINATION, _MIGRATION_ADMIN); vm.stopPrank(); // Wrapped M Token Implementation assertions - assertEq(implementation_, getExpectedWrappedMTokenImplementation(_DEPLOYER, _DEPLOYER_NONCE)); + assertEq(implementation_, expectedImplementation_); assertEq(IWrappedMToken(implementation_).migrationAdmin(), _MIGRATION_ADMIN); assertEq(IWrappedMToken(implementation_).mToken(), _M_TOKEN); assertEq(IWrappedMToken(implementation_).registrar(), _REGISTRAR); assertEq(IWrappedMToken(implementation_).excessDestination(), _EXCESS_DESTINATION); // Wrapped M Token Proxy assertions - assertEq(proxy_, getExpectedWrappedMTokenProxy(_DEPLOYER, _DEPLOYER_NONCE)); + assertEq(proxy_, expectedProxy_); assertEq(IWrappedMToken(proxy_).migrationAdmin(), _MIGRATION_ADMIN); assertEq(IWrappedMToken(proxy_).mToken(), _M_TOKEN); assertEq(IWrappedMToken(proxy_).registrar(), _REGISTRAR); assertEq(IWrappedMToken(proxy_).excessDestination(), _EXCESS_DESTINATION); + assertEq(IWrappedMToken(proxy_).implementation(), implementation_); } } diff --git a/test/integration/Protocol.t.sol b/test/integration/Protocol.t.sol index f8459e2..dffe6c6 100644 --- a/test/integration/Protocol.t.sol +++ b/test/integration/Protocol.t.sol @@ -53,7 +53,7 @@ contract ProtocolIntegrationTests is TestBase { assertEq(_wrappedMToken.EARNERS_LIST_NAME(), "earners"); assertEq(_wrappedMToken.CLAIM_OVERRIDE_RECIPIENT_KEY_PREFIX(), "wm_claim_override_recipient"); assertEq(_wrappedMToken.MIGRATOR_KEY_PREFIX(), "wm_migrator_v2"); - assertEq(_wrappedMToken.name(), "M by M^0 (wrapped)"); + assertEq(_wrappedMToken.name(), "M (Wrapped) by M^0"); assertEq(_wrappedMToken.symbol(), "wM"); assertEq(_wrappedMToken.decimals(), 6); } diff --git a/test/integration/Upgrade.t.sol b/test/integration/Upgrade.t.sol new file mode 100644 index 0000000..b8e1fd5 --- /dev/null +++ b/test/integration/Upgrade.t.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: UNLICENSED + +pragma solidity 0.8.26; + +import { Test } from "../../lib/forge-std/src/Test.sol"; + +import { IWrappedMToken } from "../../src/interfaces/IWrappedMToken.sol"; + +import { DeployBase } from "../../script/DeployBase.sol"; + +contract UpgradeTests is Test, DeployBase { + address internal constant _WRAPPED_M_TOKEN = 0x437cc33344a0B27A429f795ff6B469C72698B291; + address internal constant _REGISTRAR = 0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c; + address internal constant _M_TOKEN = 0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b; + address internal constant _MIGRATION_ADMIN = 0x431169728D75bd02f4053435b87D15c8d1FB2C72; + address internal constant _EXCESS_DESTINATION = 0xd7298f620B0F752Cf41BD818a16C756d9dCAA34f; // Vault + address internal constant _DEPLOYER = 0xF2f1ACbe0BA726fEE8d75f3E32900526874740BB; + + uint64 internal constant _DEPLOYER_NONCE = 50; + + function test_upgrade() external { + vm.setNonce(_DEPLOYER, _DEPLOYER_NONCE); + + (address expectedImplementation_, address expectedMigrator_) = mockDeployUpgrade(_DEPLOYER, _DEPLOYER_NONCE); + + vm.startPrank(_DEPLOYER); + (address implementation_, address migrator_) = deployUpgrade( + _M_TOKEN, + _REGISTRAR, + _EXCESS_DESTINATION, + _MIGRATION_ADMIN + ); + vm.stopPrank(); + + // Wrapped M Token Implementation assertions + assertEq(implementation_, expectedImplementation_); + assertEq(IWrappedMToken(implementation_).migrationAdmin(), _MIGRATION_ADMIN); + assertEq(IWrappedMToken(implementation_).mToken(), _M_TOKEN); + assertEq(IWrappedMToken(implementation_).registrar(), _REGISTRAR); + assertEq(IWrappedMToken(implementation_).excessDestination(), _EXCESS_DESTINATION); + + // Migrator assertions + assertEq(migrator_, expectedMigrator_); + + vm.prank(IWrappedMToken(_WRAPPED_M_TOKEN).migrationAdmin()); + IWrappedMToken(_WRAPPED_M_TOKEN).migrate(migrator_); + + // Wrapped M Token Proxy assertions + assertEq(IWrappedMToken(_WRAPPED_M_TOKEN).migrationAdmin(), _MIGRATION_ADMIN); + assertEq(IWrappedMToken(_WRAPPED_M_TOKEN).mToken(), _M_TOKEN); + assertEq(IWrappedMToken(_WRAPPED_M_TOKEN).registrar(), _REGISTRAR); + assertEq(IWrappedMToken(_WRAPPED_M_TOKEN).excessDestination(), _EXCESS_DESTINATION); + assertEq(IWrappedMToken(_WRAPPED_M_TOKEN).implementation(), implementation_); + } +} diff --git a/test/unit/Migration.t.sol b/test/unit/Migration.t.sol index 597f614..6d81210 100644 --- a/test/unit/Migration.t.sol +++ b/test/unit/Migration.t.sol @@ -8,6 +8,7 @@ import { Test } from "../../lib/forge-std/src/Test.sol"; import { IWrappedMToken } from "../../src/interfaces/IWrappedMToken.sol"; import { WrappedMToken } from "../../src/WrappedMToken.sol"; +import { MigratorV1 as Migrator } from "../../src/MigratorV1.sol"; import { MockRegistrar } from "./../utils/Mocks.sol"; @@ -17,24 +18,6 @@ contract WrappedMTokenV3 { } } -contract Migrator { - uint256 private constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc; - - address public immutable implementationV2; - - constructor(address implementation_) { - implementationV2 = implementation_; - } - - fallback() external virtual { - address implementation_ = implementationV2; - - assembly { - sstore(_IMPLEMENTATION_SLOT, implementation_) - } - } -} - contract MigrationTests is Test { bytes32 internal constant _MIGRATOR_KEY_PREFIX = "wm_migrator_v2"; diff --git a/test/unit/WrappedMToken.t.sol b/test/unit/WrappedMToken.t.sol index f233dd7..53823ab 100644 --- a/test/unit/WrappedMToken.t.sol +++ b/test/unit/WrappedMToken.t.sol @@ -16,9 +16,9 @@ import { IWrappedMToken } from "../../src/interfaces/IWrappedMToken.sol"; import { MockM, MockRegistrar } from "../utils/Mocks.sol"; import { WrappedMTokenHarness } from "../utils/WrappedMTokenHarness.sol"; -// TODO: Test for `totalAccruedYield()`. // TODO: All operations involving earners should include demonstration of accrued yield being added to their balance. // TODO: Add relevant unit tests while earning enabled/disabled. +// TODO: Remove unneeded _wrappedMToken.enableEarning. contract WrappedMTokenTests is Test { uint56 internal constant _EXP_SCALED_ONE = IndexingMath.EXP_SCALED_ONE; @@ -62,13 +62,21 @@ contract WrappedMTokenTests is Test { _mToken.setCurrentIndex(_currentIndex = 1_100000068703); } + /* ============ constants ============ */ + function test_constants() external view { + assertEq(_wrappedMToken.EARNERS_LIST_IGNORED_KEY(), "earners_list_ignored"); + assertEq(_wrappedMToken.EARNERS_LIST_NAME(), _EARNERS_LIST_NAME); + assertEq(_wrappedMToken.CLAIM_OVERRIDE_RECIPIENT_KEY_PREFIX(), _CLAIM_OVERRIDE_RECIPIENT_KEY_PREFIX); + assertEq(_wrappedMToken.MIGRATOR_KEY_PREFIX(), "wm_migrator_v2"); + } + /* ============ constructor ============ */ function test_constructor() external view { assertEq(_wrappedMToken.migrationAdmin(), _migrationAdmin); assertEq(_wrappedMToken.mToken(), address(_mToken)); assertEq(_wrappedMToken.registrar(), address(_registrar)); assertEq(_wrappedMToken.excessDestination(), _excessDestination); - assertEq(_wrappedMToken.name(), "M by M^0 (wrapped)"); + assertEq(_wrappedMToken.name(), "M (Wrapped) by M^0"); assertEq(_wrappedMToken.symbol(), "wM"); assertEq(_wrappedMToken.decimals(), 6); assertEq(_wrappedMToken.implementation(), address(_implementation)); @@ -109,7 +117,7 @@ contract WrappedMTokenTests is Test { function test_wrap_invalidRecipient() external { _mToken.setBalanceOf(_alice, 1_000); - vm.expectRevert(IWrappedMToken.ZeroAccount.selector); + vm.expectRevert(abi.encodeWithSelector(IERC20Extended.InvalidRecipient.selector, address(0))); vm.prank(_alice); _wrappedMToken.wrap(address(0), 1_000); @@ -634,7 +642,7 @@ contract WrappedMTokenTests is Test { function test_transfer_invalidRecipient() external { _wrappedMToken.setAccountOf(_alice, 1_000); - vm.expectRevert(IWrappedMToken.ZeroAccount.selector); + vm.expectRevert(abi.encodeWithSelector(IERC20Extended.InvalidRecipient.selector, address(0))); vm.prank(_alice); _wrappedMToken.transfer(address(0), 1_000); @@ -1040,46 +1048,6 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.totalEarningSupply(), balance_); } - /* ============ startEarningFor batch ============ */ - function test_startEarningFor_batch_earningIsDisabled() external { - vm.expectRevert(IWrappedMToken.EarningIsDisabled.selector); - _wrappedMToken.startEarningFor(new address[](2)); - } - - function test_startEarningFor_batch_notApprovedEarner() external { - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); - _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); - - _wrappedMToken.enableEarning(); - - address[] memory accounts_ = new address[](2); - accounts_[0] = _alice; - accounts_[1] = _bob; - - vm.expectRevert(abi.encodeWithSelector(IWrappedMToken.NotApprovedEarner.selector, _bob)); - _wrappedMToken.startEarningFor(accounts_); - } - - function test_startEarningFor_batch() external { - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); - _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); - _registrar.setListContains(_EARNERS_LIST_NAME, _bob, true); - - _wrappedMToken.enableEarning(); - - address[] memory accounts_ = new address[](2); - accounts_[0] = _alice; - accounts_[1] = _bob; - - vm.expectEmit(); - emit IWrappedMToken.StartedEarning(_alice); - - vm.expectEmit(); - emit IWrappedMToken.StartedEarning(_bob); - - _wrappedMToken.startEarningFor(accounts_); - } - /* ============ stopEarningFor ============ */ function test_stopEarningFor_isApprovedEarner() external { _registrar.setListContains(_EARNERS_LIST_NAME, _alice, true); @@ -1139,39 +1107,6 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.totalEarningSupply(), 0); } - /* ============ stopEarningFor batch ============ */ - function test_stopEarningFor_batch_isApprovedEarner() external { - _registrar.setListContains(_EARNERS_LIST_NAME, _bob, true); - - address[] memory accounts_ = new address[](2); - accounts_[0] = _alice; - accounts_[1] = _bob; - - vm.expectRevert(abi.encodeWithSelector(IWrappedMToken.IsApprovedEarner.selector, _bob)); - _wrappedMToken.stopEarningFor(accounts_); - } - - function test_stopEarningFor_batch() external { - _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); - - _wrappedMToken.enableEarning(); - - _wrappedMToken.setAccountOf(_alice, 0, _currentIndex); - _wrappedMToken.setAccountOf(_bob, 0, _currentIndex); - - address[] memory accounts_ = new address[](2); - accounts_[0] = _alice; - accounts_[1] = _bob; - - vm.expectEmit(); - emit IWrappedMToken.StoppedEarning(_alice); - - vm.expectEmit(); - emit IWrappedMToken.StoppedEarning(_bob); - - _wrappedMToken.stopEarningFor(accounts_); - } - /* ============ enableEarning ============ */ function test_enableEarning_notApprovedEarner() external { vm.expectRevert(abi.encodeWithSelector(IWrappedMToken.NotApprovedEarner.selector, address(_wrappedMToken))); @@ -1241,6 +1176,10 @@ contract WrappedMTokenTests is Test { /* ============ balanceOf ============ */ function test_balanceOf_nonEarner() external { + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); + + _wrappedMToken.enableEarning(); + _wrappedMToken.setAccountOf(_alice, 500); assertEq(_wrappedMToken.balanceOf(_alice), 500); @@ -1248,6 +1187,10 @@ contract WrappedMTokenTests is Test { _wrappedMToken.setAccountOf(_alice, 1_000); assertEq(_wrappedMToken.balanceOf(_alice), 1_000); + + _mToken.setCurrentIndex(2 * _currentIndex); + + assertEq(_wrappedMToken.balanceOf(_alice), 1_000); } function test_balanceOf_earner() external { @@ -1259,13 +1202,155 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.balanceOf(_alice), 500); - _wrappedMToken.setAccountOf(_alice, 1_000); + _wrappedMToken.setAccountOf(_alice, 1_000, _EXP_SCALED_ONE); assertEq(_wrappedMToken.balanceOf(_alice), 1_000); - _wrappedMToken.setLastIndexOf(_alice, 2 * _EXP_SCALED_ONE); + _mToken.setCurrentIndex(2 * _currentIndex); assertEq(_wrappedMToken.balanceOf(_alice), 1_000); + + _wrappedMToken.setAccountOf(_alice, 1_000, 3 * _currentIndex); + + assertEq(_wrappedMToken.balanceOf(_alice), 1_000); + } + + /* ============ balanceWithYieldOf ============ */ + function test_balanceWithYieldOf_nonEarner() external { + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); + + _wrappedMToken.enableEarning(); + + _wrappedMToken.setAccountOf(_alice, 500); + + assertEq(_wrappedMToken.balanceWithYieldOf(_alice), 500); + + _wrappedMToken.setAccountOf(_alice, 1_000); + + assertEq(_wrappedMToken.balanceWithYieldOf(_alice), 1_000); + + _mToken.setCurrentIndex(2 * _currentIndex); + + assertEq(_wrappedMToken.balanceWithYieldOf(_alice), 1_000); + } + + function test_balanceWithYieldOf_earner() external { + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); + + _wrappedMToken.enableEarning(); + + _wrappedMToken.setAccountOf(_alice, 500, _EXP_SCALED_ONE); + + assertEq(_wrappedMToken.balanceWithYieldOf(_alice), 550); + + _wrappedMToken.setAccountOf(_alice, 1_000, _EXP_SCALED_ONE); + + assertEq(_wrappedMToken.balanceWithYieldOf(_alice), 1_100); + + _mToken.setCurrentIndex(2 * _currentIndex); + + assertEq(_wrappedMToken.balanceWithYieldOf(_alice), 2_200); + + _wrappedMToken.setAccountOf(_alice, 1_000, 3 * _currentIndex); + + assertEq(_wrappedMToken.balanceWithYieldOf(_alice), 1_000); + } + + /* ============ accruedYieldOf ============ */ + function test_accruedYieldOf_nonEarner() external { + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); + + _wrappedMToken.enableEarning(); + + _wrappedMToken.setAccountOf(_alice, 500); + + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); + + _wrappedMToken.setAccountOf(_alice, 1_000); + + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); + + _mToken.setCurrentIndex(2 * _currentIndex); + + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); + } + + function test_accruedYieldOf_earner() external { + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); + + _wrappedMToken.enableEarning(); + + _wrappedMToken.setAccountOf(_alice, 500, _EXP_SCALED_ONE); + + assertEq(_wrappedMToken.accruedYieldOf(_alice), 50); + + _wrappedMToken.setAccountOf(_alice, 1_000, _EXP_SCALED_ONE); + + assertEq(_wrappedMToken.accruedYieldOf(_alice), 100); + + _mToken.setCurrentIndex(2 * _currentIndex); + + assertEq(_wrappedMToken.accruedYieldOf(_alice), 1_200); + + _wrappedMToken.setAccountOf(_alice, 1_000, 3 * _currentIndex); + + assertEq(_wrappedMToken.accruedYieldOf(_alice), 0); + } + + /* ============ lastIndexOf ============ */ + function test_lastIndexOf() external { + _wrappedMToken.setAccountOf(_alice, 0, _EXP_SCALED_ONE); + + assertEq(_wrappedMToken.lastIndexOf(_alice), _EXP_SCALED_ONE); + + _wrappedMToken.setAccountOf(_alice, 0, 2 * _EXP_SCALED_ONE); + + assertEq(_wrappedMToken.lastIndexOf(_alice), 2 * _EXP_SCALED_ONE); + } + + /* ============ isEarning ============ */ + function test_isEarning() external { + _wrappedMToken.setAccountOf(_alice, 0); + + assertFalse(_wrappedMToken.isEarning(_alice)); + + _wrappedMToken.setAccountOf(_alice, 0, _EXP_SCALED_ONE); + + assertTrue(_wrappedMToken.isEarning(_alice)); + } + + /* ============ isEarningEnabled ============ */ + function test_isEarningEnabled() external { + assertFalse(_wrappedMToken.isEarningEnabled()); + + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); + + _wrappedMToken.enableEarning(); + + assertTrue(_wrappedMToken.isEarningEnabled()); + + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), false); + + _wrappedMToken.disableEarning(); + + assertFalse(_wrappedMToken.isEarningEnabled()); + } + + /* ============ wasEarningEnabled ============ */ + function test_wasEarningEnabled() external { + assertFalse(_wrappedMToken.wasEarningEnabled()); + + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); + + _wrappedMToken.enableEarning(); + + assertTrue(_wrappedMToken.wasEarningEnabled()); + + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), false); + + _wrappedMToken.disableEarning(); + + assertTrue(_wrappedMToken.wasEarningEnabled()); } /* ============ claimOverrideRecipientFor ============ */ @@ -1346,6 +1431,59 @@ contract WrappedMTokenTests is Test { assertEq(_wrappedMToken.currentIndex(), 3 * _EXP_SCALED_ONE); } + /* ============ excess ============ */ + function test_excess() external { + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); + + _wrappedMToken.enableEarning(); + + assertEq(_wrappedMToken.excess(), 0); + + _wrappedMToken.setTotalNonEarningSupply(1_000); + _wrappedMToken.setPrincipalOfTotalEarningSupply(1_000); + _wrappedMToken.setTotalEarningSupply(1_000); + + _mToken.setBalanceOf(address(_wrappedMToken), 2_100); + + assertEq(_wrappedMToken.excess(), 0); + + _mToken.setBalanceOf(address(_wrappedMToken), 2_101); + + assertEq(_wrappedMToken.excess(), 0); + + _mToken.setBalanceOf(address(_wrappedMToken), 2_102); + + assertEq(_wrappedMToken.excess(), 1); + + _mToken.setBalanceOf(address(_wrappedMToken), 3_102); + + assertEq(_wrappedMToken.excess(), 1_001); + } + + /* ============ totalAccruedYield ============ */ + function test_totalAccruedYield() external { + _registrar.setListContains(_EARNERS_LIST_NAME, address(_wrappedMToken), true); + + _wrappedMToken.enableEarning(); + + _wrappedMToken.setPrincipalOfTotalEarningSupply(909); + _wrappedMToken.setTotalEarningSupply(1_000); + + assertEq(_wrappedMToken.totalAccruedYield(), 0); + + _wrappedMToken.setPrincipalOfTotalEarningSupply(1_000); + + assertEq(_wrappedMToken.totalAccruedYield(), 100); + + _wrappedMToken.setTotalEarningSupply(900); + + assertEq(_wrappedMToken.totalAccruedYield(), 200); + + _mToken.setCurrentIndex(_currentIndex = 1_210000000000); + + assertEq(_wrappedMToken.totalAccruedYield(), 310); + } + /* ============ utils ============ */ function _getPrincipalAmountRoundedDown(uint240 presentAmount_, uint128 index_) internal pure returns (uint112) { return IndexingMath.divide240By128Down(presentAmount_, index_);