From 3e2f9e34b773b4e8eba77e349bf1d8fda2e48ffd Mon Sep 17 00:00:00 2001 From: Yorke Rhodes Date: Tue, 22 Oct 2024 16:45:05 -0400 Subject: [PATCH 01/23] refactor: implement outbound and inbound warp route amount transforms (#4729) ### Description - implements `_outbound` and `_inbound` internal amount transforms for use in scaling warp routes - simplify `HypNativeScaled` implementation ### Backward compatibility Yes ### Testing Existing HypNative Scaled Unit Tests --- .changeset/green-kangaroos-whisper.md | 5 ++ .../token/extensions/HypNativeScaled.sol | 61 +++---------------- solidity/contracts/token/libs/TokenRouter.sol | 34 ++++++++++- solidity/test/token/HypNativeScaled.t.sol | 2 +- 4 files changed, 44 insertions(+), 58 deletions(-) create mode 100644 .changeset/green-kangaroos-whisper.md diff --git a/.changeset/green-kangaroos-whisper.md b/.changeset/green-kangaroos-whisper.md new file mode 100644 index 0000000000..aa3212a5a5 --- /dev/null +++ b/.changeset/green-kangaroos-whisper.md @@ -0,0 +1,5 @@ +--- +'@hyperlane-xyz/core': patch +--- + +Refactor TokenRouter internal amount accounting for use in scaling Warp Routes diff --git a/solidity/contracts/token/extensions/HypNativeScaled.sol b/solidity/contracts/token/extensions/HypNativeScaled.sol index ad129fce80..605ec87b92 100644 --- a/solidity/contracts/token/extensions/HypNativeScaled.sol +++ b/solidity/contracts/token/extensions/HypNativeScaled.sol @@ -17,62 +17,15 @@ contract HypNativeScaled is HypNative { scale = _scale; } - /** - * @inheritdoc HypNative - * @dev Sends scaled `msg.value` (divided by `scale`) to `_recipient`. - */ - function transferRemote( - uint32 _destination, - bytes32 _recipient, + function _outboundAmount( uint256 _amount - ) external payable override returns (bytes32 messageId) { - require(msg.value >= _amount, "Native: amount exceeds msg.value"); - uint256 _hookPayment = msg.value - _amount; - uint256 _scaledAmount = _amount / scale; - return - _transferRemote( - _destination, - _recipient, - _scaledAmount, - _hookPayment - ); + ) internal view override returns (uint256) { + return _amount / scale; } - /** - * @inheritdoc TokenRouter - * @dev uses (`msg.value` - `_amount`) as hook payment. - */ - function transferRemote( - uint32 _destination, - bytes32 _recipient, - uint256 _amount, - bytes calldata _hookMetadata, - address _hook - ) external payable override returns (bytes32 messageId) { - require(msg.value >= _amount, "Native: amount exceeds msg.value"); - uint256 _hookPayment = msg.value - _amount; - uint256 _scaledAmount = _amount / scale; - return - _transferRemote( - _destination, - _recipient, - _scaledAmount, - _hookPayment, - _hookMetadata, - _hook - ); - } - - /** - * @dev Sends scaled `_amount` (multiplied by `scale`) to `_recipient`. - * @inheritdoc TokenRouter - */ - function _transferTo( - address _recipient, - uint256 _amount, - bytes calldata metadata // no metadata - ) internal override { - uint256 scaledAmount = _amount * scale; - HypNative._transferTo(_recipient, scaledAmount, metadata); + function _inboundAmount( + uint256 _amount + ) internal view override returns (uint256) { + return _amount * scale; } } diff --git a/solidity/contracts/token/libs/TokenRouter.sol b/solidity/contracts/token/libs/TokenRouter.sol index 96ffcf8385..8a474acea3 100644 --- a/solidity/contracts/token/libs/TokenRouter.sol +++ b/solidity/contracts/token/libs/TokenRouter.sol @@ -116,9 +116,11 @@ abstract contract TokenRouter is GasRouter { address _hook ) internal virtual returns (bytes32 messageId) { bytes memory _tokenMetadata = _transferFromSender(_amountOrId); + + uint256 outboundAmount = _outboundAmount(_amountOrId); bytes memory _tokenMessage = TokenMessage.format( _recipient, - _amountOrId, + outboundAmount, _tokenMetadata ); @@ -130,7 +132,29 @@ abstract contract TokenRouter is GasRouter { _hook ); - emit SentTransferRemote(_destination, _recipient, _amountOrId); + emit SentTransferRemote(_destination, _recipient, outboundAmount); + } + + /** + * @dev Should return the amount of tokens to be encoded in the message amount (eg for scaling `_localAmount`). + * @param _localAmount The amount of tokens transferred on this chain in local denomination. + * @return _messageAmount The amount of tokens to be encoded in the message body. + */ + function _outboundAmount( + uint256 _localAmount + ) internal view virtual returns (uint256 _messageAmount) { + _messageAmount = _localAmount; + } + + /** + * @dev Should return the amount of tokens to be decoded from the message amount. + * @param _messageAmount The amount of tokens received in the message body. + * @return _localAmount The amount of tokens to be transferred on this chain in local denomination. + */ + function _inboundAmount( + uint256 _messageAmount + ) internal view virtual returns (uint256 _localAmount) { + _localAmount = _messageAmount; } /** @@ -163,7 +187,11 @@ abstract contract TokenRouter is GasRouter { bytes32 recipient = _message.recipient(); uint256 amount = _message.amount(); bytes calldata metadata = _message.metadata(); - _transferTo(recipient.bytes32ToAddress(), amount, metadata); + _transferTo( + recipient.bytes32ToAddress(), + _inboundAmount(amount), + metadata + ); emit ReceivedTransferRemote(_origin, recipient, amount); } diff --git a/solidity/test/token/HypNativeScaled.t.sol b/solidity/test/token/HypNativeScaled.t.sol index ffded65655..47899ae89e 100644 --- a/solidity/test/token/HypNativeScaled.t.sol +++ b/solidity/test/token/HypNativeScaled.t.sol @@ -144,7 +144,7 @@ contract HypNativeScaledTest is Test { environment.processNextPendingMessage(); } - function test_tranferRemote(uint256 amount) public { + function test_transferRemote(uint256 amount) public { vm.assume(amount <= mintAmount); uint256 nativeValue = amount * (10 ** nativeDecimals); From a51b2882d0c6cb06c10005c821af7a2bc9e51cff Mon Sep 17 00:00:00 2001 From: Kunal Arora <55632507+aroralanuk@users.noreply.github.com> Date: Tue, 29 Oct 2024 20:56:35 +0530 Subject: [PATCH 02/23] fix(contracts): check for sufficient msgValue for `AggregationHook` (#4673) ### Description - fixes misuse of aggregation hook funds for relaying messages by making sure msg.value is adequate and refunding if excess. ### Drive-by changes - None ### Related issues - related to https://github.com/hyperlane-xyz/hyperlane-monorepo/issues/3437 ### Backward compatibility No, needs new deployments of aggregationHooks ### Testing Unit --- .changeset/friendly-owls-stare.md | 5 ++ .../aggregation/StaticAggregationHook.sol | 25 +++++++++ solidity/test/hooks/AggregationHook.t.sol | 55 +++++++++++++++++++ 3 files changed, 85 insertions(+) create mode 100644 .changeset/friendly-owls-stare.md diff --git a/.changeset/friendly-owls-stare.md b/.changeset/friendly-owls-stare.md new file mode 100644 index 0000000000..1d75ff0b26 --- /dev/null +++ b/.changeset/friendly-owls-stare.md @@ -0,0 +1,5 @@ +--- +'@hyperlane-xyz/core': minor +--- + +Fixed misuse of aggregation hook funds for relaying messages by making sure msg.value is adequate and refunding if excess. diff --git a/solidity/contracts/hooks/aggregation/StaticAggregationHook.sol b/solidity/contracts/hooks/aggregation/StaticAggregationHook.sol index 5146ca2c02..a8e6fc7acd 100644 --- a/solidity/contracts/hooks/aggregation/StaticAggregationHook.sol +++ b/solidity/contracts/hooks/aggregation/StaticAggregationHook.sol @@ -13,11 +13,23 @@ pragma solidity >=0.8.0; @@@@@@@@@ @@@@@@@@@ @@@@@@@@@ @@@@@@@@*/ +// ============ Internal Imports ============ +import {StandardHookMetadata} from "../libs/StandardHookMetadata.sol"; +import {Message} from "../../libs/Message.sol"; +import {TypeCasts} from "../../libs/TypeCasts.sol"; import {AbstractPostDispatchHook} from "../libs/AbstractPostDispatchHook.sol"; import {IPostDispatchHook} from "../../interfaces/hooks/IPostDispatchHook.sol"; import {MetaProxy} from "../../libs/MetaProxy.sol"; +// ============ External Imports ============ +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; + contract StaticAggregationHook is AbstractPostDispatchHook { + using Message for bytes; + using TypeCasts for bytes32; + using StandardHookMetadata for bytes; + using Address for address payable; + // ============ External functions ============ /// @inheritdoc IPostDispatchHook @@ -32,16 +44,29 @@ contract StaticAggregationHook is AbstractPostDispatchHook { ) internal override { address[] memory _hooks = hooks(message); uint256 count = _hooks.length; + uint256 valueRemaining = msg.value; for (uint256 i = 0; i < count; i++) { uint256 quote = IPostDispatchHook(_hooks[i]).quoteDispatch( metadata, message ); + require( + valueRemaining >= quote, + "StaticAggregationHook: insufficient value" + ); IPostDispatchHook(_hooks[i]).postDispatch{value: quote}( metadata, message ); + + valueRemaining -= quote; + } + + if (valueRemaining > 0) { + payable(metadata.refundAddress(message.senderAddress())).sendValue( + valueRemaining + ); } } diff --git a/solidity/test/hooks/AggregationHook.t.sol b/solidity/test/hooks/AggregationHook.t.sol index a37b20f1a1..bbd403ce33 100644 --- a/solidity/test/hooks/AggregationHook.t.sol +++ b/solidity/test/hooks/AggregationHook.t.sol @@ -3,12 +3,16 @@ pragma solidity ^0.8.13; import {Test} from "forge-std/Test.sol"; +import {Message} from "../../contracts/libs/Message.sol"; +import {TypeCasts} from "../../contracts/libs/TypeCasts.sol"; import {StaticAggregationHook} from "../../contracts/hooks/aggregation/StaticAggregationHook.sol"; import {StaticAggregationHookFactory} from "../../contracts/hooks/aggregation/StaticAggregationHookFactory.sol"; import {TestPostDispatchHook} from "../../contracts/test/TestPostDispatchHook.sol"; import {IPostDispatchHook} from "../../contracts/interfaces/hooks/IPostDispatchHook.sol"; contract AggregationHookTest is Test { + using TypeCasts for address; + StaticAggregationHookFactory internal factory; StaticAggregationHook internal hook; @@ -72,6 +76,55 @@ contract AggregationHookTest is Test { hook.postDispatch{value: _msgValue}("", message); } + function test_postDispatch_refundsExcess( + uint8 _hooks, + bytes calldata body + ) public { + uint256 fee = PER_HOOK_GAS_AMOUNT; + address[] memory hooksDeployed = deployHooks(_hooks, fee); + uint256 requiredValue = hooksDeployed.length * fee; + uint256 overpaidValue = requiredValue + 1000; + + vm.prank(address(this)); + + uint256 initialBalance = address(this).balance; + + bytes memory message = Message.formatMessage( + 1, + 0, + 1, + address(this).addressToBytes32(), + 2, + address(this).addressToBytes32(), + body + ); + hook.postDispatch{value: overpaidValue}("", message); + + assertEq(address(hook).balance, 0); + assertEq(address(this).balance, initialBalance - requiredValue); + } + + function testPostDispatch_preventsUsingContractFunds( + uint8 _hooks, + bytes calldata body + ) public { + uint256 fee = PER_HOOK_GAS_AMOUNT; + deployHooks(_hooks, fee); + vm.assume(_hooks > 0); + + vm.prank(address(this)); + + uint256 additionalFunds = 1 ether; + vm.deal(address(hook), additionalFunds); + + bytes memory message = abi.encodePacked("hello world"); + + vm.expectRevert("StaticAggregationHook: insufficient value"); + hook.postDispatch{value: 0}("", message); + + assertEq(address(hook).balance, additionalFunds); + } + function testQuoteDispatch(uint8 _hooks) public { uint256 fee = PER_HOOK_GAS_AMOUNT; address[] memory hooksDeployed = deployHooks(_hooks, fee); @@ -94,4 +147,6 @@ contract AggregationHookTest is Test { deployHooks(1, 0); assertEq(hook.hookType(), uint8(IPostDispatchHook.Types.AGGREGATION)); } + + receive() external payable {} } From 72155772105d45442a8fbf677483e9a61e7385c1 Mon Sep 17 00:00:00 2001 From: -f Date: Tue, 5 Nov 2024 19:01:47 +0530 Subject: [PATCH 03/23] init --- .../hooks/libs/StandardHookMetadata.sol | 26 +++ solidity/contracts/token/HypValue.sol | 166 +++++++++++++++ solidity/test/token/HypValue.t.sol | 197 ++++++++++++++++++ 3 files changed, 389 insertions(+) create mode 100644 solidity/contracts/token/HypValue.sol create mode 100644 solidity/test/token/HypValue.t.sol diff --git a/solidity/contracts/hooks/libs/StandardHookMetadata.sol b/solidity/contracts/hooks/libs/StandardHookMetadata.sol index e12f6822ad..165ee6820d 100644 --- a/solidity/contracts/hooks/libs/StandardHookMetadata.sol +++ b/solidity/contracts/hooks/libs/StandardHookMetadata.sol @@ -165,4 +165,30 @@ library StandardHookMetadata { ) internal pure returns (bytes memory) { return formatMetadata(uint256(0), uint256(0), _refundAddress, ""); } + + function overrideMsgValue( + bytes calldata _metadata, + uint256 _msgValue + ) internal view returns (bytes memory) { + return + formatMetadata( + _msgValue, + gasLimit(_metadata, 0), + refundAddress(_metadata, msg.sender), + getCustomMetadata(_metadata) + ); + } + + function overrideGasLimit( + bytes calldata _metadata, + uint256 _gasLimit + ) internal view returns (bytes memory) { + return + formatMetadata( + msgValue(_metadata, 0), + _gasLimit, + refundAddress(_metadata, msg.sender), + getCustomMetadata(_metadata) + ); + } } diff --git a/solidity/contracts/token/HypValue.sol b/solidity/contracts/token/HypValue.sol new file mode 100644 index 0000000000..3908be868a --- /dev/null +++ b/solidity/contracts/token/HypValue.sol @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.8.0; + +/*@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@ HYPERLANE @@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ +@@@@@@@@@ @@@@@@@@*/ + +// ============ Internal Imports ============ +import {TokenRouter} from "./libs/TokenRouter.sol"; +import {StandardHookMetadata} from "../hooks/libs/StandardHookMetadata.sol"; + +// ============ External Imports ============ +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; + +/** + * @title HypValue + * @author Abacus Works + * @notice This contract facilitates the transfer of value between chains using value transfer hooks + */ +contract HypValue is TokenRouter { + // ============ Errors ============ + error InsufficientValue(uint256 amount, uint256 value); + + constructor(address _mailbox) TokenRouter(_mailbox) {} + + // ============ Initialization ============ + + /** + * @notice Initializes the contract + * @param _valuehook The address of the value transfer hook + * @param _interchainSecurityModule The address of the interchain security module + * @param _owner The owner of the contract + */ + function initialize( + address _valuehook, + address _interchainSecurityModule, + address _owner + ) public initializer { + _MailboxClient_initialize( + _valuehook, + _interchainSecurityModule, + _owner + ); + } + + // ============ External Functions ============ + + /** + * @inheritdoc TokenRouter + * @dev use _hook with caution, make sure that this hook can handle msg.value transfer using the metadata.msgValue() + */ + function transferRemote( + uint32 _destination, + bytes32 _recipient, + uint256 _amount, + bytes calldata _hookMetadata, + address _hook + ) external payable virtual override returns (bytes32 messageId) { + uint256 quote = _checkSufficientValue(_destination, _amount, _hook); + + bytes memory hookMetadata = StandardHookMetadata.overrideMsgValue( + _hookMetadata, + _amount + ); + + return + _transferRemote( + _destination, + _recipient, + _amount, + _amount + quote, + hookMetadata, + _hook + ); + } + + /// @inheritdoc TokenRouter + function transferRemote( + uint32 _destination, + bytes32 _recipient, + uint256 _amount + ) external payable virtual override returns (bytes32 messageId) { + uint256 quote = _checkSufficientValue( + _destination, + _amount, + address(hook) + ); + bytes memory hookMetadata = StandardHookMetadata.formatMetadata( + _amount, + destinationGas[_destination], + msg.sender, + "" + ); + + return + _transferRemote( + _destination, + _recipient, + _amount, + _amount + quote, + hookMetadata, + address(hook) + ); + } + + // ============ Internal Functions ============ + + /** + * @inheritdoc TokenRouter + * @dev No metadata is needed for value transfers + */ + function _transferFromSender( + uint256 + ) internal pure override returns (bytes memory) { + return bytes(""); // no metadata + } + + /** + * @inheritdoc TokenRouter + * @dev Sends the value to the recipient + */ + function _transferTo( + address _recipient, + uint256 _amount, + bytes calldata // no metadata + ) internal virtual override { + Address.sendValue(payable(_recipient), _amount); + } + + /** + * @inheritdoc TokenRouter + * @dev This contract doesn't hold value + */ + function balanceOf( + address /* _account */ + ) external pure override returns (uint256) { + return 0; + } + + /// @dev Checks if the provided value is sufficient for the transfer + function _checkSufficientValue( + uint32 _destination, + uint256 _amount, + address _hook + ) internal view returns (uint256) { + uint256 quote = _GasRouter_quoteDispatch( + _destination, + new bytes(0), + _hook + ); + if (msg.value < _amount + quote) { + revert InsufficientValue(_amount + quote, msg.value); + } + return quote; + } + + receive() external payable {} +} diff --git a/solidity/test/token/HypValue.t.sol b/solidity/test/token/HypValue.t.sol new file mode 100644 index 0000000000..5165a24555 --- /dev/null +++ b/solidity/test/token/HypValue.t.sol @@ -0,0 +1,197 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.8.0; + +import {TypeCasts} from "../../contracts/libs/TypeCasts.sol"; +import {HypTokenTest} from "./HypERC20.t.sol"; +import {HypERC20} from "../../contracts/token/HypERC20.sol"; +import {TokenRouter} from "../../contracts/token/libs/TokenRouter.sol"; +import {HypValue} from "../../contracts/token/HypValue.sol"; +import {OPL2ToL1Hook} from "../../contracts/hooks/OPL2ToL1Hook.sol"; +import {OPL2ToL1Ism} from "../../contracts/isms/hook/OPL2ToL1Ism.sol"; +import {IOptimismPortal} from "../../contracts/interfaces/optimism/IOptimismPortal.sol"; +import {TestPostDispatchHook} from "../../contracts/test/TestPostDispatchHook.sol"; +import {ICrossDomainMessenger} from "../../contracts/interfaces/optimism/ICrossDomainMessenger.sol"; +import {AbstractMessageIdAuthorizedIsm} from "../../contracts/isms/hook/AbstractMessageIdAuthorizedIsm.sol"; +import {MockOptimismMessenger, MockOptimismPortal} from "../../contracts/mock/MockOptimism.sol"; +import {TestInterchainGasPaymaster} from "../../contracts/test/TestInterchainGasPaymaster.sol"; + +contract HypValueTest is HypTokenTest { + using TypeCasts for address; + + address internal constant L2_MESSENGER_ADDRESS = + 0x4200000000000000000000000000000000000007; + + HypValue internal localValueRouter; + HypValue internal remoteValueRouter; + OPL2ToL1Hook internal valueHook; + OPL2ToL1Ism internal ism; + TestInterchainGasPaymaster internal mockOverheadIgp; + MockOptimismPortal internal portal; + MockOptimismMessenger internal l1Messenger; + + function setUp() public override { + super.setUp(); + vm.etch( + L2_MESSENGER_ADDRESS, + address(new MockOptimismMessenger()).code + ); + + localValueRouter = new HypValue(address(localMailbox)); + remoteValueRouter = new HypValue(address(remoteMailbox)); + + localToken = TokenRouter(payable(address(localValueRouter))); + remoteToken = HypERC20(payable(address(remoteValueRouter))); + + l1Messenger = new MockOptimismMessenger(); + portal = new MockOptimismPortal(); + l1Messenger.setPORTAL(address(portal)); + ism = new OPL2ToL1Ism(address(l1Messenger)); + + mockOverheadIgp = new TestInterchainGasPaymaster(); + valueHook = new OPL2ToL1Hook( + address(localMailbox), + DESTINATION, + address(localMailbox).addressToBytes32(), + L2_MESSENGER_ADDRESS, + address(mockOverheadIgp) + ); + + localValueRouter.initialize( + address(valueHook), + address(ism), + address(this) + ); + remoteValueRouter.initialize( + address(valueHook), + address(ism), + address(this) + ); + + localValueRouter.enrollRemoteRouter( + DESTINATION, + address(remoteToken).addressToBytes32() + ); + remoteValueRouter.enrollRemoteRouter( + ORIGIN, + address(localToken).addressToBytes32() + ); + + vm.deal(ALICE, 1000e18); + } + + function testRemoteTransfer() public { + uint256 quote = localValueRouter.quoteGasPayment(DESTINATION); + uint256 msgValue = TRANSFER_AMT + quote; + + vm.expectEmit(true, true, false, true); + emit TokenRouter.SentTransferRemote( + DESTINATION, + BOB.addressToBytes32(), + TRANSFER_AMT + ); + + vm.prank(ALICE); + bytes32 messageId = localToken.transferRemote{value: msgValue}( + DESTINATION, + BOB.addressToBytes32(), + TRANSFER_AMT + ); + + vm.assertEq(address(localToken).balance, 0); + vm.assertEq(address(valueHook).balance, 0); + + _externalBridgeDestinationCall(messageId, msgValue); + + vm.expectEmit(true, true, false, true); + emit ReceivedTransferRemote( + ORIGIN, + BOB.addressToBytes32(), + TRANSFER_AMT + ); + remoteMailbox.processNextInboundMessage(); + + assertEq(BOB.balance, TRANSFER_AMT); + assertEq(address(mockOverheadIgp).balance, quote); + } + + function testRemoteTransfer_invalidAmount() public { + uint256 quote = localValueRouter.quoteGasPayment(DESTINATION); + + vm.expectRevert( + abi.encodeWithSelector( + HypValue.InsufficientValue.selector, + TRANSFER_AMT + quote, + TRANSFER_AMT + ) + ); + vm.prank(ALICE); + bytes32 messageId = localToken.transferRemote{value: TRANSFER_AMT}( + DESTINATION, + BOB.addressToBytes32(), + TRANSFER_AMT + ); + } + + function testTransfer_withHookSpecified( + uint256 fee, + bytes calldata metadata + ) public override { + vm.assume(fee < TRANSFER_AMT); + uint256 msgValue = TRANSFER_AMT + fee; + vm.deal(ALICE, msgValue); + + TestPostDispatchHook hook = new TestPostDispatchHook(); + hook.setFee(fee); + + vm.prank(ALICE); + bytes32 messageId = localToken.transferRemote{value: msgValue}( + DESTINATION, + BOB.addressToBytes32(), + TRANSFER_AMT, + metadata, + address(hook) + ); + + vm.assertEq(address(localToken).balance, 0); + vm.assertEq(address(valueHook).balance, 0); + } + + function testBenchmark_overheadGasUsage() public override { + vm.deal(address(localValueRouter), TRANSFER_AMT); + super.testBenchmark_overheadGasUsage(); + } + + // helper function to simulate the external bridge destination call using OP L2 -> L1 bridge + function _externalBridgeDestinationCall( + bytes32 _messageId, + uint256 _msgValue + ) internal { + bytes memory encodedHookData = abi.encodeCall( + AbstractMessageIdAuthorizedIsm.preVerifyMessage, + (_messageId, _msgValue) + ); + + bytes memory messengerCalldata = abi.encodeCall( + ICrossDomainMessenger.relayMessage, + ( + 0, + address(valueHook), + address(ism), + _msgValue, + uint256(100_000), + encodedHookData + ) + ); + vm.deal(address(portal), _msgValue); + IOptimismPortal.WithdrawalTransaction + memory withdrawal = IOptimismPortal.WithdrawalTransaction({ + nonce: 0, + sender: L2_MESSENGER_ADDRESS, + target: address(l1Messenger), + value: _msgValue, + gasLimit: 100_000, + data: messengerCalldata + }); + portal.finalizeWithdrawalTransaction(withdrawal); + } +} From 99647fe3d50c85427c72b6a41bffce3f2fdad7c7 Mon Sep 17 00:00:00 2001 From: -f Date: Thu, 7 Nov 2024 17:50:46 +0530 Subject: [PATCH 04/23] revert --- .changeset/friendly-owls-stare.md | 5 -- .changeset/green-kangaroos-whisper.md | 5 -- .../aggregation/StaticAggregationHook.sol | 25 -------- .../token/extensions/HypNativeScaled.sol | 61 ++++++++++++++++--- solidity/contracts/token/libs/TokenRouter.sol | 34 +---------- solidity/test/hooks/AggregationHook.t.sol | 55 ----------------- solidity/test/token/HypNativeScaled.t.sol | 2 +- 7 files changed, 58 insertions(+), 129 deletions(-) delete mode 100644 .changeset/friendly-owls-stare.md delete mode 100644 .changeset/green-kangaroos-whisper.md diff --git a/.changeset/friendly-owls-stare.md b/.changeset/friendly-owls-stare.md deleted file mode 100644 index 1d75ff0b26..0000000000 --- a/.changeset/friendly-owls-stare.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@hyperlane-xyz/core': minor ---- - -Fixed misuse of aggregation hook funds for relaying messages by making sure msg.value is adequate and refunding if excess. diff --git a/.changeset/green-kangaroos-whisper.md b/.changeset/green-kangaroos-whisper.md deleted file mode 100644 index aa3212a5a5..0000000000 --- a/.changeset/green-kangaroos-whisper.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@hyperlane-xyz/core': patch ---- - -Refactor TokenRouter internal amount accounting for use in scaling Warp Routes diff --git a/solidity/contracts/hooks/aggregation/StaticAggregationHook.sol b/solidity/contracts/hooks/aggregation/StaticAggregationHook.sol index a8e6fc7acd..5146ca2c02 100644 --- a/solidity/contracts/hooks/aggregation/StaticAggregationHook.sol +++ b/solidity/contracts/hooks/aggregation/StaticAggregationHook.sol @@ -13,23 +13,11 @@ pragma solidity >=0.8.0; @@@@@@@@@ @@@@@@@@@ @@@@@@@@@ @@@@@@@@*/ -// ============ Internal Imports ============ -import {StandardHookMetadata} from "../libs/StandardHookMetadata.sol"; -import {Message} from "../../libs/Message.sol"; -import {TypeCasts} from "../../libs/TypeCasts.sol"; import {AbstractPostDispatchHook} from "../libs/AbstractPostDispatchHook.sol"; import {IPostDispatchHook} from "../../interfaces/hooks/IPostDispatchHook.sol"; import {MetaProxy} from "../../libs/MetaProxy.sol"; -// ============ External Imports ============ -import {Address} from "@openzeppelin/contracts/utils/Address.sol"; - contract StaticAggregationHook is AbstractPostDispatchHook { - using Message for bytes; - using TypeCasts for bytes32; - using StandardHookMetadata for bytes; - using Address for address payable; - // ============ External functions ============ /// @inheritdoc IPostDispatchHook @@ -44,29 +32,16 @@ contract StaticAggregationHook is AbstractPostDispatchHook { ) internal override { address[] memory _hooks = hooks(message); uint256 count = _hooks.length; - uint256 valueRemaining = msg.value; for (uint256 i = 0; i < count; i++) { uint256 quote = IPostDispatchHook(_hooks[i]).quoteDispatch( metadata, message ); - require( - valueRemaining >= quote, - "StaticAggregationHook: insufficient value" - ); IPostDispatchHook(_hooks[i]).postDispatch{value: quote}( metadata, message ); - - valueRemaining -= quote; - } - - if (valueRemaining > 0) { - payable(metadata.refundAddress(message.senderAddress())).sendValue( - valueRemaining - ); } } diff --git a/solidity/contracts/token/extensions/HypNativeScaled.sol b/solidity/contracts/token/extensions/HypNativeScaled.sol index 605ec87b92..ad129fce80 100644 --- a/solidity/contracts/token/extensions/HypNativeScaled.sol +++ b/solidity/contracts/token/extensions/HypNativeScaled.sol @@ -17,15 +17,62 @@ contract HypNativeScaled is HypNative { scale = _scale; } - function _outboundAmount( + /** + * @inheritdoc HypNative + * @dev Sends scaled `msg.value` (divided by `scale`) to `_recipient`. + */ + function transferRemote( + uint32 _destination, + bytes32 _recipient, uint256 _amount - ) internal view override returns (uint256) { - return _amount / scale; + ) external payable override returns (bytes32 messageId) { + require(msg.value >= _amount, "Native: amount exceeds msg.value"); + uint256 _hookPayment = msg.value - _amount; + uint256 _scaledAmount = _amount / scale; + return + _transferRemote( + _destination, + _recipient, + _scaledAmount, + _hookPayment + ); } - function _inboundAmount( - uint256 _amount - ) internal view override returns (uint256) { - return _amount * scale; + /** + * @inheritdoc TokenRouter + * @dev uses (`msg.value` - `_amount`) as hook payment. + */ + function transferRemote( + uint32 _destination, + bytes32 _recipient, + uint256 _amount, + bytes calldata _hookMetadata, + address _hook + ) external payable override returns (bytes32 messageId) { + require(msg.value >= _amount, "Native: amount exceeds msg.value"); + uint256 _hookPayment = msg.value - _amount; + uint256 _scaledAmount = _amount / scale; + return + _transferRemote( + _destination, + _recipient, + _scaledAmount, + _hookPayment, + _hookMetadata, + _hook + ); + } + + /** + * @dev Sends scaled `_amount` (multiplied by `scale`) to `_recipient`. + * @inheritdoc TokenRouter + */ + function _transferTo( + address _recipient, + uint256 _amount, + bytes calldata metadata // no metadata + ) internal override { + uint256 scaledAmount = _amount * scale; + HypNative._transferTo(_recipient, scaledAmount, metadata); } } diff --git a/solidity/contracts/token/libs/TokenRouter.sol b/solidity/contracts/token/libs/TokenRouter.sol index 8a474acea3..96ffcf8385 100644 --- a/solidity/contracts/token/libs/TokenRouter.sol +++ b/solidity/contracts/token/libs/TokenRouter.sol @@ -116,11 +116,9 @@ abstract contract TokenRouter is GasRouter { address _hook ) internal virtual returns (bytes32 messageId) { bytes memory _tokenMetadata = _transferFromSender(_amountOrId); - - uint256 outboundAmount = _outboundAmount(_amountOrId); bytes memory _tokenMessage = TokenMessage.format( _recipient, - outboundAmount, + _amountOrId, _tokenMetadata ); @@ -132,29 +130,7 @@ abstract contract TokenRouter is GasRouter { _hook ); - emit SentTransferRemote(_destination, _recipient, outboundAmount); - } - - /** - * @dev Should return the amount of tokens to be encoded in the message amount (eg for scaling `_localAmount`). - * @param _localAmount The amount of tokens transferred on this chain in local denomination. - * @return _messageAmount The amount of tokens to be encoded in the message body. - */ - function _outboundAmount( - uint256 _localAmount - ) internal view virtual returns (uint256 _messageAmount) { - _messageAmount = _localAmount; - } - - /** - * @dev Should return the amount of tokens to be decoded from the message amount. - * @param _messageAmount The amount of tokens received in the message body. - * @return _localAmount The amount of tokens to be transferred on this chain in local denomination. - */ - function _inboundAmount( - uint256 _messageAmount - ) internal view virtual returns (uint256 _localAmount) { - _localAmount = _messageAmount; + emit SentTransferRemote(_destination, _recipient, _amountOrId); } /** @@ -187,11 +163,7 @@ abstract contract TokenRouter is GasRouter { bytes32 recipient = _message.recipient(); uint256 amount = _message.amount(); bytes calldata metadata = _message.metadata(); - _transferTo( - recipient.bytes32ToAddress(), - _inboundAmount(amount), - metadata - ); + _transferTo(recipient.bytes32ToAddress(), amount, metadata); emit ReceivedTransferRemote(_origin, recipient, amount); } diff --git a/solidity/test/hooks/AggregationHook.t.sol b/solidity/test/hooks/AggregationHook.t.sol index 3a0fdf263d..879f3de30b 100644 --- a/solidity/test/hooks/AggregationHook.t.sol +++ b/solidity/test/hooks/AggregationHook.t.sol @@ -3,16 +3,12 @@ pragma solidity ^0.8.13; import {Test} from "forge-std/Test.sol"; -import {Message} from "../../contracts/libs/Message.sol"; -import {TypeCasts} from "../../contracts/libs/TypeCasts.sol"; import {StaticAggregationHook} from "../../contracts/hooks/aggregation/StaticAggregationHook.sol"; import {StaticAggregationHookFactory} from "../../contracts/hooks/aggregation/StaticAggregationHookFactory.sol"; import {TestPostDispatchHook} from "../../contracts/test/TestPostDispatchHook.sol"; import {IPostDispatchHook} from "../../contracts/interfaces/hooks/IPostDispatchHook.sol"; contract AggregationHookTest is Test { - using TypeCasts for address; - StaticAggregationHookFactory internal factory; StaticAggregationHook internal hook; @@ -78,55 +74,6 @@ contract AggregationHookTest is Test { hook.postDispatch{value: _msgValue}("", message); } - function test_postDispatch_refundsExcess( - uint8 _hooks, - bytes calldata body - ) public { - uint256 fee = PER_HOOK_GAS_AMOUNT; - address[] memory hooksDeployed = deployHooks(_hooks, fee); - uint256 requiredValue = hooksDeployed.length * fee; - uint256 overpaidValue = requiredValue + 1000; - - vm.prank(address(this)); - - uint256 initialBalance = address(this).balance; - - bytes memory message = Message.formatMessage( - 1, - 0, - 1, - address(this).addressToBytes32(), - 2, - address(this).addressToBytes32(), - body - ); - hook.postDispatch{value: overpaidValue}("", message); - - assertEq(address(hook).balance, 0); - assertEq(address(this).balance, initialBalance - requiredValue); - } - - function testPostDispatch_preventsUsingContractFunds( - uint8 _hooks, - bytes calldata body - ) public { - uint256 fee = PER_HOOK_GAS_AMOUNT; - deployHooks(_hooks, fee); - vm.assume(_hooks > 0); - - vm.prank(address(this)); - - uint256 additionalFunds = 1 ether; - vm.deal(address(hook), additionalFunds); - - bytes memory message = abi.encodePacked("hello world"); - - vm.expectRevert("StaticAggregationHook: insufficient value"); - hook.postDispatch{value: 0}("", message); - - assertEq(address(hook).balance, additionalFunds); - } - function testQuoteDispatch(uint8 _hooks) public { uint256 fee = PER_HOOK_GAS_AMOUNT; address[] memory hooksDeployed = deployHooks(_hooks, fee); @@ -149,6 +96,4 @@ contract AggregationHookTest is Test { deployHooks(1, 0); assertEq(hook.hookType(), uint8(IPostDispatchHook.Types.AGGREGATION)); } - - receive() external payable {} } diff --git a/solidity/test/token/HypNativeScaled.t.sol b/solidity/test/token/HypNativeScaled.t.sol index 47899ae89e..ffded65655 100644 --- a/solidity/test/token/HypNativeScaled.t.sol +++ b/solidity/test/token/HypNativeScaled.t.sol @@ -144,7 +144,7 @@ contract HypNativeScaledTest is Test { environment.processNextPendingMessage(); } - function test_transferRemote(uint256 amount) public { + function test_tranferRemote(uint256 amount) public { vm.assume(amount <= mintAmount); uint256 nativeValue = amount * (10 ** nativeDecimals); From 6876905ed7964a6f3f745f949c2ffedea3362e30 Mon Sep 17 00:00:00 2001 From: -f Date: Thu, 7 Nov 2024 18:25:04 +0530 Subject: [PATCH 05/23] more --- solidity/contracts/hooks/OPL2ToL1Hook.sol | 13 ++++++++++--- solidity/contracts/token/HypValue.sol | 2 +- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/solidity/contracts/hooks/OPL2ToL1Hook.sol b/solidity/contracts/hooks/OPL2ToL1Hook.sol index 165289e57f..a8396b5de8 100644 --- a/solidity/contracts/hooks/OPL2ToL1Hook.sol +++ b/solidity/contracts/hooks/OPL2ToL1Hook.sol @@ -67,8 +67,12 @@ contract OPL2ToL1Hook is AbstractMessageIdAuthHook { bytes calldata metadata, bytes calldata message ) internal view override returns (uint256) { + bytes memory metadataWithGasLimit = metadata.overrideGasLimit( + MIN_GAS_LIMIT + ); return - metadata.msgValue(0) + childHook.quoteDispatch(metadata, message); + metadata.msgValue(0) + + childHook.quoteDispatch(metadataWithGasLimit, message); } // ============ Internal functions ============ @@ -83,9 +87,12 @@ contract OPL2ToL1Hook is AbstractMessageIdAuthHook { (message.id(), metadata.msgValue(0)) ); + bytes memory metadataWithGasLimit = metadata.overrideGasLimit( + MIN_GAS_LIMIT + ); childHook.postDispatch{ - value: childHook.quoteDispatch(metadata, message) - }(metadata, message); + value: childHook.quoteDispatch(metadataWithGasLimit, message) + }(metadataWithGasLimit, message); l2Messenger.sendMessage{value: metadata.msgValue(0)}( TypeCasts.bytes32ToAddress(ism), payload, diff --git a/solidity/contracts/token/HypValue.sol b/solidity/contracts/token/HypValue.sol index 3908be868a..ef04ab6b09 100644 --- a/solidity/contracts/token/HypValue.sol +++ b/solidity/contracts/token/HypValue.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT AND Apache-2.0 pragma solidity >=0.8.0; /*@@@@@@@ @@@@@@@@@ From 25140437bdcc5196e28e5df3593a63f1a5bd6da2 Mon Sep 17 00:00:00 2001 From: -f Date: Thu, 7 Nov 2024 18:51:12 +0530 Subject: [PATCH 06/23] more --- .../contracts/hooks/libs/StandardHookMetadata.sol | 12 ++++++++++++ solidity/contracts/token/HypValue.sol | 8 ++++---- solidity/test/token/HypValue.t.sol | 2 +- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/solidity/contracts/hooks/libs/StandardHookMetadata.sol b/solidity/contracts/hooks/libs/StandardHookMetadata.sol index 165ee6820d..72f84e06f1 100644 --- a/solidity/contracts/hooks/libs/StandardHookMetadata.sol +++ b/solidity/contracts/hooks/libs/StandardHookMetadata.sol @@ -166,6 +166,12 @@ library StandardHookMetadata { return formatMetadata(uint256(0), uint256(0), _refundAddress, ""); } + /** + * @notice Overrides the msg.value in the metadata. + * @param _metadata encoded standard hook metadata. + * @param _msgValue msg.value for the message. + * @return encoded standard hook metadata. + */ function overrideMsgValue( bytes calldata _metadata, uint256 _msgValue @@ -179,6 +185,12 @@ library StandardHookMetadata { ); } + /** + * @notice Overrides the gas limit in the metadata. + * @param _metadata encoded standard hook metadata. + * @param _gasLimit gas limit for the message. + * @return encoded standard hook metadata. + */ function overrideGasLimit( bytes calldata _metadata, uint256 _gasLimit diff --git a/solidity/contracts/token/HypValue.sol b/solidity/contracts/token/HypValue.sol index ef04ab6b09..251dc2f89e 100644 --- a/solidity/contracts/token/HypValue.sol +++ b/solidity/contracts/token/HypValue.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: MIT AND Apache-2.0 +// SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity >=0.8.0; /*@@@@@@@ @@@@@@@@@ @@ -115,12 +115,12 @@ contract HypValue is TokenRouter { /** * @inheritdoc TokenRouter - * @dev No metadata is needed for value transfers + * @dev No token metadata is needed for value transfers */ function _transferFromSender( uint256 ) internal pure override returns (bytes memory) { - return bytes(""); // no metadata + return bytes(""); // no token metadata } /** @@ -130,7 +130,7 @@ contract HypValue is TokenRouter { function _transferTo( address _recipient, uint256 _amount, - bytes calldata // no metadata + bytes calldata // no token metadata ) internal virtual override { Address.sendValue(payable(_recipient), _amount); } diff --git a/solidity/test/token/HypValue.t.sol b/solidity/test/token/HypValue.t.sol index 5165a24555..3dc1ae7df1 100644 --- a/solidity/test/token/HypValue.t.sol +++ b/solidity/test/token/HypValue.t.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity >=0.8.0; import {TypeCasts} from "../../contracts/libs/TypeCasts.sol"; From 670912bb14667ed76009d67a0677872ef25017a7 Mon Sep 17 00:00:00 2001 From: -f Date: Fri, 8 Nov 2024 13:13:33 +0530 Subject: [PATCH 07/23] fixes --- solidity/contracts/token/HypValue.sol | 63 ++++++++++++--------------- solidity/test/token/HypValue.t.sol | 44 ++++++++++++++++--- 2 files changed, 64 insertions(+), 43 deletions(-) diff --git a/solidity/contracts/token/HypValue.sol b/solidity/contracts/token/HypValue.sol index 251dc2f89e..743012c506 100644 --- a/solidity/contracts/token/HypValue.sol +++ b/solidity/contracts/token/HypValue.sol @@ -26,8 +26,15 @@ import {Address} from "@openzeppelin/contracts/utils/Address.sol"; * @notice This contract facilitates the transfer of value between chains using value transfer hooks */ contract HypValue is TokenRouter { + /** + * @dev Emitted when native tokens are donated to the contract. + * @param sender The address of the sender. + * @param amount The amount of native tokens donated. + */ + event Donation(address indexed sender, uint256 amount); // ============ Errors ============ - error InsufficientValue(uint256 amount, uint256 value); + + error InsufficientValue(uint256 requiredValue, uint256 providedValue); constructor(address _mailbox) TokenRouter(_mailbox) {} @@ -63,8 +70,15 @@ contract HypValue is TokenRouter { uint256 _amount, bytes calldata _hookMetadata, address _hook - ) external payable virtual override returns (bytes32 messageId) { - uint256 quote = _checkSufficientValue(_destination, _amount, _hook); + ) public payable virtual override returns (bytes32 messageId) { + uint256 quote = _GasRouter_quoteDispatch( + _destination, + _hookMetadata, + _hook + ); + if (msg.value < _amount + quote) { + revert InsufficientValue(_amount + quote, msg.value); + } bytes memory hookMetadata = StandardHookMetadata.overrideMsgValue( _hookMetadata, @@ -88,25 +102,17 @@ contract HypValue is TokenRouter { bytes32 _recipient, uint256 _amount ) external payable virtual override returns (bytes32 messageId) { - uint256 quote = _checkSufficientValue( - _destination, - _amount, - address(hook) - ); - bytes memory hookMetadata = StandardHookMetadata.formatMetadata( - _amount, - destinationGas[_destination], - msg.sender, - "" - ); - + bytes calldata emptyBytes; + assembly { + emptyBytes.length := 0 + emptyBytes.offset := 0 + } return - _transferRemote( + transferRemote( _destination, _recipient, _amount, - _amount + quote, - hookMetadata, + emptyBytes, address(hook) ); } @@ -137,7 +143,7 @@ contract HypValue is TokenRouter { /** * @inheritdoc TokenRouter - * @dev This contract doesn't hold value + * @dev The user will hold native value */ function balanceOf( address /* _account */ @@ -145,22 +151,7 @@ contract HypValue is TokenRouter { return 0; } - /// @dev Checks if the provided value is sufficient for the transfer - function _checkSufficientValue( - uint32 _destination, - uint256 _amount, - address _hook - ) internal view returns (uint256) { - uint256 quote = _GasRouter_quoteDispatch( - _destination, - new bytes(0), - _hook - ); - if (msg.value < _amount + quote) { - revert InsufficientValue(_amount + quote, msg.value); - } - return quote; + receive() external payable { + emit Donation(msg.sender, msg.value); } - - receive() external payable {} } diff --git a/solidity/test/token/HypValue.t.sol b/solidity/test/token/HypValue.t.sol index 3dc1ae7df1..63a82b5732 100644 --- a/solidity/test/token/HypValue.t.sol +++ b/solidity/test/token/HypValue.t.sol @@ -20,6 +20,8 @@ contract HypValueTest is HypTokenTest { address internal constant L2_MESSENGER_ADDRESS = 0x4200000000000000000000000000000000000007; + uint256 internal constant OP_BRIDGE_GAS_LIMIT = 100_000; + uint256 internal constant MOCK_NONCE = 0; HypValue internal localValueRouter; HypValue internal remoteValueRouter; @@ -76,7 +78,7 @@ contract HypValueTest is HypTokenTest { address(localToken).addressToBytes32() ); - vm.deal(ALICE, 1000e18); + vm.deal(ALICE, TRANSFER_AMT * 10); } function testRemoteTransfer() public { @@ -125,7 +127,7 @@ contract HypValueTest is HypTokenTest { ) ); vm.prank(ALICE); - bytes32 messageId = localToken.transferRemote{value: TRANSFER_AMT}( + localToken.transferRemote{value: TRANSFER_AMT}( DESTINATION, BOB.addressToBytes32(), TRANSFER_AMT @@ -144,7 +146,7 @@ contract HypValueTest is HypTokenTest { hook.setFee(fee); vm.prank(ALICE); - bytes32 messageId = localToken.transferRemote{value: msgValue}( + localToken.transferRemote{value: msgValue}( DESTINATION, BOB.addressToBytes32(), TRANSFER_AMT, @@ -156,6 +158,34 @@ contract HypValueTest is HypTokenTest { vm.assertEq(address(valueHook).balance, 0); } + function testTransfer_withHookSpecified_revertsInsufficientValue( + uint256 fee, + bytes calldata metadata + ) public { + vm.assume(fee < TRANSFER_AMT); + uint256 msgValue = TRANSFER_AMT + fee; + vm.deal(ALICE, msgValue); + + TestPostDispatchHook hook = new TestPostDispatchHook(); + hook.setFee(fee); + + vm.prank(ALICE); + vm.expectRevert( + abi.encodeWithSelector( + HypValue.InsufficientValue.selector, + msgValue, + msgValue - 1 + ) + ); + localToken.transferRemote{value: msgValue - 1}( + DESTINATION, + BOB.addressToBytes32(), + TRANSFER_AMT, + metadata, + address(hook) + ); + } + function testBenchmark_overheadGasUsage() public override { vm.deal(address(localValueRouter), TRANSFER_AMT); super.testBenchmark_overheadGasUsage(); @@ -174,22 +204,22 @@ contract HypValueTest is HypTokenTest { bytes memory messengerCalldata = abi.encodeCall( ICrossDomainMessenger.relayMessage, ( - 0, + MOCK_NONCE, address(valueHook), address(ism), _msgValue, - uint256(100_000), + OP_BRIDGE_GAS_LIMIT, encodedHookData ) ); vm.deal(address(portal), _msgValue); IOptimismPortal.WithdrawalTransaction memory withdrawal = IOptimismPortal.WithdrawalTransaction({ - nonce: 0, + nonce: MOCK_NONCE, sender: L2_MESSENGER_ADDRESS, target: address(l1Messenger), value: _msgValue, - gasLimit: 100_000, + gasLimit: OP_BRIDGE_GAS_LIMIT, data: messengerCalldata }); portal.finalizeWithdrawalTransaction(withdrawal); From 3fec6a069e6a0a4023e3720c2fb64456d1ae5faa Mon Sep 17 00:00:00 2001 From: -f Date: Mon, 11 Nov 2024 16:56:12 +0530 Subject: [PATCH 08/23] rename --- solidity/contracts/token/HypNative.sol | 126 +++++++++----- .../contracts/token/HypNativeCollateral.sol | 107 ++++++++++++ solidity/contracts/token/HypValue.sol | 157 ------------------ ...aled.sol => HypNativeCollateralScaled.sol} | 13 +- solidity/test/isms/ExternalBridgeTest.sol | 1 - solidity/test/isms/OPL2ToL1Ism.t.sol | 10 +- solidity/test/token/HypERC20.t.sol | 10 +- .../token/{HypValue.t.sol => HypNative.t.sol} | 16 +- ....t.sol => HypNativeCollateralScaled.t.sol} | 20 +-- 9 files changed, 234 insertions(+), 226 deletions(-) create mode 100644 solidity/contracts/token/HypNativeCollateral.sol delete mode 100644 solidity/contracts/token/HypValue.sol rename solidity/contracts/token/extensions/{HypNativeScaled.sol => HypNativeCollateralScaled.sol} (86%) rename solidity/test/token/{HypValue.t.sol => HypNative.t.sol} (94%) rename solidity/test/token/{HypNativeScaled.t.sol => HypNativeCollateralScaled.t.sol} (91%) diff --git a/solidity/contracts/token/HypNative.sol b/solidity/contracts/token/HypNative.sol index cfb526fa40..730143e686 100644 --- a/solidity/contracts/token/HypNative.sol +++ b/solidity/contracts/token/HypNative.sol @@ -1,14 +1,29 @@ -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity >=0.8.0; +/*@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@ HYPERLANE @@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ +@@@@@@@@@ @@@@@@@@*/ + +// ============ Internal Imports ============ import {TokenRouter} from "./libs/TokenRouter.sol"; -import {TokenMessage} from "./libs/TokenMessage.sol"; +import {StandardHookMetadata} from "../hooks/libs/StandardHookMetadata.sol"; + +// ============ External Imports ============ import {Address} from "@openzeppelin/contracts/utils/Address.sol"; /** - * @title Hyperlane Native Token Router that extends ERC20 with remote transfer functionality. + * @title HypNative * @author Abacus Works - * @dev Supply on each chain is not constant but the aggregate supply across all chains is. + * @notice This contract facilitates the transfer of value between chains using value transfer hooks */ contract HypNative is TokenRouter { /** @@ -17,40 +32,37 @@ contract HypNative is TokenRouter { * @param amount The amount of native tokens donated. */ event Donation(address indexed sender, uint256 amount); + // ============ Errors ============ + + error InsufficientValue(uint256 requiredValue, uint256 providedValue); constructor(address _mailbox) TokenRouter(_mailbox) {} + // ============ Initialization ============ + /** - * @notice Initializes the Hyperlane router - * @param _hook The post-dispatch hook contract. - @param _interchainSecurityModule The interchain security module contract. - @param _owner The this contract. + * @notice Initializes the contract + * @param _valuehook The address of the value transfer hook + * @param _interchainSecurityModule The address of the interchain security module + * @param _owner The owner of the contract */ function initialize( - address _hook, + address _valuehook, address _interchainSecurityModule, address _owner ) public initializer { - _MailboxClient_initialize(_hook, _interchainSecurityModule, _owner); + _MailboxClient_initialize( + _valuehook, + _interchainSecurityModule, + _owner + ); } - /** - * @inheritdoc TokenRouter - * @dev uses (`msg.value` - `_amount`) as hook payment and `msg.sender` as refund address. - */ - function transferRemote( - uint32 _destination, - bytes32 _recipient, - uint256 _amount - ) external payable virtual override returns (bytes32 messageId) { - require(msg.value >= _amount, "Native: amount exceeds msg.value"); - uint256 _hookPayment = msg.value - _amount; - return _transferRemote(_destination, _recipient, _amount, _hookPayment); - } + // ============ External Functions ============ /** * @inheritdoc TokenRouter - * @dev uses (`msg.value` - `_amount`) as hook payment. + * @dev use _hook with caution, make sure that this hook can handle msg.value transfer using the metadata.msgValue() */ function transferRemote( uint32 _destination, @@ -58,49 +70,87 @@ contract HypNative is TokenRouter { uint256 _amount, bytes calldata _hookMetadata, address _hook - ) external payable virtual override returns (bytes32 messageId) { - require(msg.value >= _amount, "Native: amount exceeds msg.value"); - uint256 _hookPayment = msg.value - _amount; + ) public payable virtual override returns (bytes32 messageId) { + uint256 quote = _GasRouter_quoteDispatch( + _destination, + _hookMetadata, + _hook + ); + if (msg.value < _amount + quote) { + revert InsufficientValue(_amount + quote, msg.value); + } + + bytes memory hookMetadata = StandardHookMetadata.overrideMsgValue( + _hookMetadata, + _amount + ); + return _transferRemote( _destination, _recipient, _amount, - _hookPayment, - _hookMetadata, + _amount + quote, + hookMetadata, _hook ); } - function balanceOf( - address _account - ) external view override returns (uint256) { - return _account.balance; + /// @inheritdoc TokenRouter + function transferRemote( + uint32 _destination, + bytes32 _recipient, + uint256 _amount + ) external payable virtual override returns (bytes32 messageId) { + bytes calldata emptyBytes; + assembly { + emptyBytes.length := 0 + emptyBytes.offset := 0 + } + return + transferRemote( + _destination, + _recipient, + _amount, + emptyBytes, + address(hook) + ); } + // ============ Internal Functions ============ + /** * @inheritdoc TokenRouter - * @dev No-op because native amount is transferred in `msg.value` - * @dev Compiler will not include this in the bytecode. + * @dev No token metadata is needed for value transfers */ function _transferFromSender( uint256 ) internal pure override returns (bytes memory) { - return bytes(""); // no metadata + return bytes(""); // no token metadata } /** - * @dev Sends `_amount` of native token to `_recipient` balance. * @inheritdoc TokenRouter + * @dev Sends the value to the recipient */ function _transferTo( address _recipient, uint256 _amount, - bytes calldata // no metadata + bytes calldata // no token metadata ) internal virtual override { Address.sendValue(payable(_recipient), _amount); } + /** + * @inheritdoc TokenRouter + * @dev The user will hold native value + */ + function balanceOf( + address /* _account */ + ) external pure override returns (uint256) { + return 0; + } + receive() external payable { emit Donation(msg.sender, msg.value); } diff --git a/solidity/contracts/token/HypNativeCollateral.sol b/solidity/contracts/token/HypNativeCollateral.sol new file mode 100644 index 0000000000..ec6630d910 --- /dev/null +++ b/solidity/contracts/token/HypNativeCollateral.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.8.0; + +import {TokenRouter} from "./libs/TokenRouter.sol"; +import {TokenMessage} from "./libs/TokenMessage.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; + +/** + * @title Hyperlane Native Token Router that extends ERC20 with remote transfer functionality. + * @author Abacus Works + * @dev Supply on each chain is not constant but the aggregate supply across all chains is. + */ +contract HypNativeCollateral is TokenRouter { + /** + * @dev Emitted when native tokens are donated to the contract. + * @param sender The address of the sender. + * @param amount The amount of native tokens donated. + */ + event Donation(address indexed sender, uint256 amount); + + constructor(address _mailbox) TokenRouter(_mailbox) {} + + /** + * @notice Initializes the Hyperlane router + * @param _hook The post-dispatch hook contract. + * @param _interchainSecurityModule The interchain security module contract. + * @param _owner The this contract. + */ + function initialize( + address _hook, + address _interchainSecurityModule, + address _owner + ) public initializer { + _MailboxClient_initialize(_hook, _interchainSecurityModule, _owner); + } + + /** + * @inheritdoc TokenRouter + * @dev uses (`msg.value` - `_amount`) as hook payment and `msg.sender` as refund address. + */ + function transferRemote( + uint32 _destination, + bytes32 _recipient, + uint256 _amount + ) external payable virtual override returns (bytes32 messageId) { + require(msg.value >= _amount, "Native: amount exceeds msg.value"); + uint256 _hookPayment = msg.value - _amount; + return _transferRemote(_destination, _recipient, _amount, _hookPayment); + } + + /** + * @inheritdoc TokenRouter + * @dev uses (`msg.value` - `_amount`) as hook payment. + */ + function transferRemote( + uint32 _destination, + bytes32 _recipient, + uint256 _amount, + bytes calldata _hookMetadata, + address _hook + ) external payable virtual override returns (bytes32 messageId) { + require(msg.value >= _amount, "Native: amount exceeds msg.value"); + uint256 _hookPayment = msg.value - _amount; + return + _transferRemote( + _destination, + _recipient, + _amount, + _hookPayment, + _hookMetadata, + _hook + ); + } + + function balanceOf( + address _account + ) external view override returns (uint256) { + return _account.balance; + } + + /** + * @inheritdoc TokenRouter + * @dev No-op because native amount is transferred in `msg.value` + * @dev Compiler will not include this in the bytecode. + */ + function _transferFromSender( + uint256 + ) internal pure override returns (bytes memory) { + return bytes(""); // no metadata + } + + /** + * @dev Sends `_amount` of native token to `_recipient` balance. + * @inheritdoc TokenRouter + */ + function _transferTo( + address _recipient, + uint256 _amount, + bytes calldata // no metadata + ) internal virtual override { + Address.sendValue(payable(_recipient), _amount); + } + + receive() external payable { + emit Donation(msg.sender, msg.value); + } +} diff --git a/solidity/contracts/token/HypValue.sol b/solidity/contracts/token/HypValue.sol deleted file mode 100644 index 743012c506..0000000000 --- a/solidity/contracts/token/HypValue.sol +++ /dev/null @@ -1,157 +0,0 @@ -// SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity >=0.8.0; - -/*@@@@@@@ @@@@@@@@@ - @@@@@@@@@ @@@@@@@@@ - @@@@@@@@@ @@@@@@@@@ - @@@@@@@@@ @@@@@@@@@ - @@@@@@@@@@@@@@@@@@@@@@@@@ - @@@@@ HYPERLANE @@@@@@@ - @@@@@@@@@@@@@@@@@@@@@@@@@ - @@@@@@@@@ @@@@@@@@@ - @@@@@@@@@ @@@@@@@@@ - @@@@@@@@@ @@@@@@@@@ -@@@@@@@@@ @@@@@@@@*/ - -// ============ Internal Imports ============ -import {TokenRouter} from "./libs/TokenRouter.sol"; -import {StandardHookMetadata} from "../hooks/libs/StandardHookMetadata.sol"; - -// ============ External Imports ============ -import {Address} from "@openzeppelin/contracts/utils/Address.sol"; - -/** - * @title HypValue - * @author Abacus Works - * @notice This contract facilitates the transfer of value between chains using value transfer hooks - */ -contract HypValue is TokenRouter { - /** - * @dev Emitted when native tokens are donated to the contract. - * @param sender The address of the sender. - * @param amount The amount of native tokens donated. - */ - event Donation(address indexed sender, uint256 amount); - // ============ Errors ============ - - error InsufficientValue(uint256 requiredValue, uint256 providedValue); - - constructor(address _mailbox) TokenRouter(_mailbox) {} - - // ============ Initialization ============ - - /** - * @notice Initializes the contract - * @param _valuehook The address of the value transfer hook - * @param _interchainSecurityModule The address of the interchain security module - * @param _owner The owner of the contract - */ - function initialize( - address _valuehook, - address _interchainSecurityModule, - address _owner - ) public initializer { - _MailboxClient_initialize( - _valuehook, - _interchainSecurityModule, - _owner - ); - } - - // ============ External Functions ============ - - /** - * @inheritdoc TokenRouter - * @dev use _hook with caution, make sure that this hook can handle msg.value transfer using the metadata.msgValue() - */ - function transferRemote( - uint32 _destination, - bytes32 _recipient, - uint256 _amount, - bytes calldata _hookMetadata, - address _hook - ) public payable virtual override returns (bytes32 messageId) { - uint256 quote = _GasRouter_quoteDispatch( - _destination, - _hookMetadata, - _hook - ); - if (msg.value < _amount + quote) { - revert InsufficientValue(_amount + quote, msg.value); - } - - bytes memory hookMetadata = StandardHookMetadata.overrideMsgValue( - _hookMetadata, - _amount - ); - - return - _transferRemote( - _destination, - _recipient, - _amount, - _amount + quote, - hookMetadata, - _hook - ); - } - - /// @inheritdoc TokenRouter - function transferRemote( - uint32 _destination, - bytes32 _recipient, - uint256 _amount - ) external payable virtual override returns (bytes32 messageId) { - bytes calldata emptyBytes; - assembly { - emptyBytes.length := 0 - emptyBytes.offset := 0 - } - return - transferRemote( - _destination, - _recipient, - _amount, - emptyBytes, - address(hook) - ); - } - - // ============ Internal Functions ============ - - /** - * @inheritdoc TokenRouter - * @dev No token metadata is needed for value transfers - */ - function _transferFromSender( - uint256 - ) internal pure override returns (bytes memory) { - return bytes(""); // no token metadata - } - - /** - * @inheritdoc TokenRouter - * @dev Sends the value to the recipient - */ - function _transferTo( - address _recipient, - uint256 _amount, - bytes calldata // no token metadata - ) internal virtual override { - Address.sendValue(payable(_recipient), _amount); - } - - /** - * @inheritdoc TokenRouter - * @dev The user will hold native value - */ - function balanceOf( - address /* _account */ - ) external pure override returns (uint256) { - return 0; - } - - receive() external payable { - emit Donation(msg.sender, msg.value); - } -} diff --git a/solidity/contracts/token/extensions/HypNativeScaled.sol b/solidity/contracts/token/extensions/HypNativeCollateralScaled.sol similarity index 86% rename from solidity/contracts/token/extensions/HypNativeScaled.sol rename to solidity/contracts/token/extensions/HypNativeCollateralScaled.sol index ad129fce80..c12117a6d5 100644 --- a/solidity/contracts/token/extensions/HypNativeScaled.sol +++ b/solidity/contracts/token/extensions/HypNativeCollateralScaled.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity >=0.8.0; -import {HypNative} from "../HypNative.sol"; +import {HypNativeCollateral} from "../HypNativeCollateral.sol"; import {TokenRouter} from "../libs/TokenRouter.sol"; /** @@ -10,15 +10,18 @@ import {TokenRouter} from "../libs/TokenRouter.sol"; * Conversely, it divides the local native `msg.value` amount by `scale` to encode the `message.amount`. * @author Abacus Works */ -contract HypNativeScaled is HypNative { +contract HypNativeCollateralScaled is HypNativeCollateral { uint256 public immutable scale; - constructor(uint256 _scale, address _mailbox) HypNative(_mailbox) { + constructor( + uint256 _scale, + address _mailbox + ) HypNativeCollateral(_mailbox) { scale = _scale; } /** - * @inheritdoc HypNative + * @inheritdoc HypNativeCollateral * @dev Sends scaled `msg.value` (divided by `scale`) to `_recipient`. */ function transferRemote( @@ -73,6 +76,6 @@ contract HypNativeScaled is HypNative { bytes calldata metadata // no metadata ) internal override { uint256 scaledAmount = _amount * scale; - HypNative._transferTo(_recipient, scaledAmount, metadata); + HypNativeCollateral._transferTo(_recipient, scaledAmount, metadata); } } diff --git a/solidity/test/isms/ExternalBridgeTest.sol b/solidity/test/isms/ExternalBridgeTest.sol index 937e39311a..e586297822 100644 --- a/solidity/test/isms/ExternalBridgeTest.sol +++ b/solidity/test/isms/ExternalBridgeTest.sol @@ -115,7 +115,6 @@ abstract contract ExternalBridgeTest is Test { function test_postDispatch_revertWhen_insufficientValue() public { bytes memory encodedHookData = _encodeHookData(messageId, 0); originMailbox.updateLatestDispatchedId(messageId); - _expectOriginExternalBridgeCall(encodedHookData); uint256 quote = hook.quoteDispatch(testMetadata, encodedMessage); diff --git a/solidity/test/isms/OPL2ToL1Ism.t.sol b/solidity/test/isms/OPL2ToL1Ism.t.sol index 9322713d5e..195a0704c0 100644 --- a/solidity/test/isms/OPL2ToL1Ism.t.sol +++ b/solidity/test/isms/OPL2ToL1Ism.t.sol @@ -21,6 +21,7 @@ contract OPL2ToL1IsmTest is ExternalBridgeTest { 0x4200000000000000000000000000000000000007; uint256 internal constant MOCK_NONCE = 0; + uint256 internal constant GAS_LIMIT = 300_000; TestInterchainGasPaymaster internal mockOverheadIgp; MockOptimismPortal internal portal; @@ -40,6 +41,8 @@ contract OPL2ToL1IsmTest is ExternalBridgeTest { deployAll(); super.setUp(); + + GAS_QUOTE = GAS_LIMIT * 10; } function deployHook() public { @@ -76,11 +79,14 @@ contract OPL2ToL1IsmTest is ExternalBridgeTest { _expectOriginExternalBridgeCall(encodedHookData); bytes memory igpMetadata = StandardHookMetadata.overrideGasLimit( - 78_000 + GAS_LIMIT ); uint256 quote = hook.quoteDispatch(igpMetadata, encodedMessage); - assertEq(quote, mockOverheadIgp.quoteGasPayment(ORIGIN_DOMAIN, 78_000)); + assertEq( + quote, + mockOverheadIgp.quoteGasPayment(ORIGIN_DOMAIN, GAS_LIMIT) + ); hook.postDispatch{value: quote}(igpMetadata, encodedMessage); } diff --git a/solidity/test/token/HypERC20.t.sol b/solidity/test/token/HypERC20.t.sol index c3b7da74cd..66acba13d6 100644 --- a/solidity/test/token/HypERC20.t.sol +++ b/solidity/test/token/HypERC20.t.sol @@ -33,7 +33,7 @@ import {IXERC20} from "../../contracts/token/interfaces/IXERC20.sol"; import {IFiatToken} from "../../contracts/token/interfaces/IFiatToken.sol"; import {HypXERC20} from "../../contracts/token/extensions/HypXERC20.sol"; import {HypFiatToken} from "../../contracts/token/extensions/HypFiatToken.sol"; -import {HypNative} from "../../contracts/token/HypNative.sol"; +import {HypNativeCollateral} from "../../contracts/token/HypNativeCollateral.sol"; import {TokenRouter} from "../../contracts/token/libs/TokenRouter.sol"; import {TokenMessage} from "../../contracts/token/libs/TokenMessage.sol"; import {Message} from "../../contracts/libs/Message.sol"; @@ -623,16 +623,16 @@ contract HypFiatTokenTest is HypTokenTest { } } -contract HypNativeTest is HypTokenTest { +contract HypNativeCollateralTest is HypTokenTest { using TypeCasts for address; - HypNative internal nativeToken; + HypNativeCollateral internal nativeToken; function setUp() public override { super.setUp(); - localToken = new HypNative(address(localMailbox)); - nativeToken = HypNative(payable(address(localToken))); + localToken = new HypNativeCollateral(address(localMailbox)); + nativeToken = HypNativeCollateral(payable(address(localToken))); nativeToken.enrollRemoteRouter( DESTINATION, diff --git a/solidity/test/token/HypValue.t.sol b/solidity/test/token/HypNative.t.sol similarity index 94% rename from solidity/test/token/HypValue.t.sol rename to solidity/test/token/HypNative.t.sol index 63a82b5732..bac841b42d 100644 --- a/solidity/test/token/HypValue.t.sol +++ b/solidity/test/token/HypNative.t.sol @@ -5,7 +5,7 @@ import {TypeCasts} from "../../contracts/libs/TypeCasts.sol"; import {HypTokenTest} from "./HypERC20.t.sol"; import {HypERC20} from "../../contracts/token/HypERC20.sol"; import {TokenRouter} from "../../contracts/token/libs/TokenRouter.sol"; -import {HypValue} from "../../contracts/token/HypValue.sol"; +import {HypNative} from "../../contracts/token/HypNative.sol"; import {OPL2ToL1Hook} from "../../contracts/hooks/OPL2ToL1Hook.sol"; import {OPL2ToL1Ism} from "../../contracts/isms/hook/OPL2ToL1Ism.sol"; import {IOptimismPortal} from "../../contracts/interfaces/optimism/IOptimismPortal.sol"; @@ -15,7 +15,7 @@ import {AbstractMessageIdAuthorizedIsm} from "../../contracts/isms/hook/Abstract import {MockOptimismMessenger, MockOptimismPortal} from "../../contracts/mock/MockOptimism.sol"; import {TestInterchainGasPaymaster} from "../../contracts/test/TestInterchainGasPaymaster.sol"; -contract HypValueTest is HypTokenTest { +contract HypNativeTest is HypTokenTest { using TypeCasts for address; address internal constant L2_MESSENGER_ADDRESS = @@ -23,8 +23,8 @@ contract HypValueTest is HypTokenTest { uint256 internal constant OP_BRIDGE_GAS_LIMIT = 100_000; uint256 internal constant MOCK_NONCE = 0; - HypValue internal localValueRouter; - HypValue internal remoteValueRouter; + HypNative internal localValueRouter; + HypNative internal remoteValueRouter; OPL2ToL1Hook internal valueHook; OPL2ToL1Ism internal ism; TestInterchainGasPaymaster internal mockOverheadIgp; @@ -38,8 +38,8 @@ contract HypValueTest is HypTokenTest { address(new MockOptimismMessenger()).code ); - localValueRouter = new HypValue(address(localMailbox)); - remoteValueRouter = new HypValue(address(remoteMailbox)); + localValueRouter = new HypNative(address(localMailbox)); + remoteValueRouter = new HypNative(address(remoteMailbox)); localToken = TokenRouter(payable(address(localValueRouter))); remoteToken = HypERC20(payable(address(remoteValueRouter))); @@ -121,7 +121,7 @@ contract HypValueTest is HypTokenTest { vm.expectRevert( abi.encodeWithSelector( - HypValue.InsufficientValue.selector, + HypNative.InsufficientValue.selector, TRANSFER_AMT + quote, TRANSFER_AMT ) @@ -172,7 +172,7 @@ contract HypValueTest is HypTokenTest { vm.prank(ALICE); vm.expectRevert( abi.encodeWithSelector( - HypValue.InsufficientValue.selector, + HypNative.InsufficientValue.selector, msgValue, msgValue - 1 ) diff --git a/solidity/test/token/HypNativeScaled.t.sol b/solidity/test/token/HypNativeCollateralScaled.t.sol similarity index 91% rename from solidity/test/token/HypNativeScaled.t.sol rename to solidity/test/token/HypNativeCollateralScaled.t.sol index ffded65655..d867caf7c6 100644 --- a/solidity/test/token/HypNativeScaled.t.sol +++ b/solidity/test/token/HypNativeCollateralScaled.t.sol @@ -5,13 +5,13 @@ import "forge-std/Test.sol"; import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; import {TestPostDispatchHook} from "../../contracts/test/TestPostDispatchHook.sol"; -import {HypNativeScaled} from "../../contracts/token/extensions/HypNativeScaled.sol"; +import {HypNativeCollateralScaled} from "../../contracts/token/extensions/HypNativeCollateralScaled.sol"; import {HypERC20} from "../../contracts/token/HypERC20.sol"; -import {HypNative} from "../../contracts/token/HypNative.sol"; +import {HypNativeCollateral} from "../../contracts/token/HypNativeCollateral.sol"; import {TypeCasts} from "../../contracts/libs/TypeCasts.sol"; import {MockHyperlaneEnvironment} from "../../contracts/mock/MockHyperlaneEnvironment.sol"; -contract HypNativeScaledTest is Test { +contract HypNativeCollateralScaledTest is Test { uint32 nativeDomain = 1; uint32 synthDomain = 2; @@ -34,7 +34,7 @@ contract HypNativeScaledTest is Test { uint256 amount ); - HypNativeScaled native; + HypNativeCollateralScaled native; HypERC20 synth; MockHyperlaneEnvironment environment; @@ -61,22 +61,22 @@ contract HypNativeScaledTest is Test { ); synth = HypERC20(address(proxySynth)); - HypNativeScaled implementationNative = new HypNativeScaled( - scale, - address(environment.mailboxes(nativeDomain)) - ); + HypNativeCollateralScaled implementationNative = new HypNativeCollateralScaled( + scale, + address(environment.mailboxes(nativeDomain)) + ); TransparentUpgradeableProxy proxyNative = new TransparentUpgradeableProxy( address(implementationNative), address(9), abi.encodeWithSelector( - HypNative.initialize.selector, + HypNativeCollateral.initialize.selector, address(0), address(0), address(this) ) ); - native = HypNativeScaled(payable(address(proxyNative))); + native = HypNativeCollateralScaled(payable(address(proxyNative))); native.enrollRemoteRouter( synthDomain, From 1e54bf6d62670724a4f4fbcbfc8125148c70b199 Mon Sep 17 00:00:00 2001 From: -f Date: Mon, 11 Nov 2024 17:02:34 +0530 Subject: [PATCH 09/23] account.balance --- solidity/contracts/token/HypNative.sol | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/solidity/contracts/token/HypNative.sol b/solidity/contracts/token/HypNative.sol index 730143e686..dfee1a503b 100644 --- a/solidity/contracts/token/HypNative.sol +++ b/solidity/contracts/token/HypNative.sol @@ -141,14 +141,11 @@ contract HypNative is TokenRouter { Address.sendValue(payable(_recipient), _amount); } - /** - * @inheritdoc TokenRouter - * @dev The user will hold native value - */ + /// @inheritdoc TokenRouter function balanceOf( - address /* _account */ - ) external pure override returns (uint256) { - return 0; + address _account + ) external view override returns (uint256) { + return _account.balance; } receive() external payable { From d8d4d2f3c4dea17b1157817521a9abf193299de4 Mon Sep 17 00:00:00 2001 From: -f Date: Wed, 20 Nov 2024 11:59:19 +0530 Subject: [PATCH 10/23] resolve sdk imports --- .../warp-routes/monitor-warp-routes-balances.ts | 2 +- typescript/sdk/src/metadata/warpRouteConfig.ts | 2 +- .../sdk/src/token/EvmERC20WarpModule.hardhat-test.ts | 4 ++-- typescript/sdk/src/token/contracts.ts | 12 ++++++------ 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/typescript/infra/scripts/warp-routes/monitor-warp-routes-balances.ts b/typescript/infra/scripts/warp-routes/monitor-warp-routes-balances.ts index fd4da97f57..1c059f6cdb 100644 --- a/typescript/infra/scripts/warp-routes/monitor-warp-routes-balances.ts +++ b/typescript/infra/scripts/warp-routes/monitor-warp-routes-balances.ts @@ -74,7 +74,7 @@ const warpRouteTokenBalance = new Gauge({ const warpRouteCollateralValue = new Gauge({ name: 'hyperlane_warp_route_collateral_value', - help: 'Total value of collateral held in a HypERC20Collateral or HypNative contract of a Warp Route', + help: 'Total value of collateral held in a HypERC20Collateral or HypNativeCollateral contract of a Warp Route', registers: [metricsRegister], labelNames: warpRouteMetricLabels, }); diff --git a/typescript/sdk/src/metadata/warpRouteConfig.ts b/typescript/sdk/src/metadata/warpRouteConfig.ts index 8c1ee346c0..b781c1887a 100644 --- a/typescript/sdk/src/metadata/warpRouteConfig.ts +++ b/typescript/sdk/src/metadata/warpRouteConfig.ts @@ -8,7 +8,7 @@ import { ChainMap } from '../types.js'; const TokenConfigSchema = z.object({ protocolType: z.nativeEnum(ProtocolType), type: z.nativeEnum(TokenType), - hypAddress: z.string(), // HypERC20Collateral, HypERC20Synthetic, HypNativeToken address + hypAddress: z.string(), // HypERC20Collateral, HypERC20Synthetic, HypNativeCollateralToken address tokenAddress: z.string().optional(), // external token address needed for collateral type eg tokenAddress.balanceOf(hypAddress) tokenCoinGeckoId: z.string().optional(), // CoinGecko id for token name: z.string(), diff --git a/typescript/sdk/src/token/EvmERC20WarpModule.hardhat-test.ts b/typescript/sdk/src/token/EvmERC20WarpModule.hardhat-test.ts index 5895d783fc..d204aa1709 100644 --- a/typescript/sdk/src/token/EvmERC20WarpModule.hardhat-test.ts +++ b/typescript/sdk/src/token/EvmERC20WarpModule.hardhat-test.ts @@ -10,7 +10,7 @@ import { GasRouter, HypERC20__factory, HypERC4626Collateral__factory, - HypNative__factory, + HypNativeCollateral__factory, Mailbox, Mailbox__factory, } from '@hyperlane-xyz/core'; @@ -228,7 +228,7 @@ describe('EvmERC20WarpHyperlaneModule', async () => { expect(tokenType).to.equal(TokenType.native); // Validate onchain token values - const nativeContract = HypNative__factory.connect( + const nativeContract = HypNativeCollateral__factory.connect( deployedTokenRoute, signer, ); diff --git a/typescript/sdk/src/token/contracts.ts b/typescript/sdk/src/token/contracts.ts index a9ced3cb11..32a684cf30 100644 --- a/typescript/sdk/src/token/contracts.ts +++ b/typescript/sdk/src/token/contracts.ts @@ -11,8 +11,8 @@ import { HypERC4626OwnerCollateral__factory, HypERC4626__factory, HypFiatToken__factory, - HypNativeScaled__factory, - HypNative__factory, + HypNativeCollateralScaled__factory, + HypNativeCollateral__factory, HypXERC20Lockbox__factory, HypXERC20__factory, } from '@hyperlane-xyz/core'; @@ -30,8 +30,8 @@ export const hypERC20contracts = { [TokenType.XERC20Lockbox]: 'HypXERC20Lockbox', [TokenType.collateralVault]: 'HypERC4626OwnerCollateral', [TokenType.collateralVaultRebase]: 'HypERC4626Collateral', - [TokenType.native]: 'HypNative', - [TokenType.nativeScaled]: 'HypNativeScaled', + [TokenType.native]: 'HypNativeCollateral', + [TokenType.nativeScaled]: 'HypNativeCollateralScaled', }; export type HypERC20contracts = typeof hypERC20contracts; @@ -46,8 +46,8 @@ export const hypERC20factories = { [TokenType.collateralFiat]: new HypFiatToken__factory(), [TokenType.XERC20]: new HypXERC20__factory(), [TokenType.XERC20Lockbox]: new HypXERC20Lockbox__factory(), - [TokenType.native]: new HypNative__factory(), - [TokenType.nativeScaled]: new HypNativeScaled__factory(), + [TokenType.native]: new HypNativeCollateral__factory(), + [TokenType.nativeScaled]: new HypNativeCollateralScaled__factory(), }; export type HypERC20Factories = typeof hypERC20factories; From ecd1e90057812989bf62aea6a9cdd542348788c8 Mon Sep 17 00:00:00 2001 From: -f Date: Wed, 27 Nov 2024 09:07:47 +0530 Subject: [PATCH 11/23] override if more --- .../hooks/libs/StandardHookMetadata.sol | 19 +++++++++++-------- solidity/contracts/token/HypNative.sol | 4 +--- solidity/test/isms/OPL2ToL1Ism.t.sol | 15 +++++++++++++-- solidity/test/token/HypNative.t.sol | 16 ++-------------- 4 files changed, 27 insertions(+), 27 deletions(-) diff --git a/solidity/contracts/hooks/libs/StandardHookMetadata.sol b/solidity/contracts/hooks/libs/StandardHookMetadata.sol index 72f84e06f1..d68a3e943e 100644 --- a/solidity/contracts/hooks/libs/StandardHookMetadata.sol +++ b/solidity/contracts/hooks/libs/StandardHookMetadata.sol @@ -186,7 +186,7 @@ library StandardHookMetadata { } /** - * @notice Overrides the gas limit in the metadata. + * @notice Overrides the gas limit in the metadata if _gasLimit is higher than the current gas limit. * @param _metadata encoded standard hook metadata. * @param _gasLimit gas limit for the message. * @return encoded standard hook metadata. @@ -195,12 +195,15 @@ library StandardHookMetadata { bytes calldata _metadata, uint256 _gasLimit ) internal view returns (bytes memory) { - return - formatMetadata( - msgValue(_metadata, 0), - _gasLimit, - refundAddress(_metadata, msg.sender), - getCustomMetadata(_metadata) - ); + if (gasLimit(_metadata, 0) < _gasLimit) { + return + formatMetadata( + msgValue(_metadata, 0), + _gasLimit, + refundAddress(_metadata, msg.sender), + getCustomMetadata(_metadata) + ); + } + return _metadata; } } diff --git a/solidity/contracts/token/HypNative.sol b/solidity/contracts/token/HypNative.sol index dfee1a503b..aa473c6e33 100644 --- a/solidity/contracts/token/HypNative.sol +++ b/solidity/contracts/token/HypNative.sol @@ -76,9 +76,7 @@ contract HypNative is TokenRouter { _hookMetadata, _hook ); - if (msg.value < _amount + quote) { - revert InsufficientValue(_amount + quote, msg.value); - } + require(msg.value >= _amount + quote, "HypNative: insufficient value"); bytes memory hookMetadata = StandardHookMetadata.overrideMsgValue( _hookMetadata, diff --git a/solidity/test/isms/OPL2ToL1Ism.t.sol b/solidity/test/isms/OPL2ToL1Ism.t.sol index 195a0704c0..9aa31b9f8f 100644 --- a/solidity/test/isms/OPL2ToL1Ism.t.sol +++ b/solidity/test/isms/OPL2ToL1Ism.t.sol @@ -22,7 +22,8 @@ contract OPL2ToL1IsmTest is ExternalBridgeTest { uint256 internal constant MOCK_NONCE = 0; uint256 internal constant GAS_LIMIT = 300_000; - + uint256 internal constant GAS_PRICE = 10; + uint256 internal constant OVERRIDE_MULTIPLIER = 2; TestInterchainGasPaymaster internal mockOverheadIgp; MockOptimismPortal internal portal; MockOptimismMessenger internal l1Messenger; @@ -42,7 +43,7 @@ contract OPL2ToL1IsmTest is ExternalBridgeTest { deployAll(); super.setUp(); - GAS_QUOTE = GAS_LIMIT * 10; + GAS_QUOTE = GAS_LIMIT * GAS_PRICE; } function deployHook() public { @@ -90,6 +91,16 @@ contract OPL2ToL1IsmTest is ExternalBridgeTest { hook.postDispatch{value: quote}(igpMetadata, encodedMessage); } + function test_quoteDispatch_dontOverrideGasLimit() public { + bytes memory igpMetadata = StandardHookMetadata.overrideGasLimit( + GAS_LIMIT * OVERRIDE_MULTIPLIER + ); + assertEq( + hook.quoteDispatch(igpMetadata, encodedMessage), + GAS_LIMIT * GAS_PRICE * OVERRIDE_MULTIPLIER + ); + } + /* ============ helper functions ============ */ function _expectOriginExternalBridgeCall( diff --git a/solidity/test/token/HypNative.t.sol b/solidity/test/token/HypNative.t.sol index bac841b42d..28628f64ce 100644 --- a/solidity/test/token/HypNative.t.sol +++ b/solidity/test/token/HypNative.t.sol @@ -119,13 +119,7 @@ contract HypNativeTest is HypTokenTest { function testRemoteTransfer_invalidAmount() public { uint256 quote = localValueRouter.quoteGasPayment(DESTINATION); - vm.expectRevert( - abi.encodeWithSelector( - HypNative.InsufficientValue.selector, - TRANSFER_AMT + quote, - TRANSFER_AMT - ) - ); + vm.expectRevert("HypNative: insufficient value"); vm.prank(ALICE); localToken.transferRemote{value: TRANSFER_AMT}( DESTINATION, @@ -170,13 +164,7 @@ contract HypNativeTest is HypTokenTest { hook.setFee(fee); vm.prank(ALICE); - vm.expectRevert( - abi.encodeWithSelector( - HypNative.InsufficientValue.selector, - msgValue, - msgValue - 1 - ) - ); + vm.expectRevert("HypNative: insufficient value"); localToken.transferRemote{value: msgValue - 1}( DESTINATION, BOB.addressToBytes32(), From 8fb963c0487a6b5a27308c4457a0b55978e7ef2d Mon Sep 17 00:00:00 2001 From: -f Date: Thu, 28 Nov 2024 14:28:12 +0530 Subject: [PATCH 12/23] testpostdispatchhook --- solidity/test/token/HypNative.t.sol | 90 +++++------------------------ 1 file changed, 14 insertions(+), 76 deletions(-) diff --git a/solidity/test/token/HypNative.t.sol b/solidity/test/token/HypNative.t.sol index 28628f64ce..f66b0836db 100644 --- a/solidity/test/token/HypNative.t.sol +++ b/solidity/test/token/HypNative.t.sol @@ -6,37 +6,19 @@ import {HypTokenTest} from "./HypERC20.t.sol"; import {HypERC20} from "../../contracts/token/HypERC20.sol"; import {TokenRouter} from "../../contracts/token/libs/TokenRouter.sol"; import {HypNative} from "../../contracts/token/HypNative.sol"; -import {OPL2ToL1Hook} from "../../contracts/hooks/OPL2ToL1Hook.sol"; -import {OPL2ToL1Ism} from "../../contracts/isms/hook/OPL2ToL1Ism.sol"; -import {IOptimismPortal} from "../../contracts/interfaces/optimism/IOptimismPortal.sol"; import {TestPostDispatchHook} from "../../contracts/test/TestPostDispatchHook.sol"; -import {ICrossDomainMessenger} from "../../contracts/interfaces/optimism/ICrossDomainMessenger.sol"; -import {AbstractMessageIdAuthorizedIsm} from "../../contracts/isms/hook/AbstractMessageIdAuthorizedIsm.sol"; -import {MockOptimismMessenger, MockOptimismPortal} from "../../contracts/mock/MockOptimism.sol"; -import {TestInterchainGasPaymaster} from "../../contracts/test/TestInterchainGasPaymaster.sol"; +import {TestIsm} from "../../contracts/test/TestIsm.sol"; contract HypNativeTest is HypTokenTest { using TypeCasts for address; - address internal constant L2_MESSENGER_ADDRESS = - 0x4200000000000000000000000000000000000007; - uint256 internal constant OP_BRIDGE_GAS_LIMIT = 100_000; - uint256 internal constant MOCK_NONCE = 0; - HypNative internal localValueRouter; HypNative internal remoteValueRouter; - OPL2ToL1Hook internal valueHook; - OPL2ToL1Ism internal ism; - TestInterchainGasPaymaster internal mockOverheadIgp; - MockOptimismPortal internal portal; - MockOptimismMessenger internal l1Messenger; + TestPostDispatchHook internal valueHook; + TestIsm internal ism; function setUp() public override { super.setUp(); - vm.etch( - L2_MESSENGER_ADDRESS, - address(new MockOptimismMessenger()).code - ); localValueRouter = new HypNative(address(localMailbox)); remoteValueRouter = new HypNative(address(remoteMailbox)); @@ -44,19 +26,10 @@ contract HypNativeTest is HypTokenTest { localToken = TokenRouter(payable(address(localValueRouter))); remoteToken = HypERC20(payable(address(remoteValueRouter))); - l1Messenger = new MockOptimismMessenger(); - portal = new MockOptimismPortal(); - l1Messenger.setPORTAL(address(portal)); - ism = new OPL2ToL1Ism(address(l1Messenger)); + ism = new TestIsm(); - mockOverheadIgp = new TestInterchainGasPaymaster(); - valueHook = new OPL2ToL1Hook( - address(localMailbox), - DESTINATION, - address(localMailbox).addressToBytes32(), - L2_MESSENGER_ADDRESS, - address(mockOverheadIgp) - ); + valueHook = new TestPostDispatchHook(); + valueHook.setFee(1e10); localValueRouter.initialize( address(valueHook), @@ -93,27 +66,26 @@ contract HypNativeTest is HypTokenTest { ); vm.prank(ALICE); - bytes32 messageId = localToken.transferRemote{value: msgValue}( + localToken.transferRemote{value: msgValue}( DESTINATION, BOB.addressToBytes32(), TRANSFER_AMT ); vm.assertEq(address(localToken).balance, 0); - vm.assertEq(address(valueHook).balance, 0); + vm.assertEq(address(valueHook).balance, msgValue); - _externalBridgeDestinationCall(messageId, msgValue); + vm.deal(address(remoteToken), TRANSFER_AMT); + vm.prank(address(remoteMailbox)); - vm.expectEmit(true, true, false, true); - emit ReceivedTransferRemote( + remoteToken.handle( ORIGIN, - BOB.addressToBytes32(), - TRANSFER_AMT + address(localToken).addressToBytes32(), + abi.encodePacked(BOB.addressToBytes32(), TRANSFER_AMT) ); - remoteMailbox.processNextInboundMessage(); assertEq(BOB.balance, TRANSFER_AMT); - assertEq(address(mockOverheadIgp).balance, quote); + assertEq(address(valueHook).balance, msgValue); } function testRemoteTransfer_invalidAmount() public { @@ -178,38 +150,4 @@ contract HypNativeTest is HypTokenTest { vm.deal(address(localValueRouter), TRANSFER_AMT); super.testBenchmark_overheadGasUsage(); } - - // helper function to simulate the external bridge destination call using OP L2 -> L1 bridge - function _externalBridgeDestinationCall( - bytes32 _messageId, - uint256 _msgValue - ) internal { - bytes memory encodedHookData = abi.encodeCall( - AbstractMessageIdAuthorizedIsm.preVerifyMessage, - (_messageId, _msgValue) - ); - - bytes memory messengerCalldata = abi.encodeCall( - ICrossDomainMessenger.relayMessage, - ( - MOCK_NONCE, - address(valueHook), - address(ism), - _msgValue, - OP_BRIDGE_GAS_LIMIT, - encodedHookData - ) - ); - vm.deal(address(portal), _msgValue); - IOptimismPortal.WithdrawalTransaction - memory withdrawal = IOptimismPortal.WithdrawalTransaction({ - nonce: MOCK_NONCE, - sender: L2_MESSENGER_ADDRESS, - target: address(l1Messenger), - value: _msgValue, - gasLimit: OP_BRIDGE_GAS_LIMIT, - data: messengerCalldata - }); - portal.finalizeWithdrawalTransaction(withdrawal); - } } From bee6d22ee97331d4a03dc0cc21485fa95ecedbbb Mon Sep 17 00:00:00 2001 From: -f Date: Thu, 28 Nov 2024 14:29:16 +0530 Subject: [PATCH 13/23] rm quote fetch --- solidity/test/token/HypNative.t.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/solidity/test/token/HypNative.t.sol b/solidity/test/token/HypNative.t.sol index f66b0836db..6428ec186e 100644 --- a/solidity/test/token/HypNative.t.sol +++ b/solidity/test/token/HypNative.t.sol @@ -89,8 +89,6 @@ contract HypNativeTest is HypTokenTest { } function testRemoteTransfer_invalidAmount() public { - uint256 quote = localValueRouter.quoteGasPayment(DESTINATION); - vm.expectRevert("HypNative: insufficient value"); vm.prank(ALICE); localToken.transferRemote{value: TRANSFER_AMT}( From 139ba55d57cf2642c69c31d5ba8402b38e502319 Mon Sep 17 00:00:00 2001 From: -f Date: Fri, 13 Dec 2024 16:23:56 +0530 Subject: [PATCH 14/23] rm extra insuff check --- solidity/contracts/token/HypNative.sol | 2 +- solidity/test/token/HypNative.t.sol | 7 ++++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/solidity/contracts/token/HypNative.sol b/solidity/contracts/token/HypNative.sol index aa473c6e33..81480e77f8 100644 --- a/solidity/contracts/token/HypNative.sol +++ b/solidity/contracts/token/HypNative.sol @@ -76,7 +76,7 @@ contract HypNative is TokenRouter { _hookMetadata, _hook ); - require(msg.value >= _amount + quote, "HypNative: insufficient value"); + // require(msg.value >= _amount + quote, "HypNative: insufficient value"); bytes memory hookMetadata = StandardHookMetadata.overrideMsgValue( _hookMetadata, diff --git a/solidity/test/token/HypNative.t.sol b/solidity/test/token/HypNative.t.sol index 6428ec186e..eae4a8f21a 100644 --- a/solidity/test/token/HypNative.t.sol +++ b/solidity/test/token/HypNative.t.sol @@ -88,8 +88,9 @@ contract HypNativeTest is HypTokenTest { assertEq(address(valueHook).balance, msgValue); } - function testRemoteTransfer_invalidAmount() public { - vm.expectRevert("HypNative: insufficient value"); + // when msg.value is >= quote + amount, it should revert in + function testRemoteTransfer_insufficientValue() public { + vm.expectRevert(); vm.prank(ALICE); localToken.transferRemote{value: TRANSFER_AMT}( DESTINATION, @@ -134,7 +135,7 @@ contract HypNativeTest is HypTokenTest { hook.setFee(fee); vm.prank(ALICE); - vm.expectRevert("HypNative: insufficient value"); + vm.expectRevert(); localToken.transferRemote{value: msgValue - 1}( DESTINATION, BOB.addressToBytes32(), From 9fcc4d9dc4b8e65c0c10308afa9b1296b83ca2ac Mon Sep 17 00:00:00 2001 From: -f Date: Fri, 13 Dec 2024 16:24:36 +0530 Subject: [PATCH 15/23] rm comment --- solidity/contracts/token/HypNative.sol | 1 - 1 file changed, 1 deletion(-) diff --git a/solidity/contracts/token/HypNative.sol b/solidity/contracts/token/HypNative.sol index 81480e77f8..1c72daf0c7 100644 --- a/solidity/contracts/token/HypNative.sol +++ b/solidity/contracts/token/HypNative.sol @@ -76,7 +76,6 @@ contract HypNative is TokenRouter { _hookMetadata, _hook ); - // require(msg.value >= _amount + quote, "HypNative: insufficient value"); bytes memory hookMetadata = StandardHookMetadata.overrideMsgValue( _hookMetadata, From 41ff0833840d7f4634dad10d9853924b457e0e18 Mon Sep 17 00:00:00 2001 From: -f Date: Fri, 13 Dec 2024 16:40:12 +0530 Subject: [PATCH 16/23] inherit from hypnativecollateral --- solidity/contracts/token/HypNative.sol | 113 +++++-------------------- 1 file changed, 23 insertions(+), 90 deletions(-) diff --git a/solidity/contracts/token/HypNative.sol b/solidity/contracts/token/HypNative.sol index 1c72daf0c7..8a7ae06d72 100644 --- a/solidity/contracts/token/HypNative.sol +++ b/solidity/contracts/token/HypNative.sol @@ -15,51 +15,40 @@ pragma solidity >=0.8.0; // ============ Internal Imports ============ import {TokenRouter} from "./libs/TokenRouter.sol"; +import {HypNativeCollateral} from "./HypNativeCollateral.sol"; import {StandardHookMetadata} from "../hooks/libs/StandardHookMetadata.sol"; -// ============ External Imports ============ -import {Address} from "@openzeppelin/contracts/utils/Address.sol"; - /** * @title HypNative * @author Abacus Works * @notice This contract facilitates the transfer of value between chains using value transfer hooks */ -contract HypNative is TokenRouter { - /** - * @dev Emitted when native tokens are donated to the contract. - * @param sender The address of the sender. - * @param amount The amount of native tokens donated. - */ - event Donation(address indexed sender, uint256 amount); - // ============ Errors ============ - - error InsufficientValue(uint256 requiredValue, uint256 providedValue); - - constructor(address _mailbox) TokenRouter(_mailbox) {} +contract HypNative is HypNativeCollateral { + constructor(address _mailbox) HypNativeCollateral(_mailbox) {} - // ============ Initialization ============ + // ============ External Functions ============ - /** - * @notice Initializes the contract - * @param _valuehook The address of the value transfer hook - * @param _interchainSecurityModule The address of the interchain security module - * @param _owner The owner of the contract - */ - function initialize( - address _valuehook, - address _interchainSecurityModule, - address _owner - ) public initializer { - _MailboxClient_initialize( - _valuehook, - _interchainSecurityModule, - _owner - ); + /// @inheritdoc TokenRouter + function transferRemote( + uint32 _destination, + bytes32 _recipient, + uint256 _amount + ) external payable virtual override returns (bytes32 messageId) { + bytes calldata emptyBytes; + assembly { + emptyBytes.length := 0 + emptyBytes.offset := 0 + } + return + transferRemote( + _destination, + _recipient, + _amount, + emptyBytes, + address(hook) + ); } - // ============ External Functions ============ - /** * @inheritdoc TokenRouter * @dev use _hook with caution, make sure that this hook can handle msg.value transfer using the metadata.msgValue() @@ -92,60 +81,4 @@ contract HypNative is TokenRouter { _hook ); } - - /// @inheritdoc TokenRouter - function transferRemote( - uint32 _destination, - bytes32 _recipient, - uint256 _amount - ) external payable virtual override returns (bytes32 messageId) { - bytes calldata emptyBytes; - assembly { - emptyBytes.length := 0 - emptyBytes.offset := 0 - } - return - transferRemote( - _destination, - _recipient, - _amount, - emptyBytes, - address(hook) - ); - } - - // ============ Internal Functions ============ - - /** - * @inheritdoc TokenRouter - * @dev No token metadata is needed for value transfers - */ - function _transferFromSender( - uint256 - ) internal pure override returns (bytes memory) { - return bytes(""); // no token metadata - } - - /** - * @inheritdoc TokenRouter - * @dev Sends the value to the recipient - */ - function _transferTo( - address _recipient, - uint256 _amount, - bytes calldata // no token metadata - ) internal virtual override { - Address.sendValue(payable(_recipient), _amount); - } - - /// @inheritdoc TokenRouter - function balanceOf( - address _account - ) external view override returns (uint256) { - return _account.balance; - } - - receive() external payable { - emit Donation(msg.sender, msg.value); - } } From 9c38a7aae2dd70e4f1826f084467c95010cbf920 Mon Sep 17 00:00:00 2001 From: -f Date: Fri, 13 Dec 2024 17:13:53 +0530 Subject: [PATCH 17/23] interchange names --- .../token/extensions/HypNativeScaled.sol | 78 +++++++ solidity/test/token/HypNativeScaled.t.sol | 217 ++++++++++++++++++ 2 files changed, 295 insertions(+) create mode 100644 solidity/contracts/token/extensions/HypNativeScaled.sol create mode 100644 solidity/test/token/HypNativeScaled.t.sol diff --git a/solidity/contracts/token/extensions/HypNativeScaled.sol b/solidity/contracts/token/extensions/HypNativeScaled.sol new file mode 100644 index 0000000000..ad129fce80 --- /dev/null +++ b/solidity/contracts/token/extensions/HypNativeScaled.sol @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.8.0; + +import {HypNative} from "../HypNative.sol"; +import {TokenRouter} from "../libs/TokenRouter.sol"; + +/** + * @title Hyperlane Native Token that scales native value by a fixed factor for consistency with other tokens. + * @dev The scale factor multiplies the `message.amount` to the local native token amount. + * Conversely, it divides the local native `msg.value` amount by `scale` to encode the `message.amount`. + * @author Abacus Works + */ +contract HypNativeScaled is HypNative { + uint256 public immutable scale; + + constructor(uint256 _scale, address _mailbox) HypNative(_mailbox) { + scale = _scale; + } + + /** + * @inheritdoc HypNative + * @dev Sends scaled `msg.value` (divided by `scale`) to `_recipient`. + */ + function transferRemote( + uint32 _destination, + bytes32 _recipient, + uint256 _amount + ) external payable override returns (bytes32 messageId) { + require(msg.value >= _amount, "Native: amount exceeds msg.value"); + uint256 _hookPayment = msg.value - _amount; + uint256 _scaledAmount = _amount / scale; + return + _transferRemote( + _destination, + _recipient, + _scaledAmount, + _hookPayment + ); + } + + /** + * @inheritdoc TokenRouter + * @dev uses (`msg.value` - `_amount`) as hook payment. + */ + function transferRemote( + uint32 _destination, + bytes32 _recipient, + uint256 _amount, + bytes calldata _hookMetadata, + address _hook + ) external payable override returns (bytes32 messageId) { + require(msg.value >= _amount, "Native: amount exceeds msg.value"); + uint256 _hookPayment = msg.value - _amount; + uint256 _scaledAmount = _amount / scale; + return + _transferRemote( + _destination, + _recipient, + _scaledAmount, + _hookPayment, + _hookMetadata, + _hook + ); + } + + /** + * @dev Sends scaled `_amount` (multiplied by `scale`) to `_recipient`. + * @inheritdoc TokenRouter + */ + function _transferTo( + address _recipient, + uint256 _amount, + bytes calldata metadata // no metadata + ) internal override { + uint256 scaledAmount = _amount * scale; + HypNative._transferTo(_recipient, scaledAmount, metadata); + } +} diff --git a/solidity/test/token/HypNativeScaled.t.sol b/solidity/test/token/HypNativeScaled.t.sol new file mode 100644 index 0000000000..ffded65655 --- /dev/null +++ b/solidity/test/token/HypNativeScaled.t.sol @@ -0,0 +1,217 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity >=0.8.0; + +import "forge-std/Test.sol"; +import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; + +import {TestPostDispatchHook} from "../../contracts/test/TestPostDispatchHook.sol"; +import {HypNativeScaled} from "../../contracts/token/extensions/HypNativeScaled.sol"; +import {HypERC20} from "../../contracts/token/HypERC20.sol"; +import {HypNative} from "../../contracts/token/HypNative.sol"; +import {TypeCasts} from "../../contracts/libs/TypeCasts.sol"; +import {MockHyperlaneEnvironment} from "../../contracts/mock/MockHyperlaneEnvironment.sol"; + +contract HypNativeScaledTest is Test { + uint32 nativeDomain = 1; + uint32 synthDomain = 2; + + address internal constant ALICE = address(0x1); + + uint8 decimals = 9; + uint256 mintAmount = 123456789; + uint256 nativeDecimals = 18; + uint256 scale = 10 ** (nativeDecimals - decimals); + + event Donation(address indexed sender, uint256 amount); + event SentTransferRemote( + uint32 indexed destination, + bytes32 indexed recipient, + uint256 amount + ); + event ReceivedTransferRemote( + uint32 indexed origin, + bytes32 indexed recipient, + uint256 amount + ); + + HypNativeScaled native; + HypERC20 synth; + + MockHyperlaneEnvironment environment; + + function setUp() public { + environment = new MockHyperlaneEnvironment(synthDomain, nativeDomain); + + HypERC20 implementationSynth = new HypERC20( + decimals, + address(environment.mailboxes(synthDomain)) + ); + TransparentUpgradeableProxy proxySynth = new TransparentUpgradeableProxy( + address(implementationSynth), + address(9), + abi.encodeWithSelector( + HypERC20.initialize.selector, + mintAmount * (10 ** decimals), + "Zebec BSC Token", + "ZBC", + address(0), + address(0), + address(this) + ) + ); + synth = HypERC20(address(proxySynth)); + + HypNativeScaled implementationNative = new HypNativeScaled( + scale, + address(environment.mailboxes(nativeDomain)) + ); + TransparentUpgradeableProxy proxyNative = new TransparentUpgradeableProxy( + address(implementationNative), + address(9), + abi.encodeWithSelector( + HypNative.initialize.selector, + address(0), + address(0), + address(this) + ) + ); + + native = HypNativeScaled(payable(address(proxyNative))); + + native.enrollRemoteRouter( + synthDomain, + TypeCasts.addressToBytes32(address(synth)) + ); + synth.enrollRemoteRouter( + nativeDomain, + TypeCasts.addressToBytes32(address(native)) + ); + } + + function test_constructor() public { + assertEq(native.scale(), scale); + } + + uint256 receivedValue; + + receive() external payable { + receivedValue = msg.value; + } + + function test_receive(uint256 amount) public { + vm.assume(amount < address(this).balance); + vm.expectEmit(true, true, true, true); + emit Donation(address(this), amount); + (bool success, bytes memory returnData) = address(native).call{ + value: amount + }(""); + assert(success); + assertEq(returnData.length, 0); + } + + function test_handle(uint256 amount) public { + vm.assume(amount <= mintAmount); + + uint256 synthAmount = amount * (10 ** decimals); + uint256 nativeAmount = amount * (10 ** nativeDecimals); + + vm.deal(address(native), nativeAmount); + + bytes32 recipient = TypeCasts.addressToBytes32(address(this)); + synth.transferRemote(nativeDomain, recipient, synthAmount); + + vm.expectEmit(true, true, true, true); + emit ReceivedTransferRemote(synthDomain, recipient, synthAmount); + environment.processNextPendingMessage(); + + assertEq(receivedValue, nativeAmount); + } + + function test_handle_reverts_whenAmountExceedsSupply( + uint256 amount + ) public { + vm.assume(amount <= mintAmount); + + bytes32 recipient = TypeCasts.addressToBytes32(address(this)); + synth.transferRemote(nativeDomain, recipient, amount); + + uint256 nativeValue = amount * scale; + vm.deal(address(native), nativeValue / 2); + + if (amount > 0) { + vm.expectRevert(bytes("Address: insufficient balance")); + } + environment.processNextPendingMessage(); + } + + function test_tranferRemote(uint256 amount) public { + vm.assume(amount <= mintAmount); + + uint256 nativeValue = amount * (10 ** nativeDecimals); + uint256 synthAmount = amount * (10 ** decimals); + address recipient = address(0xdeadbeef); + bytes32 bRecipient = TypeCasts.addressToBytes32(recipient); + + vm.deal(address(this), nativeValue); + vm.expectEmit(true, true, true, true); + emit SentTransferRemote(synthDomain, bRecipient, synthAmount); + native.transferRemote{value: nativeValue}( + synthDomain, + bRecipient, + nativeValue + ); + environment.processNextPendingMessageFromDestination(); + assertEq(synth.balanceOf(recipient), synthAmount); + } + + function testTransfer_withHookSpecified( + uint256 amount, + bytes calldata metadata + ) public { + vm.assume(amount <= mintAmount); + + uint256 nativeValue = amount * (10 ** nativeDecimals); + uint256 synthAmount = amount * (10 ** decimals); + address recipient = address(0xdeadbeef); + bytes32 bRecipient = TypeCasts.addressToBytes32(recipient); + + TestPostDispatchHook hook = new TestPostDispatchHook(); + + vm.deal(address(this), nativeValue); + vm.expectEmit(true, true, true, true); + emit SentTransferRemote(synthDomain, bRecipient, synthAmount); + native.transferRemote{value: nativeValue}( + synthDomain, + bRecipient, + nativeValue, + metadata, + address(hook) + ); + environment.processNextPendingMessageFromDestination(); + assertEq(synth.balanceOf(recipient), synthAmount); + } + + function test_transferRemote_reverts_whenAmountExceedsValue( + uint256 nativeValue + ) public { + vm.assume(nativeValue < address(this).balance); + + address recipient = address(0xdeadbeef); + bytes32 bRecipient = TypeCasts.addressToBytes32(recipient); + vm.expectRevert("Native: amount exceeds msg.value"); + native.transferRemote{value: nativeValue}( + synthDomain, + bRecipient, + nativeValue + 1 + ); + + vm.expectRevert("Native: amount exceeds msg.value"); + native.transferRemote{value: nativeValue}( + synthDomain, + bRecipient, + nativeValue + 1, + bytes(""), + address(0) + ); + } +} From 5c1f2e8ffc88612b724dccb774aaee97f2b7585d Mon Sep 17 00:00:00 2001 From: -f Date: Fri, 13 Dec 2024 17:16:09 +0530 Subject: [PATCH 18/23] more --- solidity/contracts/hooks/OPL2ToL1Hook.sol | 13 +- solidity/contracts/token/HypNative.sol | 122 ++++++---- .../contracts/token/HypNativeCollateral.sol | 123 ++++------ .../extensions/HypNativeCollateralScaled.sol | 81 ------- solidity/test/token/HypNative.t.sol | 12 +- .../token/HypNativeCollateralScaled.t.sol | 217 ------------------ typescript/sdk/src/token/contracts.ts | 6 +- 7 files changed, 134 insertions(+), 440 deletions(-) delete mode 100644 solidity/contracts/token/extensions/HypNativeCollateralScaled.sol delete mode 100644 solidity/test/token/HypNativeCollateralScaled.t.sol diff --git a/solidity/contracts/hooks/OPL2ToL1Hook.sol b/solidity/contracts/hooks/OPL2ToL1Hook.sol index a8396b5de8..165289e57f 100644 --- a/solidity/contracts/hooks/OPL2ToL1Hook.sol +++ b/solidity/contracts/hooks/OPL2ToL1Hook.sol @@ -67,12 +67,8 @@ contract OPL2ToL1Hook is AbstractMessageIdAuthHook { bytes calldata metadata, bytes calldata message ) internal view override returns (uint256) { - bytes memory metadataWithGasLimit = metadata.overrideGasLimit( - MIN_GAS_LIMIT - ); return - metadata.msgValue(0) + - childHook.quoteDispatch(metadataWithGasLimit, message); + metadata.msgValue(0) + childHook.quoteDispatch(metadata, message); } // ============ Internal functions ============ @@ -87,12 +83,9 @@ contract OPL2ToL1Hook is AbstractMessageIdAuthHook { (message.id(), metadata.msgValue(0)) ); - bytes memory metadataWithGasLimit = metadata.overrideGasLimit( - MIN_GAS_LIMIT - ); childHook.postDispatch{ - value: childHook.quoteDispatch(metadataWithGasLimit, message) - }(metadataWithGasLimit, message); + value: childHook.quoteDispatch(metadata, message) + }(metadata, message); l2Messenger.sendMessage{value: metadata.msgValue(0)}( TypeCasts.bytes32ToAddress(ism), payload, diff --git a/solidity/contracts/token/HypNative.sol b/solidity/contracts/token/HypNative.sol index 8a7ae06d72..c06fc624f4 100644 --- a/solidity/contracts/token/HypNative.sol +++ b/solidity/contracts/token/HypNative.sol @@ -1,57 +1,55 @@ -// SPDX-License-Identifier: MIT OR Apache-2.0 +// SPDX-License-Identifier: Apache-2.0 pragma solidity >=0.8.0; -/*@@@@@@@ @@@@@@@@@ - @@@@@@@@@ @@@@@@@@@ - @@@@@@@@@ @@@@@@@@@ - @@@@@@@@@ @@@@@@@@@ - @@@@@@@@@@@@@@@@@@@@@@@@@ - @@@@@ HYPERLANE @@@@@@@ - @@@@@@@@@@@@@@@@@@@@@@@@@ - @@@@@@@@@ @@@@@@@@@ - @@@@@@@@@ @@@@@@@@@ - @@@@@@@@@ @@@@@@@@@ -@@@@@@@@@ @@@@@@@@*/ - -// ============ Internal Imports ============ import {TokenRouter} from "./libs/TokenRouter.sol"; -import {HypNativeCollateral} from "./HypNativeCollateral.sol"; -import {StandardHookMetadata} from "../hooks/libs/StandardHookMetadata.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; /** - * @title HypNative + * @title Hyperlane Native Token Router that extends ERC20 with remote transfer functionality. * @author Abacus Works - * @notice This contract facilitates the transfer of value between chains using value transfer hooks + * @dev Supply on each chain is not constant but the aggregate supply across all chains is. */ -contract HypNative is HypNativeCollateral { - constructor(address _mailbox) HypNativeCollateral(_mailbox) {} +contract HypNative is TokenRouter { + /** + * @dev Emitted when native tokens are donated to the contract. + * @param sender The address of the sender. + * @param amount The amount of native tokens donated. + */ + event Donation(address indexed sender, uint256 amount); - // ============ External Functions ============ + constructor(address _mailbox) TokenRouter(_mailbox) {} - /// @inheritdoc TokenRouter + /** + * @notice Initializes the Hyperlane router + * @param _hook The post-dispatch hook contract. + * @param _interchainSecurityModule The interchain security module contract. + * @param _owner The this contract. + */ + function initialize( + address _hook, + address _interchainSecurityModule, + address _owner + ) public initializer { + _MailboxClient_initialize(_hook, _interchainSecurityModule, _owner); + } + + /** + * @inheritdoc TokenRouter + * @dev uses (`msg.value` - `_amount`) as hook payment and `msg.sender` as refund address. + */ function transferRemote( uint32 _destination, bytes32 _recipient, uint256 _amount ) external payable virtual override returns (bytes32 messageId) { - bytes calldata emptyBytes; - assembly { - emptyBytes.length := 0 - emptyBytes.offset := 0 - } - return - transferRemote( - _destination, - _recipient, - _amount, - emptyBytes, - address(hook) - ); + require(msg.value >= _amount, "Native: amount exceeds msg.value"); + uint256 _hookPayment = msg.value - _amount; + return _transferRemote(_destination, _recipient, _amount, _hookPayment); } /** * @inheritdoc TokenRouter - * @dev use _hook with caution, make sure that this hook can handle msg.value transfer using the metadata.msgValue() + * @dev uses (`msg.value` - `_amount`) as hook payment. */ function transferRemote( uint32 _destination, @@ -59,26 +57,50 @@ contract HypNative is HypNativeCollateral { uint256 _amount, bytes calldata _hookMetadata, address _hook - ) public payable virtual override returns (bytes32 messageId) { - uint256 quote = _GasRouter_quoteDispatch( - _destination, - _hookMetadata, - _hook - ); - - bytes memory hookMetadata = StandardHookMetadata.overrideMsgValue( - _hookMetadata, - _amount - ); - + ) external payable virtual override returns (bytes32 messageId) { + require(msg.value >= _amount, "Native: amount exceeds msg.value"); + uint256 _hookPayment = msg.value - _amount; return _transferRemote( _destination, _recipient, _amount, - _amount + quote, - hookMetadata, + _hookPayment, + _hookMetadata, _hook ); } + + function balanceOf( + address _account + ) external view override returns (uint256) { + return _account.balance; + } + + /** + * @inheritdoc TokenRouter + * @dev No-op because native amount is transferred in `msg.value` + * @dev Compiler will not include this in the bytecode. + */ + function _transferFromSender( + uint256 + ) internal pure override returns (bytes memory) { + return bytes(""); // no metadata + } + + /** + * @dev Sends `_amount` of native token to `_recipient` balance. + * @inheritdoc TokenRouter + */ + function _transferTo( + address _recipient, + uint256 _amount, + bytes calldata // no metadata + ) internal virtual override { + Address.sendValue(payable(_recipient), _amount); + } + + receive() external payable { + emit Donation(msg.sender, msg.value); + } } diff --git a/solidity/contracts/token/HypNativeCollateral.sol b/solidity/contracts/token/HypNativeCollateral.sol index ec6630d910..b11fa1030c 100644 --- a/solidity/contracts/token/HypNativeCollateral.sol +++ b/solidity/contracts/token/HypNativeCollateral.sol @@ -1,56 +1,57 @@ -// SPDX-License-Identifier: Apache-2.0 +// SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity >=0.8.0; +/*@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@ HYPERLANE @@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ +@@@@@@@@@ @@@@@@@@*/ + +// ============ Internal Imports ============ import {TokenRouter} from "./libs/TokenRouter.sol"; -import {TokenMessage} from "./libs/TokenMessage.sol"; -import {Address} from "@openzeppelin/contracts/utils/Address.sol"; +import {HypNative} from "./HypNative.sol"; +import {StandardHookMetadata} from "../hooks/libs/StandardHookMetadata.sol"; /** - * @title Hyperlane Native Token Router that extends ERC20 with remote transfer functionality. + * @title HypNativeCollateral * @author Abacus Works - * @dev Supply on each chain is not constant but the aggregate supply across all chains is. + * @notice This contract facilitates the transfer of value between chains using value transfer hooks */ -contract HypNativeCollateral is TokenRouter { - /** - * @dev Emitted when native tokens are donated to the contract. - * @param sender The address of the sender. - * @param amount The amount of native tokens donated. - */ - event Donation(address indexed sender, uint256 amount); +contract HypNativeCollateral is HypNative { + constructor(address _mailbox) HypNative(_mailbox) {} - constructor(address _mailbox) TokenRouter(_mailbox) {} + // ============ External Functions ============ - /** - * @notice Initializes the Hyperlane router - * @param _hook The post-dispatch hook contract. - * @param _interchainSecurityModule The interchain security module contract. - * @param _owner The this contract. - */ - function initialize( - address _hook, - address _interchainSecurityModule, - address _owner - ) public initializer { - _MailboxClient_initialize(_hook, _interchainSecurityModule, _owner); - } - - /** - * @inheritdoc TokenRouter - * @dev uses (`msg.value` - `_amount`) as hook payment and `msg.sender` as refund address. - */ + /// @inheritdoc TokenRouter function transferRemote( uint32 _destination, bytes32 _recipient, uint256 _amount ) external payable virtual override returns (bytes32 messageId) { - require(msg.value >= _amount, "Native: amount exceeds msg.value"); - uint256 _hookPayment = msg.value - _amount; - return _transferRemote(_destination, _recipient, _amount, _hookPayment); + bytes calldata emptyBytes; + assembly { + emptyBytes.length := 0 + emptyBytes.offset := 0 + } + return + transferRemote( + _destination, + _recipient, + _amount, + emptyBytes, + address(hook) + ); } /** * @inheritdoc TokenRouter - * @dev uses (`msg.value` - `_amount`) as hook payment. + * @dev use _hook with caution, make sure that this hook can handle msg.value transfer using the metadata.msgValue() */ function transferRemote( uint32 _destination, @@ -58,50 +59,26 @@ contract HypNativeCollateral is TokenRouter { uint256 _amount, bytes calldata _hookMetadata, address _hook - ) external payable virtual override returns (bytes32 messageId) { - require(msg.value >= _amount, "Native: amount exceeds msg.value"); - uint256 _hookPayment = msg.value - _amount; + ) public payable virtual override returns (bytes32 messageId) { + uint256 quote = _GasRouter_quoteDispatch( + _destination, + _hookMetadata, + _hook + ); + + bytes memory hookMetadata = StandardHookMetadata.overrideMsgValue( + _hookMetadata, + _amount + ); + return _transferRemote( _destination, _recipient, _amount, - _hookPayment, - _hookMetadata, + _amount + quote, + hookMetadata, _hook ); } - - function balanceOf( - address _account - ) external view override returns (uint256) { - return _account.balance; - } - - /** - * @inheritdoc TokenRouter - * @dev No-op because native amount is transferred in `msg.value` - * @dev Compiler will not include this in the bytecode. - */ - function _transferFromSender( - uint256 - ) internal pure override returns (bytes memory) { - return bytes(""); // no metadata - } - - /** - * @dev Sends `_amount` of native token to `_recipient` balance. - * @inheritdoc TokenRouter - */ - function _transferTo( - address _recipient, - uint256 _amount, - bytes calldata // no metadata - ) internal virtual override { - Address.sendValue(payable(_recipient), _amount); - } - - receive() external payable { - emit Donation(msg.sender, msg.value); - } } diff --git a/solidity/contracts/token/extensions/HypNativeCollateralScaled.sol b/solidity/contracts/token/extensions/HypNativeCollateralScaled.sol deleted file mode 100644 index c12117a6d5..0000000000 --- a/solidity/contracts/token/extensions/HypNativeCollateralScaled.sol +++ /dev/null @@ -1,81 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity >=0.8.0; - -import {HypNativeCollateral} from "../HypNativeCollateral.sol"; -import {TokenRouter} from "../libs/TokenRouter.sol"; - -/** - * @title Hyperlane Native Token that scales native value by a fixed factor for consistency with other tokens. - * @dev The scale factor multiplies the `message.amount` to the local native token amount. - * Conversely, it divides the local native `msg.value` amount by `scale` to encode the `message.amount`. - * @author Abacus Works - */ -contract HypNativeCollateralScaled is HypNativeCollateral { - uint256 public immutable scale; - - constructor( - uint256 _scale, - address _mailbox - ) HypNativeCollateral(_mailbox) { - scale = _scale; - } - - /** - * @inheritdoc HypNativeCollateral - * @dev Sends scaled `msg.value` (divided by `scale`) to `_recipient`. - */ - function transferRemote( - uint32 _destination, - bytes32 _recipient, - uint256 _amount - ) external payable override returns (bytes32 messageId) { - require(msg.value >= _amount, "Native: amount exceeds msg.value"); - uint256 _hookPayment = msg.value - _amount; - uint256 _scaledAmount = _amount / scale; - return - _transferRemote( - _destination, - _recipient, - _scaledAmount, - _hookPayment - ); - } - - /** - * @inheritdoc TokenRouter - * @dev uses (`msg.value` - `_amount`) as hook payment. - */ - function transferRemote( - uint32 _destination, - bytes32 _recipient, - uint256 _amount, - bytes calldata _hookMetadata, - address _hook - ) external payable override returns (bytes32 messageId) { - require(msg.value >= _amount, "Native: amount exceeds msg.value"); - uint256 _hookPayment = msg.value - _amount; - uint256 _scaledAmount = _amount / scale; - return - _transferRemote( - _destination, - _recipient, - _scaledAmount, - _hookPayment, - _hookMetadata, - _hook - ); - } - - /** - * @dev Sends scaled `_amount` (multiplied by `scale`) to `_recipient`. - * @inheritdoc TokenRouter - */ - function _transferTo( - address _recipient, - uint256 _amount, - bytes calldata metadata // no metadata - ) internal override { - uint256 scaledAmount = _amount * scale; - HypNativeCollateral._transferTo(_recipient, scaledAmount, metadata); - } -} diff --git a/solidity/test/token/HypNative.t.sol b/solidity/test/token/HypNative.t.sol index eae4a8f21a..5d9004b41f 100644 --- a/solidity/test/token/HypNative.t.sol +++ b/solidity/test/token/HypNative.t.sol @@ -5,23 +5,23 @@ import {TypeCasts} from "../../contracts/libs/TypeCasts.sol"; import {HypTokenTest} from "./HypERC20.t.sol"; import {HypERC20} from "../../contracts/token/HypERC20.sol"; import {TokenRouter} from "../../contracts/token/libs/TokenRouter.sol"; -import {HypNative} from "../../contracts/token/HypNative.sol"; +import {HypNativeCollateral} from "../../contracts/token/HypNativeCollateral.sol"; import {TestPostDispatchHook} from "../../contracts/test/TestPostDispatchHook.sol"; import {TestIsm} from "../../contracts/test/TestIsm.sol"; -contract HypNativeTest is HypTokenTest { +contract HypNativeCollateralTest is HypTokenTest { using TypeCasts for address; - HypNative internal localValueRouter; - HypNative internal remoteValueRouter; + HypNativeCollateral internal localValueRouter; + HypNativeCollateral internal remoteValueRouter; TestPostDispatchHook internal valueHook; TestIsm internal ism; function setUp() public override { super.setUp(); - localValueRouter = new HypNative(address(localMailbox)); - remoteValueRouter = new HypNative(address(remoteMailbox)); + localValueRouter = new HypNativeCollateral(address(localMailbox)); + remoteValueRouter = new HypNativeCollateral(address(remoteMailbox)); localToken = TokenRouter(payable(address(localValueRouter))); remoteToken = HypERC20(payable(address(remoteValueRouter))); diff --git a/solidity/test/token/HypNativeCollateralScaled.t.sol b/solidity/test/token/HypNativeCollateralScaled.t.sol deleted file mode 100644 index d867caf7c6..0000000000 --- a/solidity/test/token/HypNativeCollateralScaled.t.sol +++ /dev/null @@ -1,217 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity >=0.8.0; - -import "forge-std/Test.sol"; -import {TransparentUpgradeableProxy} from "@openzeppelin/contracts/proxy/transparent/TransparentUpgradeableProxy.sol"; - -import {TestPostDispatchHook} from "../../contracts/test/TestPostDispatchHook.sol"; -import {HypNativeCollateralScaled} from "../../contracts/token/extensions/HypNativeCollateralScaled.sol"; -import {HypERC20} from "../../contracts/token/HypERC20.sol"; -import {HypNativeCollateral} from "../../contracts/token/HypNativeCollateral.sol"; -import {TypeCasts} from "../../contracts/libs/TypeCasts.sol"; -import {MockHyperlaneEnvironment} from "../../contracts/mock/MockHyperlaneEnvironment.sol"; - -contract HypNativeCollateralScaledTest is Test { - uint32 nativeDomain = 1; - uint32 synthDomain = 2; - - address internal constant ALICE = address(0x1); - - uint8 decimals = 9; - uint256 mintAmount = 123456789; - uint256 nativeDecimals = 18; - uint256 scale = 10 ** (nativeDecimals - decimals); - - event Donation(address indexed sender, uint256 amount); - event SentTransferRemote( - uint32 indexed destination, - bytes32 indexed recipient, - uint256 amount - ); - event ReceivedTransferRemote( - uint32 indexed origin, - bytes32 indexed recipient, - uint256 amount - ); - - HypNativeCollateralScaled native; - HypERC20 synth; - - MockHyperlaneEnvironment environment; - - function setUp() public { - environment = new MockHyperlaneEnvironment(synthDomain, nativeDomain); - - HypERC20 implementationSynth = new HypERC20( - decimals, - address(environment.mailboxes(synthDomain)) - ); - TransparentUpgradeableProxy proxySynth = new TransparentUpgradeableProxy( - address(implementationSynth), - address(9), - abi.encodeWithSelector( - HypERC20.initialize.selector, - mintAmount * (10 ** decimals), - "Zebec BSC Token", - "ZBC", - address(0), - address(0), - address(this) - ) - ); - synth = HypERC20(address(proxySynth)); - - HypNativeCollateralScaled implementationNative = new HypNativeCollateralScaled( - scale, - address(environment.mailboxes(nativeDomain)) - ); - TransparentUpgradeableProxy proxyNative = new TransparentUpgradeableProxy( - address(implementationNative), - address(9), - abi.encodeWithSelector( - HypNativeCollateral.initialize.selector, - address(0), - address(0), - address(this) - ) - ); - - native = HypNativeCollateralScaled(payable(address(proxyNative))); - - native.enrollRemoteRouter( - synthDomain, - TypeCasts.addressToBytes32(address(synth)) - ); - synth.enrollRemoteRouter( - nativeDomain, - TypeCasts.addressToBytes32(address(native)) - ); - } - - function test_constructor() public { - assertEq(native.scale(), scale); - } - - uint256 receivedValue; - - receive() external payable { - receivedValue = msg.value; - } - - function test_receive(uint256 amount) public { - vm.assume(amount < address(this).balance); - vm.expectEmit(true, true, true, true); - emit Donation(address(this), amount); - (bool success, bytes memory returnData) = address(native).call{ - value: amount - }(""); - assert(success); - assertEq(returnData.length, 0); - } - - function test_handle(uint256 amount) public { - vm.assume(amount <= mintAmount); - - uint256 synthAmount = amount * (10 ** decimals); - uint256 nativeAmount = amount * (10 ** nativeDecimals); - - vm.deal(address(native), nativeAmount); - - bytes32 recipient = TypeCasts.addressToBytes32(address(this)); - synth.transferRemote(nativeDomain, recipient, synthAmount); - - vm.expectEmit(true, true, true, true); - emit ReceivedTransferRemote(synthDomain, recipient, synthAmount); - environment.processNextPendingMessage(); - - assertEq(receivedValue, nativeAmount); - } - - function test_handle_reverts_whenAmountExceedsSupply( - uint256 amount - ) public { - vm.assume(amount <= mintAmount); - - bytes32 recipient = TypeCasts.addressToBytes32(address(this)); - synth.transferRemote(nativeDomain, recipient, amount); - - uint256 nativeValue = amount * scale; - vm.deal(address(native), nativeValue / 2); - - if (amount > 0) { - vm.expectRevert(bytes("Address: insufficient balance")); - } - environment.processNextPendingMessage(); - } - - function test_tranferRemote(uint256 amount) public { - vm.assume(amount <= mintAmount); - - uint256 nativeValue = amount * (10 ** nativeDecimals); - uint256 synthAmount = amount * (10 ** decimals); - address recipient = address(0xdeadbeef); - bytes32 bRecipient = TypeCasts.addressToBytes32(recipient); - - vm.deal(address(this), nativeValue); - vm.expectEmit(true, true, true, true); - emit SentTransferRemote(synthDomain, bRecipient, synthAmount); - native.transferRemote{value: nativeValue}( - synthDomain, - bRecipient, - nativeValue - ); - environment.processNextPendingMessageFromDestination(); - assertEq(synth.balanceOf(recipient), synthAmount); - } - - function testTransfer_withHookSpecified( - uint256 amount, - bytes calldata metadata - ) public { - vm.assume(amount <= mintAmount); - - uint256 nativeValue = amount * (10 ** nativeDecimals); - uint256 synthAmount = amount * (10 ** decimals); - address recipient = address(0xdeadbeef); - bytes32 bRecipient = TypeCasts.addressToBytes32(recipient); - - TestPostDispatchHook hook = new TestPostDispatchHook(); - - vm.deal(address(this), nativeValue); - vm.expectEmit(true, true, true, true); - emit SentTransferRemote(synthDomain, bRecipient, synthAmount); - native.transferRemote{value: nativeValue}( - synthDomain, - bRecipient, - nativeValue, - metadata, - address(hook) - ); - environment.processNextPendingMessageFromDestination(); - assertEq(synth.balanceOf(recipient), synthAmount); - } - - function test_transferRemote_reverts_whenAmountExceedsValue( - uint256 nativeValue - ) public { - vm.assume(nativeValue < address(this).balance); - - address recipient = address(0xdeadbeef); - bytes32 bRecipient = TypeCasts.addressToBytes32(recipient); - vm.expectRevert("Native: amount exceeds msg.value"); - native.transferRemote{value: nativeValue}( - synthDomain, - bRecipient, - nativeValue + 1 - ); - - vm.expectRevert("Native: amount exceeds msg.value"); - native.transferRemote{value: nativeValue}( - synthDomain, - bRecipient, - nativeValue + 1, - bytes(""), - address(0) - ); - } -} diff --git a/typescript/sdk/src/token/contracts.ts b/typescript/sdk/src/token/contracts.ts index 32a684cf30..40fc40d2d0 100644 --- a/typescript/sdk/src/token/contracts.ts +++ b/typescript/sdk/src/token/contracts.ts @@ -11,8 +11,8 @@ import { HypERC4626OwnerCollateral__factory, HypERC4626__factory, HypFiatToken__factory, - HypNativeCollateralScaled__factory, HypNativeCollateral__factory, + HypNativeScaled__factory, HypXERC20Lockbox__factory, HypXERC20__factory, } from '@hyperlane-xyz/core'; @@ -31,7 +31,7 @@ export const hypERC20contracts = { [TokenType.collateralVault]: 'HypERC4626OwnerCollateral', [TokenType.collateralVaultRebase]: 'HypERC4626Collateral', [TokenType.native]: 'HypNativeCollateral', - [TokenType.nativeScaled]: 'HypNativeCollateralScaled', + [TokenType.nativeScaled]: 'HypNativeScaled', }; export type HypERC20contracts = typeof hypERC20contracts; @@ -47,7 +47,7 @@ export const hypERC20factories = { [TokenType.XERC20]: new HypXERC20__factory(), [TokenType.XERC20Lockbox]: new HypXERC20Lockbox__factory(), [TokenType.native]: new HypNativeCollateral__factory(), - [TokenType.nativeScaled]: new HypNativeCollateralScaled__factory(), + [TokenType.nativeScaled]: new HypNativeScaled__factory(), }; export type HypERC20Factories = typeof hypERC20factories; From ec225b4e13e78d394c5a39954280d2812fe7aad8 Mon Sep 17 00:00:00 2001 From: -f Date: Fri, 13 Dec 2024 17:19:41 +0530 Subject: [PATCH 19/23] revert --- solidity/contracts/token/HypNative.sol | 5 +- solidity/test/isms/ExternalBridgeTest.sol | 1 + solidity/test/isms/OPL2ToL1Ism.t.sol | 23 +- solidity/test/token/HypERC20.t.sol | 10 +- typescript/cli/cli.ts | 7 +- typescript/cli/src/commands/config.ts | 15 ++ typescript/cli/src/commands/options.ts | 5 +- typescript/cli/src/commands/signCommands.ts | 29 +- typescript/cli/src/commands/strategy.ts | 70 +++++ typescript/cli/src/config/core.ts | 4 +- typescript/cli/src/config/hooks.ts | 6 +- typescript/cli/src/config/ism.ts | 6 +- typescript/cli/src/config/strategy.ts | 186 +++++++++++++ typescript/cli/src/config/warp.ts | 15 +- typescript/cli/src/context/context.ts | 58 +++- .../strategies/chain/ChainResolverFactory.ts | 36 +++ .../strategies/chain/MultiChainResolver.ts | 249 ++++++++++++++++++ .../strategies/chain/SingleChainResolver.ts | 25 ++ .../cli/src/context/strategies/chain/types.ts | 10 + .../signer/BaseMultiProtocolSigner.ts | 22 ++ .../signer/MultiProtocolSignerFactory.ts | 79 ++++++ .../signer/MultiProtocolSignerManager.ts | 153 +++++++++++ typescript/cli/src/context/types.ts | 7 +- typescript/cli/src/deploy/agent.ts | 1 + typescript/cli/src/deploy/core.ts | 6 +- typescript/cli/src/deploy/utils.ts | 30 ++- typescript/cli/src/deploy/warp.ts | 17 +- typescript/cli/src/read/warp.ts | 12 +- typescript/cli/src/send/transfer.ts | 24 +- typescript/cli/src/tests/commands/helpers.ts | 7 + typescript/cli/src/utils/balances.ts | 5 +- typescript/cli/src/utils/chains.ts | 33 +++ typescript/cli/src/utils/output.ts | 47 ++++ .../config/environments/mainnet3/agent.ts | 5 +- .../config/environments/mainnet3/chains.ts | 10 + .../environments/mainnet3/gasPrices.json | 32 +-- .../config/environments/mainnet3/owners.ts | 7 + .../environments/mainnet3/tokenPrices.json | 188 ++++++------- .../getEthereumVictionETHWarpConfig.ts | 13 +- .../getEthereumVictionUSDCWarpConfig.ts | 14 +- .../getEthereumVictionUSDTWarpConfig.ts | 13 +- .../config/environments/testnet4/agent.ts | 2 - typescript/infra/scripts/check/check-utils.ts | 3 +- typescript/infra/scripts/safes/parse-txs.ts | 9 +- .../infra/src/govern/HyperlaneAppGovernor.ts | 14 + .../infra/src/govern/HyperlaneHaasGovernor.ts | 4 +- .../infra/src/govern/HyperlaneICAChecker.ts | 63 +++++ .../infra/src/tx/govern-transaction-reader.ts | 55 +++- typescript/sdk/src/consts/multisigIsm.ts | 47 +++- .../sdk/src/metadata/warpRouteConfig.ts | 2 +- .../transactions/submitter/ethersV5/types.ts | 2 + .../token/EvmERC20WarpModule.hardhat-test.ts | 4 +- typescript/sdk/src/token/contracts.ts | 6 +- typescript/utils/src/addresses.ts | 10 +- typescript/utils/src/index.ts | 1 + 55 files changed, 1444 insertions(+), 263 deletions(-) create mode 100644 typescript/cli/src/commands/strategy.ts create mode 100644 typescript/cli/src/config/strategy.ts create mode 100644 typescript/cli/src/context/strategies/chain/ChainResolverFactory.ts create mode 100644 typescript/cli/src/context/strategies/chain/MultiChainResolver.ts create mode 100644 typescript/cli/src/context/strategies/chain/SingleChainResolver.ts create mode 100644 typescript/cli/src/context/strategies/chain/types.ts create mode 100644 typescript/cli/src/context/strategies/signer/BaseMultiProtocolSigner.ts create mode 100644 typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts create mode 100644 typescript/cli/src/context/strategies/signer/MultiProtocolSignerManager.ts create mode 100644 typescript/infra/src/govern/HyperlaneICAChecker.ts diff --git a/solidity/contracts/token/HypNative.sol b/solidity/contracts/token/HypNative.sol index c06fc624f4..cfb526fa40 100644 --- a/solidity/contracts/token/HypNative.sol +++ b/solidity/contracts/token/HypNative.sol @@ -2,6 +2,7 @@ pragma solidity >=0.8.0; import {TokenRouter} from "./libs/TokenRouter.sol"; +import {TokenMessage} from "./libs/TokenMessage.sol"; import {Address} from "@openzeppelin/contracts/utils/Address.sol"; /** @@ -22,8 +23,8 @@ contract HypNative is TokenRouter { /** * @notice Initializes the Hyperlane router * @param _hook The post-dispatch hook contract. - * @param _interchainSecurityModule The interchain security module contract. - * @param _owner The this contract. + @param _interchainSecurityModule The interchain security module contract. + @param _owner The this contract. */ function initialize( address _hook, diff --git a/solidity/test/isms/ExternalBridgeTest.sol b/solidity/test/isms/ExternalBridgeTest.sol index e586297822..937e39311a 100644 --- a/solidity/test/isms/ExternalBridgeTest.sol +++ b/solidity/test/isms/ExternalBridgeTest.sol @@ -115,6 +115,7 @@ abstract contract ExternalBridgeTest is Test { function test_postDispatch_revertWhen_insufficientValue() public { bytes memory encodedHookData = _encodeHookData(messageId, 0); originMailbox.updateLatestDispatchedId(messageId); + _expectOriginExternalBridgeCall(encodedHookData); uint256 quote = hook.quoteDispatch(testMetadata, encodedMessage); diff --git a/solidity/test/isms/OPL2ToL1Ism.t.sol b/solidity/test/isms/OPL2ToL1Ism.t.sol index 9aa31b9f8f..9322713d5e 100644 --- a/solidity/test/isms/OPL2ToL1Ism.t.sol +++ b/solidity/test/isms/OPL2ToL1Ism.t.sol @@ -21,9 +21,7 @@ contract OPL2ToL1IsmTest is ExternalBridgeTest { 0x4200000000000000000000000000000000000007; uint256 internal constant MOCK_NONCE = 0; - uint256 internal constant GAS_LIMIT = 300_000; - uint256 internal constant GAS_PRICE = 10; - uint256 internal constant OVERRIDE_MULTIPLIER = 2; + TestInterchainGasPaymaster internal mockOverheadIgp; MockOptimismPortal internal portal; MockOptimismMessenger internal l1Messenger; @@ -42,8 +40,6 @@ contract OPL2ToL1IsmTest is ExternalBridgeTest { deployAll(); super.setUp(); - - GAS_QUOTE = GAS_LIMIT * GAS_PRICE; } function deployHook() public { @@ -80,27 +76,14 @@ contract OPL2ToL1IsmTest is ExternalBridgeTest { _expectOriginExternalBridgeCall(encodedHookData); bytes memory igpMetadata = StandardHookMetadata.overrideGasLimit( - GAS_LIMIT + 78_000 ); uint256 quote = hook.quoteDispatch(igpMetadata, encodedMessage); - assertEq( - quote, - mockOverheadIgp.quoteGasPayment(ORIGIN_DOMAIN, GAS_LIMIT) - ); + assertEq(quote, mockOverheadIgp.quoteGasPayment(ORIGIN_DOMAIN, 78_000)); hook.postDispatch{value: quote}(igpMetadata, encodedMessage); } - function test_quoteDispatch_dontOverrideGasLimit() public { - bytes memory igpMetadata = StandardHookMetadata.overrideGasLimit( - GAS_LIMIT * OVERRIDE_MULTIPLIER - ); - assertEq( - hook.quoteDispatch(igpMetadata, encodedMessage), - GAS_LIMIT * GAS_PRICE * OVERRIDE_MULTIPLIER - ); - } - /* ============ helper functions ============ */ function _expectOriginExternalBridgeCall( diff --git a/solidity/test/token/HypERC20.t.sol b/solidity/test/token/HypERC20.t.sol index 66acba13d6..c3b7da74cd 100644 --- a/solidity/test/token/HypERC20.t.sol +++ b/solidity/test/token/HypERC20.t.sol @@ -33,7 +33,7 @@ import {IXERC20} from "../../contracts/token/interfaces/IXERC20.sol"; import {IFiatToken} from "../../contracts/token/interfaces/IFiatToken.sol"; import {HypXERC20} from "../../contracts/token/extensions/HypXERC20.sol"; import {HypFiatToken} from "../../contracts/token/extensions/HypFiatToken.sol"; -import {HypNativeCollateral} from "../../contracts/token/HypNativeCollateral.sol"; +import {HypNative} from "../../contracts/token/HypNative.sol"; import {TokenRouter} from "../../contracts/token/libs/TokenRouter.sol"; import {TokenMessage} from "../../contracts/token/libs/TokenMessage.sol"; import {Message} from "../../contracts/libs/Message.sol"; @@ -623,16 +623,16 @@ contract HypFiatTokenTest is HypTokenTest { } } -contract HypNativeCollateralTest is HypTokenTest { +contract HypNativeTest is HypTokenTest { using TypeCasts for address; - HypNativeCollateral internal nativeToken; + HypNative internal nativeToken; function setUp() public override { super.setUp(); - localToken = new HypNativeCollateral(address(localMailbox)); - nativeToken = HypNativeCollateral(payable(address(localToken))); + localToken = new HypNative(address(localMailbox)); + nativeToken = HypNative(payable(address(localToken))); nativeToken.enrollRemoteRouter( DESTINATION, diff --git a/typescript/cli/cli.ts b/typescript/cli/cli.ts index 77be0b86f8..45cad33bb8 100644 --- a/typescript/cli/cli.ts +++ b/typescript/cli/cli.ts @@ -19,15 +19,17 @@ import { overrideRegistryUriCommandOption, registryUriCommandOption, skipConfirmationOption, + strategyCommandOption, } from './src/commands/options.js'; import { registryCommand } from './src/commands/registry.js'; import { relayerCommand } from './src/commands/relayer.js'; import { sendCommand } from './src/commands/send.js'; import { statusCommand } from './src/commands/status.js'; +import { strategyCommand } from './src/commands/strategy.js'; import { submitCommand } from './src/commands/submit.js'; import { validatorCommand } from './src/commands/validator.js'; import { warpCommand } from './src/commands/warp.js'; -import { contextMiddleware } from './src/context/context.js'; +import { contextMiddleware, signerMiddleware } from './src/context/context.js'; import { configureLogger, errorRed } from './src/logger.js'; import { checkVersion } from './src/utils/version-check.js'; import { VERSION } from './src/version.js'; @@ -49,12 +51,14 @@ try { .option('key', keyCommandOption) .option('disableProxy', disableProxyCommandOption) .option('yes', skipConfirmationOption) + .option('strategy', strategyCommandOption) .global(['log', 'verbosity', 'registry', 'overrides', 'yes']) .middleware([ (argv) => { configureLogger(argv.log as LogFormat, argv.verbosity as LogLevel); }, contextMiddleware, + signerMiddleware, ]) .command(avsCommand) .command(configCommand) @@ -66,6 +70,7 @@ try { .command(relayerCommand) .command(sendCommand) .command(statusCommand) + .command(strategyCommand) .command(submitCommand) .command(validatorCommand) .command(warpCommand) diff --git a/typescript/cli/src/commands/config.ts b/typescript/cli/src/commands/config.ts index e72b72452a..4a5c6b580a 100644 --- a/typescript/cli/src/commands/config.ts +++ b/typescript/cli/src/commands/config.ts @@ -3,6 +3,7 @@ import { CommandModule } from 'yargs'; import { readChainConfigs } from '../config/chain.js'; import { readIsmConfig } from '../config/ism.js'; import { readMultisigConfig } from '../config/multisig.js'; +import { readChainSubmissionStrategyConfig } from '../config/strategy.js'; import { readWarpRouteDeployConfig } from '../config/warp.js'; import { CommandModuleWithContext } from '../context/types.js'; import { log, logGreen } from '../logger.js'; @@ -31,6 +32,7 @@ const validateCommand: CommandModule = { .command(validateChainCommand) .command(validateIsmCommand) .command(validateIsmAdvancedCommand) + .command(validateStrategyCommand) .command(validateWarpCommand) .version(false) .demandCommand(), @@ -76,6 +78,19 @@ const validateIsmAdvancedCommand: CommandModuleWithContext<{ path: string }> = { }, }; +const validateStrategyCommand: CommandModuleWithContext<{ path: string }> = { + command: 'strategy', + describe: 'Validates a Strategy config file', + builder: { + path: inputFileCommandOption(), + }, + handler: async ({ path }) => { + await readChainSubmissionStrategyConfig(path); + logGreen('Config is valid'); + process.exit(0); + }, +}; + const validateWarpCommand: CommandModuleWithContext<{ path: string }> = { command: 'warp', describe: 'Validate a Warp Route deployment config file', diff --git a/typescript/cli/src/commands/options.ts b/typescript/cli/src/commands/options.ts index f23194c804..baf0fa8472 100644 --- a/typescript/cli/src/commands/options.ts +++ b/typescript/cli/src/commands/options.ts @@ -95,6 +95,7 @@ export const DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH = './configs/warp-route-deployment.yaml'; export const DEFAULT_CORE_DEPLOYMENT_CONFIG_PATH = './configs/core-config.yaml'; +export const DEFAULT_STRATEGY_CONFIG_PATH = `${os.homedir()}/.hyperlane/strategies/default-strategy.yaml`; export const warpDeploymentConfigCommandOption: Options = { type: 'string', @@ -196,8 +197,8 @@ export const transactionsCommandOption: Options = { export const strategyCommandOption: Options = { type: 'string', description: 'The submission strategy input file path.', - alias: 's', - demandOption: true, + alias: ['s', 'strategy'], + demandOption: false, }; export const addressCommandOption = ( diff --git a/typescript/cli/src/commands/signCommands.ts b/typescript/cli/src/commands/signCommands.ts index 37d83096a0..8cfa6d4c1a 100644 --- a/typescript/cli/src/commands/signCommands.ts +++ b/typescript/cli/src/commands/signCommands.ts @@ -1,11 +1,36 @@ // Commands that send tx and require a key to sign. // It's useful to have this listed here so the context // middleware can request keys up front when required. -export const SIGN_COMMANDS = ['deploy', 'send', 'status', 'submit', 'relayer']; +export const SIGN_COMMANDS = [ + 'apply', + 'deploy', + 'send', + 'status', + 'submit', + 'relayer', +]; export function isSignCommand(argv: any): boolean { + //TODO: fix reading and checking warp without signer, and remove this + const temporarySignCommandsCheck = + argv._[0] === 'warp' && (argv._[1] === 'read' || argv._[1] === 'check'); return ( SIGN_COMMANDS.includes(argv._[0]) || - (argv._.length > 1 && SIGN_COMMANDS.includes(argv._[1])) + (argv._.length > 1 && SIGN_COMMANDS.includes(argv._[1])) || + temporarySignCommandsCheck ); } + +export enum CommandType { + WARP_DEPLOY = 'warp:deploy', + WARP_SEND = 'warp:send', + WARP_APPLY = 'warp:apply', + WARP_READ = 'warp:read', + WARP_CHECK = 'warp:check', + SEND_MESSAGE = 'send:message', + AGENT_KURTOSIS = 'deploy:kurtosis-agents', + STATUS = 'status:', + SUBMIT = 'submit:', + RELAYER = 'relayer:', + CORE_APPLY = 'core:apply', +} diff --git a/typescript/cli/src/commands/strategy.ts b/typescript/cli/src/commands/strategy.ts new file mode 100644 index 0000000000..414a3d48ee --- /dev/null +++ b/typescript/cli/src/commands/strategy.ts @@ -0,0 +1,70 @@ +import { stringify as yamlStringify } from 'yaml'; +import { CommandModule } from 'yargs'; + +import { + createStrategyConfig, + readChainSubmissionStrategyConfig, +} from '../config/strategy.js'; +import { CommandModuleWithWriteContext } from '../context/types.js'; +import { log, logCommandHeader } from '../logger.js'; +import { indentYamlOrJson } from '../utils/files.js'; +import { maskSensitiveData } from '../utils/output.js'; + +import { + DEFAULT_STRATEGY_CONFIG_PATH, + outputFileCommandOption, + strategyCommandOption, +} from './options.js'; + +/** + * Parent command + */ +export const strategyCommand: CommandModule = { + command: 'strategy', + describe: 'Manage Hyperlane deployment strategies', + builder: (yargs) => + yargs.command(init).command(read).version(false).demandCommand(), + handler: () => log('Command required'), +}; + +export const init: CommandModuleWithWriteContext<{ + out: string; +}> = { + command: 'init', + describe: 'Creates strategy configuration', + builder: { + out: outputFileCommandOption(DEFAULT_STRATEGY_CONFIG_PATH), + }, + handler: async ({ context, out }) => { + logCommandHeader(`Hyperlane Strategy Init`); + + await createStrategyConfig({ + context, + outPath: out, + }); + process.exit(0); + }, +}; + +export const read: CommandModuleWithWriteContext<{ + strategy: string; +}> = { + command: 'read', + describe: 'Reads strategy configuration', + builder: { + strategy: { + ...strategyCommandOption, + demandOption: true, + default: DEFAULT_STRATEGY_CONFIG_PATH, + }, + }, + handler: async ({ strategy: strategyUrl }) => { + logCommandHeader(`Hyperlane Strategy Read`); + + const strategy = await readChainSubmissionStrategyConfig(strategyUrl); + const maskedConfig = maskSensitiveData(strategy); + log(indentYamlOrJson(yamlStringify(maskedConfig, null, 2), 4)); + + process.exit(0); + }, +}; diff --git a/typescript/cli/src/config/core.ts b/typescript/cli/src/config/core.ts index c7161f6b4b..5b4395c951 100644 --- a/typescript/cli/src/config/core.ts +++ b/typescript/cli/src/config/core.ts @@ -39,7 +39,7 @@ export async function createCoreDeployConfig({ logBlue('Creating a new core deployment config...'); const owner = await detectAndConfirmOrPrompt( - async () => context.signer?.getAddress(), + async () => context.signerAddress, ENTER_DESIRED_VALUE_MSG, 'owner address', SIGNER_PROMPT_LABEL, @@ -64,7 +64,7 @@ export async function createCoreDeployConfig({ }); proxyAdmin = { owner: await detectAndConfirmOrPrompt( - async () => context.signer?.getAddress(), + async () => context.signerAddress, ENTER_DESIRED_VALUE_MSG, 'ProxyAdmin owner address', SIGNER_PROMPT_LABEL, diff --git a/typescript/cli/src/config/hooks.ts b/typescript/cli/src/config/hooks.ts index e8df64dc00..5ad005dc24 100644 --- a/typescript/cli/src/config/hooks.ts +++ b/typescript/cli/src/config/hooks.ts @@ -243,10 +243,10 @@ async function getOwnerAndBeneficiary( advanced: boolean, ) { const unnormalizedOwner = - !advanced && context.signer - ? await context.signer.getAddress() + !advanced && context.signerAddress + ? context.signerAddress : await detectAndConfirmOrPrompt( - async () => context.signer?.getAddress(), + async () => context.signerAddress, `For ${module}, enter`, 'owner address', 'signer', diff --git a/typescript/cli/src/config/ism.ts b/typescript/cli/src/config/ism.ts index f7f6bab9ad..81440e96eb 100644 --- a/typescript/cli/src/config/ism.ts +++ b/typescript/cli/src/config/ism.ts @@ -163,10 +163,10 @@ export const createTrustedRelayerConfig = callWithConfigCreationLogs( advanced: boolean = false, ): Promise => { const relayer = - !advanced && context.signer - ? await context.signer.getAddress() + !advanced && context.signerAddress + ? context.signerAddress : await detectAndConfirmOrPrompt( - async () => context.signer?.getAddress(), + async () => context.signerAddress, 'For trusted relayer ISM, enter', 'relayer address', 'signer', diff --git a/typescript/cli/src/config/strategy.ts b/typescript/cli/src/config/strategy.ts new file mode 100644 index 0000000000..f57c7d3378 --- /dev/null +++ b/typescript/cli/src/config/strategy.ts @@ -0,0 +1,186 @@ +import { confirm, input, password, select } from '@inquirer/prompts'; +import { Wallet } from 'ethers'; +import { stringify as yamlStringify } from 'yaml'; + +import { + ChainSubmissionStrategy, + ChainSubmissionStrategySchema, + TxSubmitterType, +} from '@hyperlane-xyz/sdk'; +import { + ProtocolType, + assert, + errorToString, + isAddress, + isPrivateKeyEvm, +} from '@hyperlane-xyz/utils'; + +import { CommandContext } from '../context/types.js'; +import { errorRed, log, logBlue, logGreen, logRed } from '../logger.js'; +import { runSingleChainSelectionStep } from '../utils/chains.js'; +import { + indentYamlOrJson, + isFile, + readYamlOrJson, + writeYamlOrJson, +} from '../utils/files.js'; +import { maskSensitiveData } from '../utils/output.js'; + +/** + * Reads and validates a chain submission strategy configuration from a file + */ +export async function readChainSubmissionStrategyConfig( + filePath: string, +): Promise { + log(`Reading submission strategy in ${filePath}`); + try { + const strategyConfig = readYamlOrJson(filePath); + + const parseResult = ChainSubmissionStrategySchema.parse(strategyConfig); + + return parseResult; + } catch (error) { + logRed(`⛔️ Error reading strategy config:`, errorToString(error)); + throw error; // Re-throw to let caller handle the error + } +} + +/** + * Safely reads chain submission strategy config, returns empty object if any errors occur + */ +export async function safeReadChainSubmissionStrategyConfig( + filePath: string, +): Promise { + try { + const trimmedFilePath = filePath.trim(); + if (!isFile(trimmedFilePath)) { + logBlue(`File ${trimmedFilePath} does not exist, returning empty config`); + return {}; + } + return await readChainSubmissionStrategyConfig(trimmedFilePath); + } catch (error) { + logRed( + `Failed to read strategy config, defaulting to empty config:`, + errorToString(error), + ); + return {}; + } +} + +export async function createStrategyConfig({ + context, + outPath, +}: { + context: CommandContext; + outPath: string; +}) { + let strategy: ChainSubmissionStrategy; + try { + const strategyObj = await readYamlOrJson(outPath); + strategy = ChainSubmissionStrategySchema.parse(strategyObj); + } catch { + strategy = writeYamlOrJson(outPath, {}, 'yaml'); + } + + const chain = await runSingleChainSelectionStep(context.chainMetadata); + const chainProtocol = context.chainMetadata[chain].protocol; + + if ( + !context.skipConfirmation && + strategy && + Object.prototype.hasOwnProperty.call(strategy, chain) + ) { + const isConfirmed = await confirm({ + message: `Default strategy for chain ${chain} already exists. Are you sure you want to overwrite existing strategy config?`, + default: false, + }); + + assert(isConfirmed, 'Strategy initialization cancelled by user.'); + } + + const isEthereum = chainProtocol === ProtocolType.Ethereum; + const submitterType = isEthereum + ? await select({ + message: 'Select the submitter type', + choices: Object.values(TxSubmitterType).map((value) => ({ + name: value, + value: value, + })), + }) + : TxSubmitterType.JSON_RPC; // Do other non-evm chains support gnosis and account impersonation? + + const submitter: Record = { type: submitterType }; + + switch (submitterType) { + case TxSubmitterType.JSON_RPC: + submitter.privateKey = await password({ + message: 'Enter the private key for JSON-RPC submission:', + validate: (pk) => (isEthereum ? isPrivateKeyEvm(pk) : true), + }); + + submitter.userAddress = isEthereum + ? await new Wallet(submitter.privateKey).getAddress() + : await input({ + message: 'Enter the user address for JSON-RPC submission:', + }); + + submitter.chain = chain; + break; + + case TxSubmitterType.IMPERSONATED_ACCOUNT: + submitter.userAddress = await input({ + message: 'Enter the user address to impersonate', + validate: (address) => + isAddress(address) ? true : 'Invalid Ethereum address', + }); + assert( + submitter.userAddress, + 'User address is required for impersonated account', + ); + break; + + case TxSubmitterType.GNOSIS_SAFE: + case TxSubmitterType.GNOSIS_TX_BUILDER: + submitter.safeAddress = await input({ + message: 'Enter the Safe address', + validate: (address) => + isAddress(address) ? true : 'Invalid Safe address', + }); + + submitter.chain = chain; + + if (submitterType === TxSubmitterType.GNOSIS_TX_BUILDER) { + submitter.version = await input({ + message: 'Enter the Safe version (default: 1.0)', + default: '1.0', + }); + } + break; + + default: + throw new Error(`Unsupported submitter type: ${submitterType}`); + } + + const strategyResult: ChainSubmissionStrategy = { + ...strategy, + [chain]: { + submitter: submitter as ChainSubmissionStrategy[string]['submitter'], + }, + }; + + try { + const strategyConfig = ChainSubmissionStrategySchema.parse(strategyResult); + logBlue(`Strategy configuration is valid. Writing to file ${outPath}:\n`); + + const maskedConfig = maskSensitiveData(strategyConfig); + log(indentYamlOrJson(yamlStringify(maskedConfig, null, 2), 4)); + + writeYamlOrJson(outPath, strategyConfig); + logGreen('✅ Successfully created a new strategy configuration.'); + } catch { + // don't log error since it may contain sensitive data + errorRed( + `The strategy configuration is invalid. Please review the submitter settings.`, + ); + } +} diff --git a/typescript/cli/src/config/warp.ts b/typescript/cli/src/config/warp.ts index 1174d0156b..f31069e091 100644 --- a/typescript/cli/src/config/warp.ts +++ b/typescript/cli/src/config/warp.ts @@ -82,7 +82,7 @@ async function fillDefaults( let owner = config.owner; if (!owner) { owner = - (await context.signer?.getAddress()) ?? + context.signerAddress ?? (await context.multiProvider.getSignerAddress(chain)); } return { @@ -122,13 +122,6 @@ export async function createWarpRouteDeployConfig({ }) { logBlue('Creating a new warp route deployment config...'); - const owner = await detectAndConfirmOrPrompt( - async () => context.signer?.getAddress(), - 'Enter the desired', - 'owner address', - 'signer', - ); - const warpChains = await runMultiChainSelectionStep({ chainMetadata: context.chainMetadata, message: 'Select chains to connect', @@ -142,6 +135,12 @@ export async function createWarpRouteDeployConfig({ let typeChoices = TYPE_CHOICES; for (const chain of warpChains) { logBlue(`${chain}: Configuring warp route...`); + const owner = await detectAndConfirmOrPrompt( + async () => context.signerAddress, + 'Enter the desired', + 'owner address', + 'signer', + ); // default to the mailbox from the registry and if not found ask to the user to submit one const chainAddresses = await context.registry.getChainAddresses(chain); diff --git a/typescript/cli/src/context/context.ts b/typescript/cli/src/context/context.ts index f9dfb34ab9..570b233cde 100644 --- a/typescript/cli/src/context/context.ts +++ b/typescript/cli/src/context/context.ts @@ -1,5 +1,5 @@ import { confirm } from '@inquirer/prompts'; -import { ethers } from 'ethers'; +import { Signer, ethers } from 'ethers'; import { DEFAULT_GITHUB_REGISTRY, @@ -16,7 +16,9 @@ import { } from '@hyperlane-xyz/sdk'; import { isHttpsUrl, isNullish, rootLogger } from '@hyperlane-xyz/utils'; +import { DEFAULT_STRATEGY_CONFIG_PATH } from '../commands/options.js'; import { isSignCommand } from '../commands/signCommands.js'; +import { safeReadChainSubmissionStrategyConfig } from '../config/strategy.js'; import { PROXY_DEPLOYED_URL } from '../consts.js'; import { forkNetworkToMultiProvider, verifyAnvil } from '../deploy/dry-run.js'; import { logBlue } from '../logger.js'; @@ -24,6 +26,8 @@ import { runSingleChainSelectionStep } from '../utils/chains.js'; import { detectAndConfirmOrPrompt } from '../utils/input.js'; import { getImpersonatedSigner, getSigner } from '../utils/keys.js'; +import { ChainResolverFactory } from './strategies/chain/ChainResolverFactory.js'; +import { MultiProtocolSignerManager } from './strategies/signer/MultiProtocolSignerManager.js'; import { CommandContext, ContextSettings, @@ -41,6 +45,7 @@ export async function contextMiddleware(argv: Record) { requiresKey, disableProxy: argv.disableProxy, skipConfirmation: argv.yes, + strategyPath: argv.strategy, }; if (!isDryRun && settings.fromAddress) throw new Error( @@ -52,6 +57,44 @@ export async function contextMiddleware(argv: Record) { argv.context = context; } +export async function signerMiddleware(argv: Record) { + const { key, requiresKey, multiProvider, strategyPath } = argv.context; + + if (!requiresKey) return argv; + + const strategyConfig = await safeReadChainSubmissionStrategyConfig( + strategyPath ?? DEFAULT_STRATEGY_CONFIG_PATH, + ); + + /** + * Intercepts Hyperlane command to determine chains. + */ + const chainStrategy = ChainResolverFactory.getStrategy(argv); + + /** + * Resolves chains based on the chain strategy. + */ + const chains = await chainStrategy.resolveChains(argv); + + /** + * Extracts signer config + */ + const multiProtocolSigner = new MultiProtocolSignerManager( + strategyConfig, + chains, + multiProvider, + { key }, + ); + + /** + * @notice Attaches signers to MultiProvider and assigns it to argv.multiProvider + */ + argv.multiProvider = await multiProtocolSigner.getMultiProvider(); + argv.multiProtocolSigner = multiProtocolSigner; + + return argv; +} + /** * Retrieves context for the user-selected command * @returns context for the current command @@ -66,19 +109,24 @@ export async function getContext({ }: ContextSettings): Promise { const registry = getRegistry(registryUri, registryOverrideUri, !disableProxy); - let signer: ethers.Wallet | undefined = undefined; - if (key || requiresKey) { + //Just for backward compatibility + let signerAddress: string | undefined = undefined; + if (key) { + let signer: Signer; ({ key, signer } = await getSigner({ key, skipConfirmation })); + signerAddress = await signer.getAddress(); } - const multiProvider = await getMultiProvider(registry, signer); + + const multiProvider = await getMultiProvider(registry); return { registry, + requiresKey, chainMetadata: multiProvider.metadata, multiProvider, key, - signer, skipConfirmation: !!skipConfirmation, + signerAddress, } as CommandContext; } diff --git a/typescript/cli/src/context/strategies/chain/ChainResolverFactory.ts b/typescript/cli/src/context/strategies/chain/ChainResolverFactory.ts new file mode 100644 index 0000000000..e417ba27b3 --- /dev/null +++ b/typescript/cli/src/context/strategies/chain/ChainResolverFactory.ts @@ -0,0 +1,36 @@ +import { CommandType } from '../../../commands/signCommands.js'; + +import { MultiChainResolver } from './MultiChainResolver.js'; +import { SingleChainResolver } from './SingleChainResolver.js'; +import { ChainResolver } from './types.js'; + +/** + * @class ChainResolverFactory + * @description Intercepts commands to determine the appropriate chain resolver strategy based on command type. + */ +export class ChainResolverFactory { + private static strategyMap: Map ChainResolver> = new Map([ + [CommandType.WARP_DEPLOY, () => MultiChainResolver.forWarpRouteConfig()], + [CommandType.WARP_SEND, () => MultiChainResolver.forOriginDestination()], + [CommandType.WARP_APPLY, () => MultiChainResolver.forWarpRouteConfig()], + [CommandType.WARP_READ, () => MultiChainResolver.forWarpCoreConfig()], + [CommandType.WARP_CHECK, () => MultiChainResolver.forWarpCoreConfig()], + [CommandType.SEND_MESSAGE, () => MultiChainResolver.forOriginDestination()], + [CommandType.AGENT_KURTOSIS, () => MultiChainResolver.forAgentKurtosis()], + [CommandType.STATUS, () => MultiChainResolver.forOriginDestination()], + [CommandType.SUBMIT, () => MultiChainResolver.forStrategyConfig()], + [CommandType.RELAYER, () => MultiChainResolver.forRelayer()], + [CommandType.CORE_APPLY, () => MultiChainResolver.forCoreApply()], + ]); + + /** + * @param argv - Command line arguments. + * @returns ChainResolver - The appropriate chain resolver strategy based on the command type. + */ + static getStrategy(argv: Record): ChainResolver { + const commandKey = `${argv._[0]}:${argv._[1] || ''}`.trim() as CommandType; + const createStrategy = + this.strategyMap.get(commandKey) || (() => new SingleChainResolver()); + return createStrategy(); + } +} diff --git a/typescript/cli/src/context/strategies/chain/MultiChainResolver.ts b/typescript/cli/src/context/strategies/chain/MultiChainResolver.ts new file mode 100644 index 0000000000..64f3257520 --- /dev/null +++ b/typescript/cli/src/context/strategies/chain/MultiChainResolver.ts @@ -0,0 +1,249 @@ +import { + ChainMap, + ChainName, + DeployedCoreAddresses, + DeployedCoreAddressesSchema, + EvmCoreModule, +} from '@hyperlane-xyz/sdk'; +import { assert } from '@hyperlane-xyz/utils'; + +import { DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH } from '../../../commands/options.js'; +import { readCoreDeployConfigs } from '../../../config/core.js'; +import { readChainSubmissionStrategyConfig } from '../../../config/strategy.js'; +import { log } from '../../../logger.js'; +import { + extractChainsFromObj, + runMultiChainSelectionStep, + runSingleChainSelectionStep, +} from '../../../utils/chains.js'; +import { + isFile, + readYamlOrJson, + runFileSelectionStep, +} from '../../../utils/files.js'; +import { getWarpCoreConfigOrExit } from '../../../utils/warp.js'; + +import { ChainResolver } from './types.js'; + +enum ChainSelectionMode { + ORIGIN_DESTINATION, + AGENT_KURTOSIS, + WARP_CONFIG, + WARP_READ, + STRATEGY, + RELAYER, + CORE_APPLY, +} + +// This class could be broken down into multiple strategies + +/** + * @title MultiChainResolver + * @notice Resolves chains based on the specified selection mode. + */ +export class MultiChainResolver implements ChainResolver { + constructor(private mode: ChainSelectionMode) {} + + async resolveChains(argv: ChainMap): Promise { + switch (this.mode) { + case ChainSelectionMode.WARP_CONFIG: + return this.resolveWarpRouteConfigChains(argv); + case ChainSelectionMode.WARP_READ: + return this.resolveWarpCoreConfigChains(argv); + case ChainSelectionMode.AGENT_KURTOSIS: + return this.resolveAgentChains(argv); + case ChainSelectionMode.STRATEGY: + return this.resolveStrategyChains(argv); + case ChainSelectionMode.RELAYER: + return this.resolveRelayerChains(argv); + case ChainSelectionMode.CORE_APPLY: + return this.resolveCoreApplyChains(argv); + case ChainSelectionMode.ORIGIN_DESTINATION: + default: + return this.resolveOriginDestinationChains(argv); + } + } + + private async resolveWarpRouteConfigChains( + argv: Record, + ): Promise { + argv.config ||= DEFAULT_WARP_ROUTE_DEPLOYMENT_CONFIG_PATH; + argv.context.chains = await this.getWarpRouteConfigChains( + argv.config.trim(), + argv.skipConfirmation, + ); + return argv.context.chains; + } + + private async resolveWarpCoreConfigChains( + argv: Record, + ): Promise { + if (argv.symbol || argv.warp) { + const warpCoreConfig = await getWarpCoreConfigOrExit({ + context: argv.context, + warp: argv.warp, + symbol: argv.symbol, + }); + argv.context.warpCoreConfig = warpCoreConfig; + const chains = extractChainsFromObj(warpCoreConfig); + return chains; + } else if (argv.chain) { + return [argv.chain]; + } else { + throw new Error( + `Please specify either a symbol, chain and address or warp file`, + ); + } + } + + private async resolveAgentChains( + argv: Record, + ): Promise { + const { chainMetadata } = argv.context; + argv.origin = + argv.origin ?? + (await runSingleChainSelectionStep( + chainMetadata, + 'Select the origin chain', + )); + + if (!argv.targets) { + const selectedRelayChains = await runMultiChainSelectionStep({ + chainMetadata: chainMetadata, + message: 'Select chains to relay between', + requireNumber: 2, + }); + argv.targets = selectedRelayChains.join(','); + } + + return [argv.origin, ...argv.targets]; + } + + private async resolveOriginDestinationChains( + argv: Record, + ): Promise { + const { chainMetadata } = argv.context; + + argv.origin = + argv.origin ?? + (await runSingleChainSelectionStep( + chainMetadata, + 'Select the origin chain', + )); + + argv.destination = + argv.destination ?? + (await runSingleChainSelectionStep( + chainMetadata, + 'Select the destination chain', + )); + + return [argv.origin, argv.destination]; + } + + private async resolveStrategyChains( + argv: Record, + ): Promise { + const strategy = await readChainSubmissionStrategyConfig(argv.strategy); + return extractChainsFromObj(strategy); + } + + private async resolveRelayerChains( + argv: Record, + ): Promise { + return argv.chains.split(',').map((item: string) => item.trim()); + } + + private async getWarpRouteConfigChains( + configPath: string, + skipConfirmation: boolean, + ): Promise { + if (!configPath || !isFile(configPath)) { + assert(!skipConfirmation, 'Warp route deployment config is required'); + configPath = await runFileSelectionStep( + './configs', + 'Warp route deployment config', + 'warp', + ); + } else { + log(`Using warp route deployment config at ${configPath}`); + } + + // Alternative to readWarpRouteDeployConfig that doesn't use context for signer and zod validation + const warpRouteConfig = (await readYamlOrJson(configPath)) as Record< + string, + any + >; + + const chains = Object.keys(warpRouteConfig) as ChainName[]; + assert( + chains.length !== 0, + 'No chains found in warp route deployment config', + ); + + return chains; + } + + private async resolveCoreApplyChains( + argv: Record, + ): Promise { + try { + const config = readCoreDeployConfigs(argv.config); + + if (!config?.interchainAccountRouter) { + return [argv.chain]; + } + + const addresses = await argv.context.registry.getChainAddresses( + argv.chain, + ); + const coreAddresses = DeployedCoreAddressesSchema.parse( + addresses, + ) as DeployedCoreAddresses; + + const evmCoreModule = new EvmCoreModule(argv.context.multiProvider, { + chain: argv.chain, + config, + addresses: coreAddresses, + }); + + const transactions = await evmCoreModule.update(config); + + return Array.from(new Set(transactions.map((tx) => tx.chainId))).map( + (chainId) => argv.context.multiProvider.getChainName(chainId), + ); + } catch (error) { + throw new Error(`Failed to resolve core apply chains`, { + cause: error, + }); + } + } + + static forAgentKurtosis(): MultiChainResolver { + return new MultiChainResolver(ChainSelectionMode.AGENT_KURTOSIS); + } + + static forOriginDestination(): MultiChainResolver { + return new MultiChainResolver(ChainSelectionMode.ORIGIN_DESTINATION); + } + + static forRelayer(): MultiChainResolver { + return new MultiChainResolver(ChainSelectionMode.RELAYER); + } + + static forStrategyConfig(): MultiChainResolver { + return new MultiChainResolver(ChainSelectionMode.STRATEGY); + } + + static forWarpRouteConfig(): MultiChainResolver { + return new MultiChainResolver(ChainSelectionMode.WARP_CONFIG); + } + + static forWarpCoreConfig(): MultiChainResolver { + return new MultiChainResolver(ChainSelectionMode.WARP_READ); + } + + static forCoreApply(): MultiChainResolver { + return new MultiChainResolver(ChainSelectionMode.CORE_APPLY); + } +} diff --git a/typescript/cli/src/context/strategies/chain/SingleChainResolver.ts b/typescript/cli/src/context/strategies/chain/SingleChainResolver.ts new file mode 100644 index 0000000000..8dddaf3c4a --- /dev/null +++ b/typescript/cli/src/context/strategies/chain/SingleChainResolver.ts @@ -0,0 +1,25 @@ +import { ChainMap, ChainName } from '@hyperlane-xyz/sdk'; + +import { runSingleChainSelectionStep } from '../../../utils/chains.js'; + +import { ChainResolver } from './types.js'; + +/** + * @title SingleChainResolver + * @notice Strategy implementation for managing single-chain operations + * @dev Primarily used for operations like 'core:apply' and 'warp:read' + */ +export class SingleChainResolver implements ChainResolver { + /** + * @notice Determines the chain to be used for signing operations + * @dev Either uses the chain specified in argv or prompts for interactive selection + */ + async resolveChains(argv: ChainMap): Promise { + argv.chain ||= await runSingleChainSelectionStep( + argv.context.chainMetadata, + 'Select chain to connect:', + ); + + return [argv.chain]; // Explicitly return as single-item array + } +} diff --git a/typescript/cli/src/context/strategies/chain/types.ts b/typescript/cli/src/context/strategies/chain/types.ts new file mode 100644 index 0000000000..9318bed8c2 --- /dev/null +++ b/typescript/cli/src/context/strategies/chain/types.ts @@ -0,0 +1,10 @@ +import { ChainMap, ChainName } from '@hyperlane-xyz/sdk'; + +export interface ChainResolver { + /** + * Determines the chains to be used for signing + * @param argv Command arguments + * @returns Array of chain names + */ + resolveChains(argv: ChainMap): Promise; +} diff --git a/typescript/cli/src/context/strategies/signer/BaseMultiProtocolSigner.ts b/typescript/cli/src/context/strategies/signer/BaseMultiProtocolSigner.ts new file mode 100644 index 0000000000..b91242b42d --- /dev/null +++ b/typescript/cli/src/context/strategies/signer/BaseMultiProtocolSigner.ts @@ -0,0 +1,22 @@ +import { Signer } from 'ethers'; + +import { ChainName, ChainSubmissionStrategy } from '@hyperlane-xyz/sdk'; +import { Address } from '@hyperlane-xyz/utils'; + +export interface SignerConfig { + privateKey: string; + address?: Address; // For chains like StarkNet that require address + extraParams?: Record; // For any additional chain-specific params +} + +export interface IMultiProtocolSigner { + getSignerConfig(chain: ChainName): Promise | SignerConfig; + getSigner(config: SignerConfig): Signer; +} + +export abstract class BaseMultiProtocolSigner implements IMultiProtocolSigner { + constructor(protected config: ChainSubmissionStrategy) {} + + abstract getSignerConfig(chain: ChainName): Promise; + abstract getSigner(config: SignerConfig): Signer; +} diff --git a/typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts b/typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts new file mode 100644 index 0000000000..030f11b5f4 --- /dev/null +++ b/typescript/cli/src/context/strategies/signer/MultiProtocolSignerFactory.ts @@ -0,0 +1,79 @@ +import { password } from '@inquirer/prompts'; +import { Signer, Wallet } from 'ethers'; + +import { + ChainName, + ChainSubmissionStrategy, + ChainTechnicalStack, + MultiProvider, + TxSubmitterType, +} from '@hyperlane-xyz/sdk'; +import { ProtocolType } from '@hyperlane-xyz/utils'; + +import { + BaseMultiProtocolSigner, + IMultiProtocolSigner, + SignerConfig, +} from './BaseMultiProtocolSigner.js'; + +export class MultiProtocolSignerFactory { + static getSignerStrategy( + chain: ChainName, + strategyConfig: ChainSubmissionStrategy, + multiProvider: MultiProvider, + ): IMultiProtocolSigner { + const { protocol, technicalStack } = multiProvider.getChainMetadata(chain); + + switch (protocol) { + case ProtocolType.Ethereum: + if (technicalStack === ChainTechnicalStack.ZkSync) + return new ZKSyncSignerStrategy(strategyConfig); + return new EthereumSignerStrategy(strategyConfig); + default: + throw new Error(`Unsupported protocol: ${protocol}`); + } + } +} + +class EthereumSignerStrategy extends BaseMultiProtocolSigner { + async getSignerConfig(chain: ChainName): Promise { + const submitter = this.config[chain]?.submitter as { + type: TxSubmitterType.JSON_RPC; + privateKey?: string; + }; + + const privateKey = + submitter?.privateKey ?? + (await password({ + message: `Please enter the private key for chain ${chain}`, + })); + + return { privateKey }; + } + + getSigner(config: SignerConfig): Signer { + return new Wallet(config.privateKey); + } +} + +// 99% overlap with EthereumSignerStrategy for the sake of keeping MultiProtocolSignerFactory clean +// TODO: import ZKSync signer +class ZKSyncSignerStrategy extends BaseMultiProtocolSigner { + async getSignerConfig(chain: ChainName): Promise { + const submitter = this.config[chain]?.submitter as { + privateKey?: string; + }; + + const privateKey = + submitter?.privateKey ?? + (await password({ + message: `Please enter the private key for chain ${chain}`, + })); + + return { privateKey }; + } + + getSigner(config: SignerConfig): Signer { + return new Wallet(config.privateKey); + } +} diff --git a/typescript/cli/src/context/strategies/signer/MultiProtocolSignerManager.ts b/typescript/cli/src/context/strategies/signer/MultiProtocolSignerManager.ts new file mode 100644 index 0000000000..12f9c0f819 --- /dev/null +++ b/typescript/cli/src/context/strategies/signer/MultiProtocolSignerManager.ts @@ -0,0 +1,153 @@ +import { Signer } from 'ethers'; +import { Logger } from 'pino'; + +import { + ChainName, + ChainSubmissionStrategy, + MultiProvider, +} from '@hyperlane-xyz/sdk'; +import { assert, rootLogger } from '@hyperlane-xyz/utils'; + +import { ENV } from '../../../utils/env.js'; + +import { IMultiProtocolSigner } from './BaseMultiProtocolSigner.js'; +import { MultiProtocolSignerFactory } from './MultiProtocolSignerFactory.js'; + +export interface MultiProtocolSignerOptions { + logger?: Logger; + key?: string; +} + +/** + * @title MultiProtocolSignerManager + * @dev Context manager for signers across multiple protocols + */ +export class MultiProtocolSignerManager { + protected readonly signerStrategies: Map; + protected readonly signers: Map; + public readonly logger: Logger; + + constructor( + protected readonly submissionStrategy: ChainSubmissionStrategy, + protected readonly chains: ChainName[], + protected readonly multiProvider: MultiProvider, + protected readonly options: MultiProtocolSignerOptions = {}, + ) { + this.logger = + options?.logger || + rootLogger.child({ + module: 'MultiProtocolSignerManager', + }); + this.signerStrategies = new Map(); + this.signers = new Map(); + this.initializeStrategies(); + } + + /** + * @notice Sets up chain-specific signer strategies + */ + protected initializeStrategies(): void { + for (const chain of this.chains) { + const strategy = MultiProtocolSignerFactory.getSignerStrategy( + chain, + this.submissionStrategy, + this.multiProvider, + ); + this.signerStrategies.set(chain, strategy); + } + } + + /** + * @dev Configures signers for EVM chains in MultiProvider + */ + async getMultiProvider(): Promise { + for (const chain of this.chains) { + const signer = await this.initSigner(chain); + this.multiProvider.setSigner(chain, signer); + } + + return this.multiProvider; + } + + /** + * @notice Creates signer for specific chain + */ + async initSigner(chain: ChainName): Promise { + const { privateKey } = await this.resolveConfig(chain); + + const signerStrategy = this.signerStrategies.get(chain); + assert(signerStrategy, `No signer strategy found for chain ${chain}`); + + return signerStrategy.getSigner({ privateKey }); + } + + /** + * @notice Creates signers for all chains + */ + async initAllSigners(): Promise { + const signerConfigs = await this.resolveAllConfigs(); + + for (const { chain, privateKey } of signerConfigs) { + const signerStrategy = this.signerStrategies.get(chain); + if (signerStrategy) { + this.signers.set(chain, signerStrategy.getSigner({ privateKey })); + } + } + + return this.signers; + } + + /** + * @notice Resolves all chain configurations + */ + private async resolveAllConfigs(): Promise< + Array<{ chain: ChainName; privateKey: string }> + > { + return Promise.all(this.chains.map((chain) => this.resolveConfig(chain))); + } + + /** + * @notice Resolves single chain configuration + */ + private async resolveConfig( + chain: ChainName, + ): Promise<{ chain: ChainName; privateKey: string }> { + const signerStrategy = this.signerStrategies.get(chain); + assert(signerStrategy, `No signer strategy found for chain ${chain}`); + + let privateKey: string; + + if (this.options.key) { + this.logger.info( + `Using private key passed via CLI --key flag for chain ${chain}`, + ); + privateKey = this.options.key; + } else if (ENV.HYP_KEY) { + this.logger.info(`Using private key from .env for chain ${chain}`); + privateKey = ENV.HYP_KEY; + } else { + privateKey = await this.extractPrivateKey(chain, signerStrategy); + } + + return { chain, privateKey }; + } + + /** + * @notice Gets private key from strategy + */ + private async extractPrivateKey( + chain: ChainName, + signerStrategy: IMultiProtocolSigner, + ): Promise { + const strategyConfig = await signerStrategy.getSignerConfig(chain); + assert( + strategyConfig.privateKey, + `No private key found for chain ${chain}`, + ); + + this.logger.info( + `Extracting private key from strategy config/user prompt for chain ${chain}`, + ); + return strategyConfig.privateKey; + } +} diff --git a/typescript/cli/src/context/types.ts b/typescript/cli/src/context/types.ts index 6c3a17c5ff..c320ff3cac 100644 --- a/typescript/cli/src/context/types.ts +++ b/typescript/cli/src/context/types.ts @@ -6,6 +6,7 @@ import type { ChainMap, ChainMetadata, MultiProvider, + WarpCoreConfig, } from '@hyperlane-xyz/sdk'; export interface ContextSettings { @@ -16,6 +17,7 @@ export interface ContextSettings { requiresKey?: boolean; disableProxy?: boolean; skipConfirmation?: boolean; + strategyPath?: string; } export interface CommandContext { @@ -24,7 +26,10 @@ export interface CommandContext { multiProvider: MultiProvider; skipConfirmation: boolean; key?: string; - signer?: ethers.Signer; + // just for evm chains backward compatibility + signerAddress?: string; + warpCoreConfig?: WarpCoreConfig; + strategyPath?: string; } export interface WriteCommandContext extends CommandContext { diff --git a/typescript/cli/src/deploy/agent.ts b/typescript/cli/src/deploy/agent.ts index ca490fc5fb..a36955a3f3 100644 --- a/typescript/cli/src/deploy/agent.ts +++ b/typescript/cli/src/deploy/agent.ts @@ -21,6 +21,7 @@ export async function runKurtosisAgentDeploy({ relayChains?: string; agentConfigurationPath?: string; }) { + // Future works: decide what to do with this, since its handled in MultiChainResolver - AGENT_KURTOSIS mode if (!originChain) { originChain = await runSingleChainSelectionStep( context.chainMetadata, diff --git a/typescript/cli/src/deploy/core.ts b/typescript/cli/src/deploy/core.ts index 06d08b13cd..7ce8a0247c 100644 --- a/typescript/cli/src/deploy/core.ts +++ b/typescript/cli/src/deploy/core.ts @@ -43,7 +43,6 @@ export async function runCoreDeploy(params: DeployParams) { let chain = params.chain; const { - signer, isDryRun, chainMetadata, dryRunChain, @@ -62,13 +61,14 @@ export async function runCoreDeploy(params: DeployParams) { 'Select chain to connect:', ); } - let apiKeys: ChainMap = {}; if (!skipConfirmation) apiKeys = await requestAndSaveApiKeys([chain], chainMetadata, registry); + const signer = multiProvider.getSigner(chain); + const deploymentParams: DeployParams = { - context, + context: { ...context, signer }, chain, config, }; diff --git a/typescript/cli/src/deploy/utils.ts b/typescript/cli/src/deploy/utils.ts index 125e7b1e77..f5ac01a175 100644 --- a/typescript/cli/src/deploy/utils.ts +++ b/typescript/cli/src/deploy/utils.ts @@ -41,7 +41,7 @@ export async function runPreflightChecksForChains({ chainsToGasCheck?: ChainName[]; }) { log('Running pre-flight checks for chains...'); - const { signer, multiProvider } = context; + const { multiProvider } = context; if (!chains?.length) throw new Error('Empty chain selection'); for (const chain of chains) { @@ -49,15 +49,14 @@ export async function runPreflightChecksForChains({ if (!metadata) throw new Error(`No chain config found for ${chain}`); if (metadata.protocol !== ProtocolType.Ethereum) throw new Error('Only Ethereum chains are supported for now'); + const signer = multiProvider.getSigner(chain); + assertSigner(signer); + logGreen(`✅ ${chain} signer is valid`); } logGreen('✅ Chains are valid'); - assertSigner(signer); - logGreen('✅ Signer is valid'); - await nativeBalancesAreSufficient( multiProvider, - signer, chainsToGasCheck ?? chains, minGas, ); @@ -70,8 +69,13 @@ export async function runDeployPlanStep({ context: WriteCommandContext; chain: ChainName; }) { - const { signer, chainMetadata: chainMetadataMap, skipConfirmation } = context; - const address = await signer.getAddress(); + const { + chainMetadata: chainMetadataMap, + multiProvider, + skipConfirmation, + } = context; + + const address = await multiProvider.getSigner(chain).getAddress(); logBlue('\nDeployment plan'); logGray('==============='); @@ -124,7 +128,7 @@ export function isZODISMConfig(filepath: string): boolean { export async function prepareDeploy( context: WriteCommandContext, - userAddress: Address, + userAddress: Address | null, chains: ChainName[], ): Promise> { const { multiProvider, isDryRun } = context; @@ -134,7 +138,9 @@ export async function prepareDeploy( const provider = isDryRun ? getLocalProvider(ENV.ANVIL_IP_ADDR, ENV.ANVIL_PORT) : multiProvider.getProvider(chain); - const currentBalance = await provider.getBalance(userAddress); + const address = + userAddress ?? (await multiProvider.getSigner(chain).getAddress()); + const currentBalance = await provider.getBalance(address); initialBalances[chain] = currentBalance; }), ); @@ -145,7 +151,7 @@ export async function completeDeploy( context: WriteCommandContext, command: string, initialBalances: Record, - userAddress: Address, + userAddress: Address | null, chains: ChainName[], ) { const { multiProvider, isDryRun } = context; @@ -154,7 +160,9 @@ export async function completeDeploy( const provider = isDryRun ? getLocalProvider(ENV.ANVIL_IP_ADDR, ENV.ANVIL_PORT) : multiProvider.getProvider(chain); - const currentBalance = await provider.getBalance(userAddress); + const address = + userAddress ?? (await multiProvider.getSigner(chain).getAddress()); + const currentBalance = await provider.getBalance(address); const balanceDelta = initialBalances[chain].sub(currentBalance); if (isDryRun && balanceDelta.lt(0)) break; logPink( diff --git a/typescript/cli/src/deploy/warp.ts b/typescript/cli/src/deploy/warp.ts index 018244700a..e94bd709da 100644 --- a/typescript/cli/src/deploy/warp.ts +++ b/typescript/cli/src/deploy/warp.ts @@ -102,7 +102,7 @@ export async function runWarpRouteDeploy({ context: WriteCommandContext; warpRouteDeploymentConfigPath?: string; }) { - const { signer, skipConfirmation, chainMetadata, registry } = context; + const { skipConfirmation, chainMetadata, registry } = context; if ( !warpRouteDeploymentConfigPath || @@ -149,13 +149,8 @@ export async function runWarpRouteDeploy({ minGas: MINIMUM_WARP_DEPLOY_GAS, }); - const userAddress = await signer.getAddress(); + const initialBalances = await prepareDeploy(context, null, ethereumChains); - const initialBalances = await prepareDeploy( - context, - userAddress, - ethereumChains, - ); const deployedContracts = await executeDeploy(deploymentParams, apiKeys); const warpCoreConfig = await getWarpCoreConfig( @@ -165,13 +160,7 @@ export async function runWarpRouteDeploy({ await writeDeploymentArtifacts(warpCoreConfig, context); - await completeDeploy( - context, - 'warp', - initialBalances, - userAddress, - ethereumChains, - ); + await completeDeploy(context, 'warp', initialBalances, null, ethereumChains!); } async function runDeployPlanStep({ context, warpDeployConfig }: DeployParams) { diff --git a/typescript/cli/src/read/warp.ts b/typescript/cli/src/read/warp.ts index bd5d01e95e..169593c5e9 100644 --- a/typescript/cli/src/read/warp.ts +++ b/typescript/cli/src/read/warp.ts @@ -34,11 +34,13 @@ export async function runWarpRouteRead({ let addresses: ChainMap; if (symbol || warp) { - const warpCoreConfig = await getWarpCoreConfigOrExit({ - context, - warp, - symbol, - }); + const warpCoreConfig = + context.warpCoreConfig ?? // this case is be handled by MultiChainHandler.forWarpCoreConfig() interceptor + (await getWarpCoreConfigOrExit({ + context, + warp, + symbol, + })); // TODO: merge with XERC20TokenAdapter and WarpRouteReader const xerc20Limits = await Promise.all( diff --git a/typescript/cli/src/send/transfer.ts b/typescript/cli/src/send/transfer.ts index a89eb6aa99..2929b09c6e 100644 --- a/typescript/cli/src/send/transfer.ts +++ b/typescript/cli/src/send/transfer.ts @@ -40,8 +40,8 @@ export async function sendTestTransfer({ }: { context: WriteCommandContext; warpCoreConfig: WarpCoreConfig; - origin?: ChainName; - destination?: ChainName; + origin?: ChainName; // resolved in signerMiddleware + destination?: ChainName; // resolved in signerMiddleware amount: string; recipient?: string; timeoutSec: number; @@ -106,10 +106,15 @@ async function executeDelivery({ skipWaitForDelivery: boolean; selfRelay?: boolean; }) { - const { signer, multiProvider, registry } = context; + const { multiProvider, registry } = context; + const signer = multiProvider.getSigner(origin); + const recipientSigner = multiProvider.getSigner(destination); + + const recipientAddress = await recipientSigner.getAddress(); const signerAddress = await signer.getAddress(); - recipient ||= signerAddress; + + recipient ||= recipientAddress; const chainAddresses = await registry.getAddresses(); @@ -136,12 +141,11 @@ async function executeDelivery({ token = warpCore.findToken(origin, routerAddress)!; } - const senderAddress = await signer.getAddress(); const errors = await warpCore.validateTransfer({ originTokenAmount: token.amount(amount), destination, - recipient: recipient ?? senderAddress, - sender: senderAddress, + recipient, + sender: signerAddress, }); if (errors) { logRed('Error validating transfer', JSON.stringify(errors)); @@ -152,8 +156,8 @@ async function executeDelivery({ const transferTxs = await warpCore.getTransferRemoteTxs({ originTokenAmount: new TokenAmount(amount, token), destination, - sender: senderAddress, - recipient: recipient ?? senderAddress, + sender: signerAddress, + recipient, }); const txReceipts = []; @@ -172,7 +176,7 @@ async function executeDelivery({ const parsed = parseWarpRouteMessage(message.parsed.body); logBlue( - `Sent transfer from sender (${senderAddress}) on ${origin} to recipient (${recipient}) on ${destination}.`, + `Sent transfer from sender (${signerAddress}) on ${origin} to recipient (${recipient}) on ${destination}.`, ); logBlue(`Message ID: ${message.id}`); log(`Message:\n${indentYamlOrJson(yamlStringify(message, null, 2), 4)}`); diff --git a/typescript/cli/src/tests/commands/helpers.ts b/typescript/cli/src/tests/commands/helpers.ts index c6bfdc9a7c..7853815459 100644 --- a/typescript/cli/src/tests/commands/helpers.ts +++ b/typescript/cli/src/tests/commands/helpers.ts @@ -1,3 +1,4 @@ +import { ethers } from 'ethers'; import { $ } from 'zx'; import { ERC20Test__factory, ERC4626Test__factory } from '@hyperlane-xyz/core'; @@ -160,6 +161,9 @@ export async function deployToken(privateKey: string, chain: string) { key: privateKey, }); + // Future works: make signer compatible with protocol/chain stack + multiProvider.setSigner(chain, new ethers.Wallet(privateKey)); + const token = await new ERC20Test__factory( multiProvider.getSigner(chain), ).deploy('token', 'token', '100000000000000000000', 18); @@ -179,6 +183,9 @@ export async function deploy4626Vault( key: privateKey, }); + // Future works: make signer compatible with protocol/chain stack + multiProvider.setSigner(chain, new ethers.Wallet(privateKey)); + const vault = await new ERC4626Test__factory( multiProvider.getSigner(chain), ).deploy(tokenAddress, 'VAULT', 'VAULT'); diff --git a/typescript/cli/src/utils/balances.ts b/typescript/cli/src/utils/balances.ts index 4536353e57..2a6e6fcb8a 100644 --- a/typescript/cli/src/utils/balances.ts +++ b/typescript/cli/src/utils/balances.ts @@ -8,12 +8,9 @@ import { logGray, logGreen, logRed } from '../logger.js'; export async function nativeBalancesAreSufficient( multiProvider: MultiProvider, - signer: ethers.Signer, chains: ChainName[], minGas: string, ) { - const address = await signer.getAddress(); - const sufficientBalances: boolean[] = []; for (const chain of chains) { // Only Ethereum chains are supported @@ -21,7 +18,7 @@ export async function nativeBalancesAreSufficient( logGray(`Skipping balance check for non-EVM chain: ${chain}`); continue; } - + const address = multiProvider.getSigner(chain).getAddress(); const provider = multiProvider.getProvider(chain); const gasPrice = await provider.getGasPrice(); const minBalanceWei = gasPrice.mul(minGas).toString(); diff --git a/typescript/cli/src/utils/chains.ts b/typescript/cli/src/utils/chains.ts index add11203d0..7e2eaccd0a 100644 --- a/typescript/cli/src/utils/chains.ts +++ b/typescript/cli/src/utils/chains.ts @@ -171,3 +171,36 @@ function handleNewChain(chainNames: string[]) { process.exit(0); } } + +/** + * @notice Extracts chain names from a nested configuration object + * @param config Object to search for chain names + * @return Array of discovered chain names + */ +export function extractChainsFromObj(config: Record): string[] { + const chains: string[] = []; + + // Recursively search for chain/chainName fields + function findChainFields(obj: any) { + if (obj === null || typeof obj !== 'object') return; + + if (Array.isArray(obj)) { + obj.forEach((item) => findChainFields(item)); + return; + } + + if ('chain' in obj) { + chains.push(obj.chain); + } + + if ('chainName' in obj) { + chains.push(obj.chainName); + } + + // Recursively search in all nested values + Object.values(obj).forEach((value) => findChainFields(value)); + } + + findChainFields(config); + return chains; +} diff --git a/typescript/cli/src/utils/output.ts b/typescript/cli/src/utils/output.ts index 442b8a0906..2e1acfdf41 100644 --- a/typescript/cli/src/utils/output.ts +++ b/typescript/cli/src/utils/output.ts @@ -54,3 +54,50 @@ export function formatYamlViolationsOutput( return highlightedLines.join('\n'); } + +/** + * @notice Masks sensitive key with dots + * @param key Sensitive key to mask + * @return Masked key + */ +export function maskSensitiveKey(key: string): string { + if (!key) return key; + const middle = '•'.repeat(key.length); + return `${middle}`; +} + +const SENSITIVE_PATTERNS = [ + 'privatekey', + 'key', + 'secret', + 'secretkey', + 'password', +]; + +const isSensitiveKey = (key: string) => { + const lowerKey = key.toLowerCase(); + return SENSITIVE_PATTERNS.some((pattern) => lowerKey.includes(pattern)); +}; + +/** + * @notice Recursively masks sensitive data in objects + * @param obj Object with potential sensitive data + * @return Object with masked sensitive data + */ +export function maskSensitiveData(obj: any): any { + if (!obj) return obj; + + if (typeof obj === 'object') { + const masked = { ...obj }; + for (const [key, value] of Object.entries(masked)) { + if (isSensitiveKey(key) && typeof value === 'string') { + masked[key] = maskSensitiveKey(value); + } else if (typeof value === 'object') { + masked[key] = maskSensitiveData(value); + } + } + return masked; + } + + return obj; +} diff --git a/typescript/infra/config/environments/mainnet3/agent.ts b/typescript/infra/config/environments/mainnet3/agent.ts index 58c42ffd81..9fa5ede3c2 100644 --- a/typescript/infra/config/environments/mainnet3/agent.ts +++ b/typescript/infra/config/environments/mainnet3/agent.ts @@ -276,7 +276,7 @@ export const hyperlaneContextAgentChainConfig: AgentChainConfig< degenchain: true, dogechain: true, duckchain: true, - // Cannot scrape Sealevel chains + // Disabled until we get archival RPC for Eclipse eclipsemainnet: false, endurance: true, ethereum: true, @@ -328,8 +328,7 @@ export const hyperlaneContextAgentChainConfig: AgentChainConfig< sei: true, shibarium: true, snaxchain: true, - // Cannot scrape Sealevel chains - solanamainnet: false, + solanamainnet: true, stride: true, superseed: true, superpositionmainnet: true, diff --git a/typescript/infra/config/environments/mainnet3/chains.ts b/typescript/infra/config/environments/mainnet3/chains.ts index 66d12a8937..f0d7f66400 100644 --- a/typescript/infra/config/environments/mainnet3/chains.ts +++ b/typescript/infra/config/environments/mainnet3/chains.ts @@ -65,6 +65,16 @@ export const chainMetadataOverrides: ChainMap> = { // maxFeePerGas: 100000 * 10 ** 9, // 100,000 gwei // }, // }, + // taiko: { + // transactionOverrides: { + // gasPrice: 25 * 10 ** 7, // 0.25 gwei + // }, + // }, + // linea: { + // transactionOverrides: { + // gasPrice: 5 * 10 ** 8, // 0.5 gwei + // }, + // }, // zircuit: { // blocks: { // confirmations: 3, diff --git a/typescript/infra/config/environments/mainnet3/gasPrices.json b/typescript/infra/config/environments/mainnet3/gasPrices.json index 1009f5eee1..1c5f05b28a 100644 --- a/typescript/infra/config/environments/mainnet3/gasPrices.json +++ b/typescript/infra/config/environments/mainnet3/gasPrices.json @@ -28,7 +28,7 @@ "decimals": 9 }, "astarzkevm": { - "amount": "0.24", + "amount": "0.0988", "decimals": 9 }, "flame": { @@ -36,11 +36,11 @@ "decimals": 9 }, "avalanche": { - "amount": "25.0", + "amount": "27.735398516", "decimals": 9 }, "b3": { - "amount": "0.001000252", + "amount": "0.001000253", "decimals": 9 }, "base": { @@ -60,7 +60,7 @@ "decimals": 9 }, "boba": { - "amount": "0.001000047", + "amount": "0.001000059", "decimals": 9 }, "bsc": { @@ -140,7 +140,7 @@ "decimals": 9 }, "gnosis": { - "amount": "1.500000008", + "amount": "1.500000007", "decimals": 9 }, "gravity": { @@ -176,11 +176,11 @@ "decimals": 9 }, "lisk": { - "amount": "0.00100103", + "amount": "0.001001147", "decimals": 9 }, "lukso": { - "amount": "0.921815267", + "amount": "1.109955713", "decimals": 9 }, "lumia": { @@ -192,7 +192,7 @@ "decimals": 9 }, "mantapacific": { - "amount": "0.00300029", + "amount": "0.003000983", "decimals": 9 }, "mantle": { @@ -216,7 +216,7 @@ "decimals": 9 }, "mode": { - "amount": "0.001000252", + "amount": "0.001001363", "decimals": 9 }, "molten": { @@ -240,7 +240,7 @@ "decimals": 9 }, "optimism": { - "amount": "0.001065045", + "amount": "0.001000469", "decimals": 9 }, "orderly": { @@ -264,7 +264,7 @@ "decimals": 9 }, "prom": { - "amount": "546.0", + "amount": "51.9", "decimals": 9 }, "proofofplay": { @@ -276,7 +276,7 @@ "decimals": 9 }, "real": { - "amount": "0.04", + "amount": "0.022", "decimals": 9 }, "redstone": { @@ -296,7 +296,7 @@ "decimals": 9 }, "sei": { - "amount": "100.0", + "amount": "3.328028877", "decimals": 9 }, "shibarium": { @@ -336,7 +336,7 @@ "decimals": 9 }, "treasure": { - "amount": "10000.0", + "amount": "702.999550885", "decimals": 9 }, "unichain": { @@ -344,7 +344,7 @@ "decimals": 9 }, "vana": { - "amount": "0.002488334", + "amount": "0.002986", "decimals": 9 }, "viction": { @@ -352,7 +352,7 @@ "decimals": 9 }, "worldchain": { - "amount": "0.00100026", + "amount": "0.001000255", "decimals": 9 }, "xai": { diff --git a/typescript/infra/config/environments/mainnet3/owners.ts b/typescript/infra/config/environments/mainnet3/owners.ts index f37ac77af8..5b495d316f 100644 --- a/typescript/infra/config/environments/mainnet3/owners.ts +++ b/typescript/infra/config/environments/mainnet3/owners.ts @@ -56,6 +56,7 @@ export const safes: ChainMap
= { endurance: '0xaCD1865B262C89Fb0b50dcc8fB095330ae8F35b5', zircuit: '0x9e2fe7723b018d02cDE4f5cC1A9bC9C65b922Fc8', zeronetwork: '0xCB21F61A3c8139F18e635d45aD1e62A4A61d2c3D', + swell: '0x5F7771EA40546e2932754C263455Cb0023a55ca7', }; export const icaOwnerChain = 'ethereum'; @@ -166,6 +167,12 @@ export const icas: Partial< vana: '0x29dfa34765e29ea353FC8aB70A19e32a5578E603', bsquared: '0xd9564EaaA68A327933f758A54450D3A0531E60BB', superseed: '0x29dfa34765e29ea353FC8aB70A19e32a5578E603', + + // Dec 4, 2024 batch + // ---------------------------------------------------------- + // swell: '0xff8326468e7AaB51c53D3569cf7C45Dd54c11687', // already has a safe + lumiaprism: '0xAFfA863646D1bC74ecEC0dB1070f069Af065EBf5', + appchain: '0x4F25DFFd10A6D61C365E1a605d07B2ab0E82A7E6', } as const; export const DEPLOYER = '0xa7ECcdb9Be08178f896c26b7BbD8C3D4E844d9Ba'; diff --git a/typescript/infra/config/environments/mainnet3/tokenPrices.json b/typescript/infra/config/environments/mainnet3/tokenPrices.json index e340241632..9567a31775 100644 --- a/typescript/infra/config/environments/mainnet3/tokenPrices.json +++ b/typescript/infra/config/environments/mainnet3/tokenPrices.json @@ -1,99 +1,99 @@ { - "ancient8": "3628.85", - "alephzeroevmmainnet": "0.61841", - "apechain": "1.52", - "appchain": "3628.85", - "arbitrum": "3628.85", - "arbitrumnova": "3628.85", - "astar": "0.078025", - "astarzkevm": "3628.85", - "flame": "7.36", - "avalanche": "48.31", - "b3": "3628.85", - "base": "3628.85", - "bitlayer": "95794", - "blast": "3628.85", - "bob": "3628.85", - "boba": "3628.85", - "bsc": "639.49", - "bsquared": "95794", - "celo": "0.983257", - "cheesechain": "0.00180324", - "chilizmainnet": "0.104233", - "coredao": "1.43", - "cyber": "3628.85", - "degenchain": "0.01711559", - "dogechain": "0.421781", - "duckchain": "6.5", - "eclipsemainnet": "3628.85", - "endurance": "3.09", - "ethereum": "3628.85", - "everclear": "3628.85", - "fantom": "1.034", - "flare": "0.03388989", - "flowmainnet": "1.01", - "fraxtal": "3614.4", - "fusemainnet": "0.03486937", - "gnosis": "0.997956", - "gravity": "0.03730451", - "harmony": "0.02834153", - "immutablezkevmmainnet": "1.94", - "inevm": "30.08", - "injective": "30.08", - "kaia": "0.357047", - "kroma": "3628.85", - "linea": "3628.85", - "lisk": "3628.85", - "lukso": "3.33", - "lumia": "1.7", - "lumiaprism": "1.7", - "mantapacific": "3628.85", - "mantle": "0.888186", - "merlin": "95787", - "metal": "3628.85", - "metis": "60.01", - "mint": "3628.85", - "mode": "3628.85", - "molten": "0.284308", - "moonbeam": "0.313413", - "morph": "3628.85", - "neutron": "0.523303", - "oortmainnet": "0.255252", - "optimism": "3628.85", - "orderly": "3628.85", - "osmosis": "0.589662", - "polygon": "0.621246", - "polygonzkevm": "3628.85", - "polynomialfi": "3628.85", - "prom": "6.5", - "proofofplay": "3628.85", - "rarichain": "3628.85", + "ancient8": "3849.95", + "alephzeroevmmainnet": "0.563568", + "apechain": "1.66", + "appchain": "3849.95", + "arbitrum": "3849.95", + "arbitrumnova": "3849.95", + "astar": "0.078825", + "astarzkevm": "3849.95", + "flame": "7.62", + "avalanche": "49.4", + "b3": "3849.95", + "base": "3849.95", + "bitlayer": "98047", + "blast": "3849.95", + "bob": "3849.95", + "boba": "3849.95", + "bsc": "714.94", + "bsquared": "98047", + "celo": "0.916567", + "cheesechain": "0.0015485", + "chilizmainnet": "0.119182", + "coredao": "1.42", + "cyber": "3849.95", + "degenchain": "0.01880045", + "dogechain": "0.429424", + "duckchain": "6.4", + "eclipsemainnet": "3849.95", + "endurance": "3.21", + "ethereum": "3849.95", + "everclear": "3849.95", + "fantom": "1.23", + "flare": "0.02912373", + "flowmainnet": "1.048", + "fraxtal": "3847.8", + "fusemainnet": "0.04124996", + "gnosis": "1.001", + "gravity": "0.03951512", + "harmony": "0.03939191", + "immutablezkevmmainnet": "1.89", + "inevm": "30.43", + "injective": "30.43", + "kaia": "0.282109", + "kroma": "3849.95", + "linea": "3849.95", + "lisk": "3849.95", + "lukso": "2.81", + "lumia": "2.25", + "lumiaprism": "2.25", + "mantapacific": "3849.95", + "mantle": "1.13", + "merlin": "99320", + "metal": "3849.95", + "metis": "59.74", + "mint": "3849.95", + "mode": "3849.95", + "molten": "0.382952", + "moonbeam": "0.345164", + "morph": "3849.95", + "neutron": "0.571583", + "oortmainnet": "0.22645", + "optimism": "3849.95", + "orderly": "3849.95", + "osmosis": "0.699208", + "polygon": "0.633271", + "polygonzkevm": "3849.95", + "polynomialfi": "3849.95", + "prom": "7.16", + "proofofplay": "3849.95", + "rarichain": "3849.95", "real": "1", - "redstone": "3628.85", - "rootstockmainnet": "95652", - "sanko": "53.86", - "scroll": "3628.85", - "sei": "0.613723", - "shibarium": "0.59728", - "snaxchain": "3628.85", - "solanamainnet": "223.96", - "stride": "0.675504", - "superseed": "3628.85", - "superpositionmainnet": "3628.85", - "swell": "3628.85", - "taiko": "3628.85", + "redstone": "3849.95", + "rootstockmainnet": "98004", + "sanko": "58.27", + "scroll": "3849.95", + "sei": "0.625869", + "shibarium": "0.670964", + "snaxchain": "3849.95", + "solanamainnet": "226.39", + "stride": "0.779753", + "superseed": "3849.95", + "superpositionmainnet": "3849.95", + "swell": "3849.95", + "taiko": "3849.95", "tangle": "1", - "treasure": "0.64326", - "unichain": "3628.85", + "treasure": "0.638598", + "unichain": "3849.95", "vana": "1", - "viction": "0.479042", - "worldchain": "3628.85", - "xai": "0.37142", - "xlayer": "53.81", - "zeronetwork": "3628.85", - "zetachain": "0.819612", - "zircuit": "3628.85", - "zklink": "3628.85", - "zksync": "3628.85", - "zoramainnet": "3628.85" + "viction": "0.50166", + "worldchain": "3849.95", + "xai": "0.368066", + "xlayer": "56.38", + "zeronetwork": "3849.95", + "zetachain": "0.805386", + "zircuit": "3849.95", + "zklink": "3849.95", + "zksync": "3849.95", + "zoramainnet": "3849.95" } diff --git a/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumVictionETHWarpConfig.ts b/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumVictionETHWarpConfig.ts index 4617aec50e..f058edb64b 100644 --- a/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumVictionETHWarpConfig.ts +++ b/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumVictionETHWarpConfig.ts @@ -1,10 +1,10 @@ +import { ethers } from 'ethers'; + import { ChainMap, HypTokenRouterConfig, OwnableConfig, TokenType, - buildAggregationIsmConfigs, - defaultMultisigConfigs, } from '@hyperlane-xyz/sdk'; import { RouterConfigWithoutOwner } from '../../../../../src/config/warp.js'; @@ -13,12 +13,6 @@ export const getEthereumVictionETHWarpConfig = async ( routerConfig: ChainMap, abacusWorksEnvOwnerConfig: ChainMap, ): Promise> => { - const ismConfig = buildAggregationIsmConfigs( - 'ethereum', - ['viction'], - defaultMultisigConfigs, - ).viction; - const viction: HypTokenRouterConfig = { ...routerConfig.viction, ...abacusWorksEnvOwnerConfig.viction, @@ -28,6 +22,7 @@ export const getEthereumVictionETHWarpConfig = async ( decimals: 18, totalSupply: 0, gas: 50_000, + interchainSecurityModule: ethers.constants.AddressZero, }; const ethereum: HypTokenRouterConfig = { @@ -35,7 +30,7 @@ export const getEthereumVictionETHWarpConfig = async ( ...abacusWorksEnvOwnerConfig.ethereum, type: TokenType.native, gas: 65_000, - interchainSecurityModule: ismConfig, + interchainSecurityModule: ethers.constants.AddressZero, }; return { diff --git a/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumVictionUSDCWarpConfig.ts b/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumVictionUSDCWarpConfig.ts index 8ea4026b82..7bcf283573 100644 --- a/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumVictionUSDCWarpConfig.ts +++ b/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumVictionUSDCWarpConfig.ts @@ -1,10 +1,10 @@ +import { ethers } from 'ethers'; + import { ChainMap, HypTokenRouterConfig, OwnableConfig, TokenType, - buildAggregationIsmConfigs, - defaultMultisigConfigs, } from '@hyperlane-xyz/sdk'; import { @@ -16,13 +16,6 @@ export const getEthereumVictionUSDCWarpConfig = async ( routerConfig: ChainMap, abacusWorksEnvOwnerConfig: ChainMap, ): Promise> => { - // commit that the config was copied from https://github.com/hyperlane-xyz/hyperlane-monorepo/pull/3067/commits/7ed5b460034ea5e140c6ff86bcd6baf6ebb824c4#diff-fab5dd1a27c76e4310699c57ccf92ab6274ef0acf17e079b17270cedf4057775R109 - const ismConfig = buildAggregationIsmConfigs( - 'ethereum', - ['viction'], - defaultMultisigConfigs, - ).viction; - const viction: HypTokenRouterConfig = { ...routerConfig.viction, ...abacusWorksEnvOwnerConfig.viction, @@ -32,6 +25,7 @@ export const getEthereumVictionUSDCWarpConfig = async ( decimals: 6, totalSupply: 0, gas: 75_000, + interchainSecurityModule: ethers.constants.AddressZero, }; const ethereum: HypTokenRouterConfig = { @@ -40,7 +34,7 @@ export const getEthereumVictionUSDCWarpConfig = async ( type: TokenType.collateral, token: tokens.ethereum.USDC, gas: 65_000, - interchainSecurityModule: ismConfig, + interchainSecurityModule: ethers.constants.AddressZero, }; return { diff --git a/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumVictionUSDTWarpConfig.ts b/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumVictionUSDTWarpConfig.ts index 3b98d5debc..3001880aa3 100644 --- a/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumVictionUSDTWarpConfig.ts +++ b/typescript/infra/config/environments/mainnet3/warp/configGetters/getEthereumVictionUSDTWarpConfig.ts @@ -1,10 +1,10 @@ +import { ethers } from 'ethers'; + import { ChainMap, HypTokenRouterConfig, OwnableConfig, TokenType, - buildAggregationIsmConfigs, - defaultMultisigConfigs, } from '@hyperlane-xyz/sdk'; import { @@ -16,12 +16,6 @@ export const getEthereumVictionUSDTWarpConfig = async ( routerConfig: ChainMap, abacusWorksEnvOwnerConfig: ChainMap, ): Promise> => { - const ismConfig = buildAggregationIsmConfigs( - 'ethereum', - ['viction'], - defaultMultisigConfigs, - ).viction; - const viction: HypTokenRouterConfig = { ...routerConfig.viction, ...abacusWorksEnvOwnerConfig.viction, @@ -31,6 +25,7 @@ export const getEthereumVictionUSDTWarpConfig = async ( decimals: 6, totalSupply: 0, gas: 75_000, + interchainSecurityModule: ethers.constants.AddressZero, }; const ethereum: HypTokenRouterConfig = { @@ -39,7 +34,7 @@ export const getEthereumVictionUSDTWarpConfig = async ( type: TokenType.collateral, token: tokens.ethereum.USDT, gas: 65_000, - interchainSecurityModule: ismConfig, + interchainSecurityModule: ethers.constants.AddressZero, }; return { diff --git a/typescript/infra/config/environments/testnet4/agent.ts b/typescript/infra/config/environments/testnet4/agent.ts index 4f984ebfe8..2bab1291ac 100644 --- a/typescript/infra/config/environments/testnet4/agent.ts +++ b/typescript/infra/config/environments/testnet4/agent.ts @@ -121,7 +121,6 @@ export const hyperlaneContextAgentChainConfig: AgentChainConfig< citreatestnet: true, connextsepolia: false, ecotestnet: true, - // Cannot scrape non-EVM chains eclipsetestnet: false, formtestnet: true, fuji: true, @@ -135,7 +134,6 @@ export const hyperlaneContextAgentChainConfig: AgentChainConfig< polygonamoy: true, scrollsepolia: true, sepolia: true, - // Cannot scrape non-EVM chains solanatestnet: false, soneiumtestnet: true, sonictestnet: true, diff --git a/typescript/infra/scripts/check/check-utils.ts b/typescript/infra/scripts/check/check-utils.ts index a8289671a0..ef7a2b1215 100644 --- a/typescript/infra/scripts/check/check-utils.ts +++ b/typescript/infra/scripts/check/check-utils.ts @@ -30,6 +30,7 @@ import { DeployEnvironment } from '../../src/config/environment.js'; import { HyperlaneAppGovernor } from '../../src/govern/HyperlaneAppGovernor.js'; import { HyperlaneCoreGovernor } from '../../src/govern/HyperlaneCoreGovernor.js'; import { HyperlaneHaasGovernor } from '../../src/govern/HyperlaneHaasGovernor.js'; +import { HyperlaneICAChecker } from '../../src/govern/HyperlaneICAChecker.js'; import { HyperlaneIgpGovernor } from '../../src/govern/HyperlaneIgpGovernor.js'; import { ProxiedRouterGovernor } from '../../src/govern/ProxiedRouterGovernor.js'; import { Role } from '../../src/roles.js'; @@ -148,7 +149,7 @@ export async function getGovernor( governor = new ProxiedRouterGovernor(checker); } else if (module === Modules.HAAS) { chainsToSkip.forEach((chain) => delete routerConfig[chain]); - const icaChecker = new InterchainAccountChecker( + const icaChecker = new HyperlaneICAChecker( multiProvider, ica, objFilter( diff --git a/typescript/infra/scripts/safes/parse-txs.ts b/typescript/infra/scripts/safes/parse-txs.ts index ac897cd279..21e8f87eed 100644 --- a/typescript/infra/scripts/safes/parse-txs.ts +++ b/typescript/infra/scripts/safes/parse-txs.ts @@ -1,7 +1,12 @@ import { BigNumber } from 'ethers'; import { AnnotatedEV5Transaction } from '@hyperlane-xyz/sdk'; -import { stringifyObject } from '@hyperlane-xyz/utils'; +import { + LogFormat, + LogLevel, + configureRootLogger, + stringifyObject, +} from '@hyperlane-xyz/utils'; import { GovernTransactionReader } from '../../src/tx/govern-transaction-reader.js'; import { getSafeTx } from '../../src/utils/safe.js'; @@ -13,6 +18,8 @@ async function main() { withChainsRequired(getArgs()), ).argv; + configureRootLogger(LogFormat.Pretty, LogLevel.Info); + const config = getEnvironmentConfig(environment); const multiProvider = await config.getMultiProvider(); const { chainAddresses } = await getHyperlaneCore(environment, multiProvider); diff --git a/typescript/infra/src/govern/HyperlaneAppGovernor.ts b/typescript/infra/src/govern/HyperlaneAppGovernor.ts index a704b3d9b8..1d7b5167e3 100644 --- a/typescript/infra/src/govern/HyperlaneAppGovernor.ts +++ b/typescript/infra/src/govern/HyperlaneAppGovernor.ts @@ -486,6 +486,20 @@ export abstract class HyperlaneAppGovernor< await this.checkSubmitterBalance(chain, submitterAddress, call.value); } + // Check if the submitter is the owner of the contract + try { + const ownable = Ownable__factory.connect(call.to, signer); + const owner = await ownable.owner(); + const isOwner = eqAddress(owner, submitterAddress); + + if (!isOwner) { + return false; + } + } catch { + // If the contract does not implement Ownable, just continue + // with the next check. + } + // Check if the transaction has additional success criteria if ( additionalTxSuccessCriteria && diff --git a/typescript/infra/src/govern/HyperlaneHaasGovernor.ts b/typescript/infra/src/govern/HyperlaneHaasGovernor.ts index 921649c5b9..6fa6ebcd18 100644 --- a/typescript/infra/src/govern/HyperlaneHaasGovernor.ts +++ b/typescript/infra/src/govern/HyperlaneHaasGovernor.ts @@ -5,7 +5,6 @@ import { HyperlaneCore, HyperlaneCoreChecker, InterchainAccount, - InterchainAccountChecker, } from '@hyperlane-xyz/sdk'; import { @@ -13,6 +12,7 @@ import { HyperlaneAppGovernor, } from './HyperlaneAppGovernor.js'; import { HyperlaneCoreGovernor } from './HyperlaneCoreGovernor.js'; +import { HyperlaneICAChecker } from './HyperlaneICAChecker.js'; import { ProxiedRouterGovernor } from './ProxiedRouterGovernor.js'; export class HyperlaneHaasGovernor extends HyperlaneAppGovernor< @@ -24,7 +24,7 @@ export class HyperlaneHaasGovernor extends HyperlaneAppGovernor< constructor( ica: InterchainAccount, - private readonly icaChecker: InterchainAccountChecker, + private readonly icaChecker: HyperlaneICAChecker, private readonly coreChecker: HyperlaneCoreChecker, ) { super(coreChecker, ica); diff --git a/typescript/infra/src/govern/HyperlaneICAChecker.ts b/typescript/infra/src/govern/HyperlaneICAChecker.ts new file mode 100644 index 0000000000..cb72bd73a6 --- /dev/null +++ b/typescript/infra/src/govern/HyperlaneICAChecker.ts @@ -0,0 +1,63 @@ +import { + ChainMap, + ChainName, + InterchainAccountChecker, + RouterViolation, + RouterViolationType, +} from '@hyperlane-xyz/sdk'; +import { AddressBytes32, addressToBytes32 } from '@hyperlane-xyz/utils'; + +export class HyperlaneICAChecker extends InterchainAccountChecker { + /* + * Check that the Ethereum router is enrolled correctly, + * and that remote chains have the correct router enrolled. + */ + async checkEthRouterEnrollment(chain: ChainName): Promise { + // If the chain is Ethereum, do the regular full check + if (chain === 'ethereum') { + return super.checkEnrolledRouters(chain); + } + + // Get the Ethereum router address and domain id + const ethereumRouterAddress = this.app.routerAddress('ethereum'); + const ethereumDomainId = this.multiProvider.getDomainId('ethereum'); + // Get the expected Ethereum router address (with padding) + const expectedRouter = addressToBytes32(ethereumRouterAddress); + + // Get the actual Ethereum router address + const router = this.app.router(this.app.getContracts(chain)); + const actualRouter = await router.routers(ethereumDomainId); + + // Check if the actual router address matches the expected router address + if (actualRouter !== expectedRouter) { + const currentRouters: ChainMap = { ethereum: actualRouter }; + const expectedRouters: ChainMap = { + ethereum: expectedRouter, + }; + const routerDiff: ChainMap<{ + actual: AddressBytes32; + expected: AddressBytes32; + }> = { + ethereum: { actual: actualRouter, expected: expectedRouter }, + }; + + const violation: RouterViolation = { + chain, + type: RouterViolationType.EnrolledRouter, + contract: router, + actual: currentRouters, + expected: expectedRouters, + routerDiff, + description: `Ethereum router is not enrolled correctly`, + }; + this.addViolation(violation); + } + } + + async checkChain(chain: ChainName): Promise { + await this.checkMailboxClient(chain); + await this.checkEthRouterEnrollment(chain); + await this.checkProxiedContracts(chain); + await this.checkOwnership(chain); + } +} diff --git a/typescript/infra/src/tx/govern-transaction-reader.ts b/typescript/infra/src/tx/govern-transaction-reader.ts index fadde0f87e..ef82ab1e1a 100644 --- a/typescript/infra/src/tx/govern-transaction-reader.ts +++ b/typescript/infra/src/tx/govern-transaction-reader.ts @@ -8,7 +8,7 @@ import assert from 'assert'; import chalk from 'chalk'; import { BigNumber, ethers } from 'ethers'; -import { TokenRouter__factory } from '@hyperlane-xyz/core'; +import { ProxyAdmin__factory, TokenRouter__factory } from '@hyperlane-xyz/core'; import { AnnotatedEV5Transaction, ChainMap, @@ -123,6 +123,11 @@ export class GovernTransactionReader { return this.readMailboxTransaction(chain, tx); } + // If it's to a Proxy Admin + if (this.isProxyAdminTransaction(chain, tx)) { + return this.readProxyAdminTransaction(chain, tx); + } + // If it's a Multisend if (await this.isMultisendTransaction(chain, tx)) { return this.readMultisendTransaction(chain, tx); @@ -154,6 +159,7 @@ export class GovernTransactionReader { ): boolean { return ( tx.to !== undefined && + this.warpRouteIndex[chain] !== undefined && this.warpRouteIndex[chain][tx.to.toLowerCase()] !== undefined ); } @@ -381,6 +387,43 @@ export class GovernTransactionReader { }; } + private async readProxyAdminTransaction( + chain: ChainName, + tx: AnnotatedEV5Transaction, + ): Promise { + if (!tx.data) { + throw new Error('⚠️ No data in proxyAdmin transaction'); + } + + const proxyAdminInterface = ProxyAdmin__factory.createInterface(); + const decoded = proxyAdminInterface.parseTransaction({ + data: tx.data, + value: tx.value, + }); + + const args = formatFunctionFragmentArgs( + decoded.args, + decoded.functionFragment, + ); + + let insight; + if ( + decoded.functionFragment.name === + proxyAdminInterface.functions['transferOwnership(address)'].name + ) { + const [newOwner] = decoded.args; + insight = `Transfer ownership to ${newOwner}`; + } + + return { + chain, + to: `Proxy Admin (${chain} ${this.chainAddresses[chain].proxyAdmin})`, + insight, + signature: decoded.signature, + args, + }; + } + private ismDerivationsInProgress: ChainMap = {}; private async deriveIsmConfig( @@ -600,6 +643,16 @@ export class GovernTransactionReader { ); } + isProxyAdminTransaction( + chain: ChainName, + tx: AnnotatedEV5Transaction, + ): boolean { + return ( + tx.to !== undefined && + eqAddress(tx.to, this.chainAddresses[chain].proxyAdmin) + ); + } + async isMultisendTransaction( chain: ChainName, tx: AnnotatedEV5Transaction, diff --git a/typescript/sdk/src/consts/multisigIsm.ts b/typescript/sdk/src/consts/multisigIsm.ts index eaa675b327..867059be1d 100644 --- a/typescript/sdk/src/consts/multisigIsm.ts +++ b/typescript/sdk/src/consts/multisigIsm.ts @@ -45,7 +45,7 @@ export const defaultMultisigConfigs: ChainMap = { }, alephzeroevmmainnet: { - threshold: 2, + threshold: 3, validators: [ { address: '0x33f20e6e775747d60301c6ea1c50e51f0389740c', @@ -53,6 +53,10 @@ export const defaultMultisigConfigs: ChainMap = { }, DEFAULT_MERKLY_VALIDATOR, DEFAULT_MITOSIS_VALIDATOR, + { + address: '0xCbf382214825F8c2f347dd4f23F0aDFaFad55dAa', + alias: 'Aleph Zero', + }, ], }, @@ -115,12 +119,14 @@ export const defaultMultisigConfigs: ChainMap = { }, appchain: { - threshold: 1, + threshold: 2, validators: [ { address: '0x0531251bbadc1f9f19ccce3ca6b3f79f08eae1be', alias: AW_VALIDATOR_ALIAS, }, + DEFAULT_MERKLY_VALIDATOR, + DEFAULT_MITOSIS_VALIDATOR, ], }, @@ -888,6 +894,10 @@ export const defaultMultisigConfigs: ChainMap = { address: '0xf0da628f3fb71652d48260bad4691054045832ce', alias: 'Luganodes', }, + { + address: '0xead4141b6ea149901ce4f4b556953f66d04b1d0c', + alias: 'Lisk', + }, ], }, @@ -919,12 +929,14 @@ export const defaultMultisigConfigs: ChainMap = { }, lumiaprism: { - threshold: 1, + threshold: 2, validators: [ { address: '0xb69731640ffd4338a2c9358a935b0274c6463f85', alias: AW_VALIDATOR_ALIAS, }, + DEFAULT_MERKLY_VALIDATOR, + DEFAULT_MITOSIS_VALIDATOR, ], }, @@ -1583,12 +1595,18 @@ export const defaultMultisigConfigs: ChainMap = { }, swell: { - threshold: 1, + threshold: 3, validators: [ { address: '0x4f51e4f4c7fb45d82f91568480a1a2cfb69216ed', alias: AW_VALIDATOR_ALIAS, }, + { + address: '0x9eadf9217be22d9878e0e464727a2176d5c69ff8', + alias: 'Luganodes', + }, + DEFAULT_MERKLY_VALIDATOR, + DEFAULT_MITOSIS_VALIDATOR, ], }, @@ -1624,12 +1642,21 @@ export const defaultMultisigConfigs: ChainMap = { }, treasure: { - threshold: 1, + threshold: 3, validators: [ { address: '0x6ad994819185553e8baa01533f0cd2c7cadfe6cc', alias: AW_VALIDATOR_ALIAS, }, + { + address: '0x278460fa51ff448eb53ffa62951b4b8e3e8f74e3', + alias: 'P2P', + }, + { + address: '0xe92ff70bb463e2aa93426fd2ba51afc39567d426', + alias: 'Treasure', + }, + DEFAULT_MITOSIS_VALIDATOR, ], }, @@ -1666,7 +1693,7 @@ export const defaultMultisigConfigs: ChainMap = { }, vana: { - threshold: 2, + threshold: 3, validators: [ { address: '0xfdf3b0dfd4b822d10cacb15c8ae945ea269e7534', @@ -1674,6 +1701,10 @@ export const defaultMultisigConfigs: ChainMap = { }, DEFAULT_MERKLY_VALIDATOR, DEFAULT_MITOSIS_VALIDATOR, + { + address: '0xba2f4f89cae6863d8b49e4ca0208ed48ad9ac354', + alias: 'P2P', + }, ], }, @@ -1782,12 +1813,14 @@ export const defaultMultisigConfigs: ChainMap = { }, zklink: { - threshold: 1, + threshold: 2, validators: [ { address: '0x217a8cb4789fc45abf56cb6e2ca96f251a5ac181', alias: AW_VALIDATOR_ALIAS, }, + DEFAULT_MERKLY_VALIDATOR, + DEFAULT_MITOSIS_VALIDATOR, ], }, diff --git a/typescript/sdk/src/metadata/warpRouteConfig.ts b/typescript/sdk/src/metadata/warpRouteConfig.ts index b781c1887a..8c1ee346c0 100644 --- a/typescript/sdk/src/metadata/warpRouteConfig.ts +++ b/typescript/sdk/src/metadata/warpRouteConfig.ts @@ -8,7 +8,7 @@ import { ChainMap } from '../types.js'; const TokenConfigSchema = z.object({ protocolType: z.nativeEnum(ProtocolType), type: z.nativeEnum(TokenType), - hypAddress: z.string(), // HypERC20Collateral, HypERC20Synthetic, HypNativeCollateralToken address + hypAddress: z.string(), // HypERC20Collateral, HypERC20Synthetic, HypNativeToken address tokenAddress: z.string().optional(), // external token address needed for collateral type eg tokenAddress.balanceOf(hypAddress) tokenCoinGeckoId: z.string().optional(), // CoinGecko id for token name: z.string(), diff --git a/typescript/sdk/src/providers/transactions/submitter/ethersV5/types.ts b/typescript/sdk/src/providers/transactions/submitter/ethersV5/types.ts index d1e1c7a90c..bf0d29d540 100644 --- a/typescript/sdk/src/providers/transactions/submitter/ethersV5/types.ts +++ b/typescript/sdk/src/providers/transactions/submitter/ethersV5/types.ts @@ -23,6 +23,8 @@ export type EV5GnosisSafeTxBuilderProps = z.infer< export const EV5JsonRpcTxSubmitterPropsSchema = z.object({ chain: ZChainName, + userAddress: ZHash.optional(), + privateKey: ZHash.optional(), }); export type EV5JsonRpcTxSubmitterProps = z.infer< diff --git a/typescript/sdk/src/token/EvmERC20WarpModule.hardhat-test.ts b/typescript/sdk/src/token/EvmERC20WarpModule.hardhat-test.ts index d4f16db8ad..40471f8409 100644 --- a/typescript/sdk/src/token/EvmERC20WarpModule.hardhat-test.ts +++ b/typescript/sdk/src/token/EvmERC20WarpModule.hardhat-test.ts @@ -9,7 +9,7 @@ import { GasRouter, HypERC20__factory, HypERC4626Collateral__factory, - HypNativeCollateral__factory, + HypNative__factory, Mailbox, MailboxClient__factory, Mailbox__factory, @@ -229,7 +229,7 @@ describe('EvmERC20WarpHyperlaneModule', async () => { expect(tokenType).to.equal(TokenType.native); // Validate onchain token values - const nativeContract = HypNativeCollateral__factory.connect( + const nativeContract = HypNative__factory.connect( deployedTokenRoute, signer, ); diff --git a/typescript/sdk/src/token/contracts.ts b/typescript/sdk/src/token/contracts.ts index 40fc40d2d0..a9ced3cb11 100644 --- a/typescript/sdk/src/token/contracts.ts +++ b/typescript/sdk/src/token/contracts.ts @@ -11,8 +11,8 @@ import { HypERC4626OwnerCollateral__factory, HypERC4626__factory, HypFiatToken__factory, - HypNativeCollateral__factory, HypNativeScaled__factory, + HypNative__factory, HypXERC20Lockbox__factory, HypXERC20__factory, } from '@hyperlane-xyz/core'; @@ -30,7 +30,7 @@ export const hypERC20contracts = { [TokenType.XERC20Lockbox]: 'HypXERC20Lockbox', [TokenType.collateralVault]: 'HypERC4626OwnerCollateral', [TokenType.collateralVaultRebase]: 'HypERC4626Collateral', - [TokenType.native]: 'HypNativeCollateral', + [TokenType.native]: 'HypNative', [TokenType.nativeScaled]: 'HypNativeScaled', }; export type HypERC20contracts = typeof hypERC20contracts; @@ -46,7 +46,7 @@ export const hypERC20factories = { [TokenType.collateralFiat]: new HypFiatToken__factory(), [TokenType.XERC20]: new HypXERC20__factory(), [TokenType.XERC20Lockbox]: new HypXERC20Lockbox__factory(), - [TokenType.native]: new HypNativeCollateral__factory(), + [TokenType.native]: new HypNative__factory(), [TokenType.nativeScaled]: new HypNativeScaled__factory(), }; export type HypERC20Factories = typeof hypERC20factories; diff --git a/typescript/utils/src/addresses.ts b/typescript/utils/src/addresses.ts index 29a35b6b88..a244c810ba 100644 --- a/typescript/utils/src/addresses.ts +++ b/typescript/utils/src/addresses.ts @@ -1,6 +1,6 @@ import { fromBech32, normalizeBech32, toBech32 } from '@cosmjs/encoding'; import { PublicKey } from '@solana/web3.js'; -import { utils as ethersUtils } from 'ethers'; +import { Wallet, utils as ethersUtils } from 'ethers'; import { isNullish } from './typeof.js'; import { Address, HexString, ProtocolType } from './types.js'; @@ -380,3 +380,11 @@ export function ensure0x(hexstr: string) { export function strip0x(hexstr: string) { return hexstr.startsWith('0x') ? hexstr.slice(2) : hexstr; } + +export function isPrivateKeyEvm(privateKey: string): boolean { + try { + return new Wallet(privateKey).privateKey === privateKey; + } catch { + throw new Error('Provided Private Key is not EVM compatible!'); + } +} diff --git a/typescript/utils/src/index.ts b/typescript/utils/src/index.ts index 8314418631..f4bd9779cb 100644 --- a/typescript/utils/src/index.ts +++ b/typescript/utils/src/index.ts @@ -26,6 +26,7 @@ export { isValidAddressCosmos, isValidAddressEvm, isValidAddressSealevel, + isPrivateKeyEvm, isValidTransactionHash, isValidTransactionHashCosmos, isValidTransactionHashEvm, From 4f04099933a4264af04103665899c7cc2cb00999 Mon Sep 17 00:00:00 2001 From: -f Date: Fri, 13 Dec 2024 17:27:10 +0530 Subject: [PATCH 20/23] rm override if higher --- .changeset/long-llamas-fly.md | 5 ----- .../hooks/libs/StandardHookMetadata.sol | 22 ------------------- 2 files changed, 27 deletions(-) delete mode 100644 .changeset/long-llamas-fly.md diff --git a/.changeset/long-llamas-fly.md b/.changeset/long-llamas-fly.md deleted file mode 100644 index f9da56dd0f..0000000000 --- a/.changeset/long-llamas-fly.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@hyperlane-xyz/cli': patch ---- - -Suppress help on CLI failures diff --git a/solidity/contracts/hooks/libs/StandardHookMetadata.sol b/solidity/contracts/hooks/libs/StandardHookMetadata.sol index d68a3e943e..f672e9b58b 100644 --- a/solidity/contracts/hooks/libs/StandardHookMetadata.sol +++ b/solidity/contracts/hooks/libs/StandardHookMetadata.sol @@ -184,26 +184,4 @@ library StandardHookMetadata { getCustomMetadata(_metadata) ); } - - /** - * @notice Overrides the gas limit in the metadata if _gasLimit is higher than the current gas limit. - * @param _metadata encoded standard hook metadata. - * @param _gasLimit gas limit for the message. - * @return encoded standard hook metadata. - */ - function overrideGasLimit( - bytes calldata _metadata, - uint256 _gasLimit - ) internal view returns (bytes memory) { - if (gasLimit(_metadata, 0) < _gasLimit) { - return - formatMetadata( - msgValue(_metadata, 0), - _gasLimit, - refundAddress(_metadata, msg.sender), - getCustomMetadata(_metadata) - ); - } - return _metadata; - } } From 7cf2d75075f8e0bb54c529df496601cd3b4b14c0 Mon Sep 17 00:00:00 2001 From: -f Date: Fri, 13 Dec 2024 17:31:05 +0530 Subject: [PATCH 21/23] docs(changeset): Added a new router HypNativeCollateral with a unified interface for sending value hook/ism agnostic --- .changeset/pretty-keys-give.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/pretty-keys-give.md diff --git a/.changeset/pretty-keys-give.md b/.changeset/pretty-keys-give.md new file mode 100644 index 0000000000..d5c5e7300a --- /dev/null +++ b/.changeset/pretty-keys-give.md @@ -0,0 +1,5 @@ +--- +'@hyperlane-xyz/core': minor +--- + +Added a new router HypNativeCollateral with a unified interface for sending value hook/ism agnostic From b5411291ca4fbfed24613491955df2fa7f58e676 Mon Sep 17 00:00:00 2001 From: -f Date: Fri, 13 Dec 2024 17:31:37 +0530 Subject: [PATCH 22/23] revrt --- .changeset/long-llamas-fly.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/long-llamas-fly.md diff --git a/.changeset/long-llamas-fly.md b/.changeset/long-llamas-fly.md new file mode 100644 index 0000000000..f9da56dd0f --- /dev/null +++ b/.changeset/long-llamas-fly.md @@ -0,0 +1,5 @@ +--- +'@hyperlane-xyz/cli': patch +--- + +Suppress help on CLI failures From 13e79af5dddfcc7d66d7d8d1d946fd08d65990b7 Mon Sep 17 00:00:00 2001 From: Kunal Arora <55632507+aroralanuk@users.noreply.github.com> Date: Fri, 13 Dec 2024 17:32:11 +0530 Subject: [PATCH 23/23] Update long-llamas-fly.md --- .changeset/long-llamas-fly.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/long-llamas-fly.md b/.changeset/long-llamas-fly.md index f9da56dd0f..c0d6d01e9b 100644 --- a/.changeset/long-llamas-fly.md +++ b/.changeset/long-llamas-fly.md @@ -1,5 +1,5 @@ --- -'@hyperlane-xyz/cli': patch +"@hyperlane-xyz/cli": patch --- Suppress help on CLI failures