diff --git a/.gitmodules b/.gitmodules index 2251cfc..4dae5ce 100644 --- a/.gitmodules +++ b/.gitmodules @@ -17,3 +17,6 @@ path = lib/example-native-token-transfers url = git@github.com:wormhole-foundation/example-native-token-transfers.git branch = main +[submodule "lib/wrapped-m-token"] + path = lib/wrapped-m-token + url = https://github.com/m0-foundation/wrapped-m-token diff --git a/lib/common b/lib/common new file mode 160000 index 0000000..4c88b3f --- /dev/null +++ b/lib/common @@ -0,0 +1 @@ +Subproject commit 4c88b3f012beda49cd29e25642c123f6a2e97616 diff --git a/lib/example-native-token-transfers b/lib/example-native-token-transfers new file mode 160000 index 0000000..5d6ca4d --- /dev/null +++ b/lib/example-native-token-transfers @@ -0,0 +1 @@ +Subproject commit 5d6ca4dabcf268814ffaabe740d9edc3351a545e diff --git a/lib/forge-std b/lib/forge-std new file mode 160000 index 0000000..8f24d6b --- /dev/null +++ b/lib/forge-std @@ -0,0 +1 @@ +Subproject commit 8f24d6b04c92975e0795b5868aa0d783251cdeaa diff --git a/lib/protocol b/lib/protocol new file mode 160000 index 0000000..2e42f85 --- /dev/null +++ b/lib/protocol @@ -0,0 +1 @@ +Subproject commit 2e42f856715416cab4540a7b574a0bda61962599 diff --git a/lib/ttg b/lib/ttg new file mode 160000 index 0000000..2a11c24 --- /dev/null +++ b/lib/ttg @@ -0,0 +1 @@ +Subproject commit 2a11c24f88f4b0b31e376dc61b73ab8a9a67190a diff --git a/lib/wrapped-m-token b/lib/wrapped-m-token new file mode 160000 index 0000000..18e10a6 --- /dev/null +++ b/lib/wrapped-m-token @@ -0,0 +1 @@ +Subproject commit 18e10a6d5057093ce7c156f2dc33043598d4ad0c diff --git a/script/deploy/DeployBase.sol b/script/deploy/DeployBase.sol index 1d5a8eb..07297ef 100644 --- a/script/deploy/DeployBase.sol +++ b/script/deploy/DeployBase.sol @@ -4,8 +4,12 @@ pragma solidity 0.8.26; import { Script, console2 } from "../../lib/forge-std/src/Script.sol"; +import { ContractHelper } from "../../lib/common/src/libs/ContractHelper.sol"; + import { MToken as SpokeMToken } from "../../lib/protocol/src/MToken.sol"; import { Registrar as SpokeRegistrar } from "../../lib/ttg/src/Registrar.sol"; +import { WrappedMToken as SpokeSmartMToken } from "../../lib/wrapped-m-token/src/WrappedMToken.sol"; +import { Proxy as SpokeSmartMTokenProxy } from "../../lib/wrapped-m-token/src/Proxy.sol"; import { ERC1967Proxy @@ -83,16 +87,16 @@ contract DeployBase is Script, Utils { /** * @dev Deploys Spoke components. - * @param deployer_ The address of the deployer. - * @param wormholeChainId_ The Wormhole chain ID on which the contracts will be deployed. - * @param wormholeCoreBridge_ The address of the Wormhole Core Bridge. - * @param wormholeRelayerAddr_ The address of the Wormhole Standard Relayer. - * @param specialRelayerAddr_ The address of the Specialized Relayer. - * @param burnNonces_ The function to burn nonces. - * @return spokePortal_ The address of the deployed Spoke Portal. - * @return spokeTransceiver_ The address of the deployed Spoke WormholeTransceiver. - * @return spokeRegistrar_ The address of the deployed Spoke Registrar. - * @return spokeMToken_ The address of the deployed Spoke MToken. + * @param deployer_ The address of the deployer. + * @param wormholeChainId_ The Wormhole chain ID on which the contracts will be deployed. + * @param wormholeCoreBridge_ The address of the Wormhole Core Bridge. + * @param wormholeRelayerAddr_ The address of the Wormhole Standard Relayer. + * @param specialRelayerAddr_ The address of the Specialized Relayer. + * @param burnNonces_ The function to burn nonces. + * @return spokePortal_ The address of the deployed Spoke Portal. + * @return spokeTransceiver_ The address of the deployed Spoke WormholeTransceiver. + * @return spokeRegistrar_ The address of the deployed Spoke Registrar. + * @return spokeMToken_ The address of the deployed Spoke MToken. */ function _deploySpokeComponents( address deployer_, @@ -235,6 +239,57 @@ contract DeployBase is Script, Utils { return address(spokeMToken_); } + function _deploySpokeSmartMToken( + address deployer_, + address spokeMToken_, + address registrar_, + address migrationAdmin_, + function(address, uint64, uint64) internal burnNonces_ + ) internal returns (address spokeSmartMTokenImplementation_, address spokeSmartMTokenProxy_) { + uint64 deployerNonce_ = vm.getNonce(deployer_); + + if (deployerNonce_ > _SPOKE_SMART_TOKEN_NONCE) { + revert DeployerNonceTooHigh(_SPOKE_SMART_TOKEN_NONCE, deployerNonce_); + } + + burnNonces_(deployer_, deployerNonce_, _SPOKE_SMART_TOKEN_NONCE); + + deployerNonce_ = vm.getNonce(deployer_); + if (deployerNonce_ != _SPOKE_SMART_TOKEN_NONCE) { + revert DeployerNonceTooHigh(_SPOKE_SMART_TOKEN_NONCE, deployerNonce_); + } + + // Pre-compute the expected SpokeSmartMToken implementation address. + address expectedSmartMTokenImplementation_ = ContractHelper.getContractFrom( + deployer_, + _SPOKE_SMART_TOKEN_NONCE + ); + + spokeSmartMTokenImplementation_ = address(new SpokeSmartMToken(spokeMToken_, registrar_, migrationAdmin_)); + + if (expectedSmartMTokenImplementation_ != spokeSmartMTokenImplementation_) { + revert ExpectedAddressMismatch(expectedSmartMTokenImplementation_, spokeSmartMTokenImplementation_); + } + + console2.log("SpokeSmartMTokenImplementation:", spokeSmartMTokenImplementation_); + + deployerNonce_ = vm.getNonce(deployer_); + if (deployerNonce_ != _SPOKE_SMART_TOKEN_PROXY_NONCE) { + revert DeployerNonceTooHigh(_SPOKE_SMART_TOKEN_PROXY_NONCE, deployerNonce_); + } + + // Pre-compute the expected SpokeSmartMToken proxy address. + address expectedSmartMTokenProxy_ = ContractHelper.getContractFrom(deployer_, _SPOKE_SMART_TOKEN_PROXY_NONCE); + + spokeSmartMTokenProxy_ = address(new SpokeSmartMTokenProxy(spokeSmartMTokenImplementation_)); + + if (expectedSmartMTokenProxy_ != spokeSmartMTokenProxy_) { + revert ExpectedAddressMismatch(expectedSmartMTokenProxy_, spokeSmartMTokenProxy_); + } + + console2.log("SpokeSmartMTokenProxy:", spokeSmartMTokenProxy_); + } + function _configurePortal(address portal_, address transceiver_) internal { IManagerBase(portal_).setTransceiver(transceiver_); console2.log("Transceiver address set: ", transceiver_); diff --git a/script/deploy/dev/DeployDevSpoke.s.sol b/script/deploy/dev/DeployDevSpoke.s.sol index 6e56436..828919c 100644 --- a/script/deploy/dev/DeployDevSpoke.s.sol +++ b/script/deploy/dev/DeployDevSpoke.s.sol @@ -27,12 +27,28 @@ contract DeployDevSpoke is DeployBase { _burnNonces ); + ( + address spokeBaseSepoliaSmartMTokenImplementation_, + address spokeBaseSepoliaSmartMTokenProxy_ + ) = _deploySpokeSmartMToken( + deployer_, + spokeBaseSepoliaMToken_, + spokeBaseSepoliaRegistrar_, + deployer_, + _burnNonces + ); + vm.stopBroadcast(); console2.log("Base Sepolia Spoke NTT Manager address:", spokeBaseSepoliaNTTManager_); console2.log("Base Sepolia Spoke Wormhole Transceiver address:", spokeBaseSepoliaWormholeTransceiver_); console2.log("Base Sepolia Spoke Registrar address:", spokeBaseSepoliaRegistrar_); console2.log("Base Sepolia Spoke MToken address:", spokeBaseSepoliaMToken_); + console2.log( + "Base Sepolia SmartMToken implementation address:", + spokeBaseSepoliaSmartMTokenImplementation_ + ); + console2.log("Base Sepolia Spoke MToken proxy address:", spokeBaseSepoliaSmartMTokenProxy_); } else if (block.chainid == _OPTIMISM_SEPOLIA_CHAIN_ID) { vm.startBroadcast(deployer_); @@ -50,6 +66,17 @@ contract DeployDevSpoke is DeployBase { _burnNonces ); + ( + address spokeOptimismSepoliaSmartMTokenImplementation_, + address spokeOptimismSepoliaSmartMTokenProxy_ + ) = _deploySpokeSmartMToken( + deployer_, + spokeOptimismSepoliaMToken_, + spokeOptimismSepoliaRegistrar_, + deployer_, + _burnNonces + ); + console2.log("Optimism Sepolia Spoke NTT Manager address:", spokeOptimismSepoliaNTTManager_); console2.log( "Optimism Sepolia Spoke Wormhole Transceiver address:", @@ -57,6 +84,11 @@ contract DeployDevSpoke is DeployBase { ); console2.log("Optimism Sepolia Spoke Registrar address:", spokeOptimismSepoliaRegistrar_); console2.log("Optimism Sepolia Spoke MToken address:", spokeOptimismSepoliaMToken_); + console2.log( + "Optimism Sepolia SmartMToken implementation address:", + spokeOptimismSepoliaSmartMTokenImplementation_ + ); + console2.log("Optimism Sepolia Spoke MToken proxy address:", spokeOptimismSepoliaSmartMTokenProxy_); vm.stopBroadcast(); } else { diff --git a/script/helpers/Utils.sol b/script/helpers/Utils.sol index 1ec76e5..c02c9d3 100644 --- a/script/helpers/Utils.sol +++ b/script/helpers/Utils.sol @@ -9,6 +9,8 @@ import { ICreateXLike } from "../deploy/interfaces/ICreateXLike.sol"; contract Utils { uint64 internal constant _SPOKE_REGISTRAR_NONCE = 7; uint64 internal constant _SPOKE_M_TOKEN_NONCE = 8; + uint64 internal constant _SPOKE_SMART_TOKEN_NONCE = 39; + uint64 internal constant _SPOKE_SMART_TOKEN_PROXY_NONCE = 40; address internal constant _MAINNET_REGISTRAR = 0x119FbeeDD4F4f4298Fb59B720d5654442b81ae2c; address internal constant _MAINNET_M_TOKEN = 0x866A2BF4E572CbcF37D5071A7a58503Bfb36be1b; diff --git a/src/interfaces/IMTokenLike.sol b/src/interfaces/IMTokenLike.sol index 87fb558..ed9f93d 100644 --- a/src/interfaces/IMTokenLike.sol +++ b/src/interfaces/IMTokenLike.sol @@ -22,4 +22,4 @@ interface IMTokenLike { /// @notice Stops earning for caller. function stopEarning() external; -} \ No newline at end of file +} diff --git a/src/interfaces/IRegistrarLike.sol b/src/interfaces/IRegistrarLike.sol index bd748db..64c2d85 100644 --- a/src/interfaces/IRegistrarLike.sol +++ b/src/interfaces/IRegistrarLike.sol @@ -45,4 +45,4 @@ interface IRegistrarLike { /// @notice Returns the address of the Portal contract. function portal() external view returns (address); -} \ No newline at end of file +} diff --git a/src/interfaces/ISpokeMTokenLike.sol b/src/interfaces/ISpokeMTokenLike.sol index 02461c7..5d7e162 100644 --- a/src/interfaces/ISpokeMTokenLike.sol +++ b/src/interfaces/ISpokeMTokenLike.sol @@ -31,4 +31,3 @@ interface ISpokeMTokenLike is IMTokenLike { */ function updateIndex(uint128 index) external; } - diff --git a/test/fork/Deploy.t.sol b/test/fork/Deploy.t.sol index 03901b1..9f58976 100644 --- a/test/fork/Deploy.t.sol +++ b/test/fork/Deploy.t.sol @@ -4,6 +4,8 @@ pragma solidity 0.8.26; import { Test } from "../../lib/forge-std/src/Test.sol"; +import { ContractHelper } from "../../lib/common/src/libs/ContractHelper.sol"; + import { MToken as SpokeMToken } from "../../lib/protocol/src/MToken.sol"; import { Registrar as SpokeRegistrar } from "../../lib/ttg/src/Registrar.sol"; @@ -63,6 +65,14 @@ contract Deploy is DeployBase, Test { _burnNonces ); + // TODO: remove once Wrapped M Token has been updated to pass vault address to constructor. + vm.mockCall(baseSpokeRegistrar_, abi.encodeWithSelector(bytes4(keccak256("vault()"))), abi.encode(address(0))); + + ( + address spokeBaseSepoliaSmartMTokenImplementation_, + address spokeBaseSepoliaSmartMTokenProxy_ + ) = _deploySpokeSmartMToken(_DEPLOYER, baseSpokeMToken_, baseSpokeRegistrar_, _DEPLOYER, _burnNonces); + vm.stopPrank(); // Contracts addresses should be the same across all networks. @@ -72,10 +82,15 @@ contract Deploy is DeployBase, Test { _computeSalt(_DEPLOYER, "WormholeTransceiver") ); + address expectedSmartMTokenImplementation_ = ContractHelper.getContractFrom(_DEPLOYER, 39); + address expectedSmartMTokenProxy_ = ContractHelper.getContractFrom(_DEPLOYER, 40); + assertEq(baseSpokePortal_, expectedSpokePortal_); assertEq(baseSpokeWormholeTransceiver_, expectedSpokeWormholeTransceiver_); assertEq(baseSpokeRegistrar_, _MAINNET_REGISTRAR); assertEq(baseSpokeMToken_, _MAINNET_M_TOKEN); + assertEq(spokeBaseSepoliaSmartMTokenImplementation_, expectedSmartMTokenImplementation_); + assertEq(spokeBaseSepoliaSmartMTokenProxy_, expectedSmartMTokenProxy_); assertEq(SpokeMToken(baseSpokeMToken_).portal(), baseSpokePortal_); assertEq(SpokeMToken(baseSpokeMToken_).registrar(), baseSpokeRegistrar_);