diff --git a/.changeset/green-ads-live.md b/.changeset/green-ads-live.md new file mode 100644 index 0000000000..f847f584b6 --- /dev/null +++ b/.changeset/green-ads-live.md @@ -0,0 +1,5 @@ +--- +'@hyperlane-xyz/cli': minor +--- + +Default to home directory for local registry diff --git a/.changeset/lemon-horses-swim.md b/.changeset/lemon-horses-swim.md new file mode 100644 index 0000000000..fe5b1c9538 --- /dev/null +++ b/.changeset/lemon-horses-swim.md @@ -0,0 +1,5 @@ +--- +'@hyperlane-xyz/cli': patch +--- + +Improve defaults in chain config command diff --git a/.changeset/sour-bats-sort.md b/.changeset/sour-bats-sort.md new file mode 100644 index 0000000000..37d54bb108 --- /dev/null +++ b/.changeset/sour-bats-sort.md @@ -0,0 +1,6 @@ +--- +'@hyperlane-xyz/utils': minor +'@hyperlane-xyz/sdk': minor +--- + +Implement aggregation and multisig ISM metadata encoding diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 46da47483c..4a4727d03c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -346,9 +346,6 @@ jobs: - environment: testnet4 chain: sepolia module: core - - environment: mainnet3 - chain: inevm - module: warp steps: - uses: actions/checkout@v3 diff --git a/.gitmodules b/.gitmodules index 3077b9e20f..d5392fba8e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "solidity/lib/forge-std"] path = solidity/lib/forge-std url = https://github.com/foundry-rs/forge-std +[submodule "solidity/lib/fx-portal"] + path = solidity/lib/fx-portal + url = https://github.com/0xPolygon/fx-portal diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000000..9a2a0e219c --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v20 diff --git a/package.json b/package.json index 344b1f87d1..3da986a519 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "prettier": "yarn workspaces foreach --since --parallel run prettier", "lint": "yarn workspaces foreach --all --parallel run lint", "test": "yarn workspaces foreach --all --parallel run test", - "test:ci": "yarn workspaces foreach --all --parallel run test:ci", + "test:ci": "yarn workspaces foreach --all --topological run test:ci", "coverage": "yarn workspaces foreach --all --parallel run coverage", "version:prepare": "yarn changeset version && yarn workspaces foreach --all --parallel run version:update && yarn install --no-immutable", "version:check": "yarn changeset status", diff --git a/solidity/.gitignore b/solidity/.gitignore index c3e1139887..6f01b8e770 100644 --- a/solidity/.gitignore +++ b/solidity/.gitignore @@ -14,3 +14,4 @@ forge-cache docs flattened/ buildArtifact.json +fixtures/ diff --git a/solidity/README.md b/solidity/README.md index f6af38b8ef..d800b4a1c4 100644 --- a/solidity/README.md +++ b/solidity/README.md @@ -6,14 +6,30 @@ Hyperlane Core contains the contracts and typechain artifacts for the Hyperlane ```bash # Install with NPM -npm install @hyperlane-xyz/utils +npm install @hyperlane-xyz/core # Or with Yarn -yarn add @hyperlane-xyz/utils +yarn add @hyperlane-xyz/core ``` Note, this package uses [ESM Modules](https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c#pure-esm-package) +## Build + +```bash +yarn build +``` + +## Test + +```bash +yarn test +``` + +### Fixtures + +Some forge tests may generate fixtures in the [fixtures](./fixtures/) directory. This allows [SDK](../typescript/sdk) tests to leverage forge fuzzing. These are git ignored and should not be committed. + ## License Apache 2.0 diff --git a/solidity/contracts/hooks/PolygonPosHook.sol b/solidity/contracts/hooks/PolygonPosHook.sol new file mode 100644 index 0000000000..6831f9a332 --- /dev/null +++ b/solidity/contracts/hooks/PolygonPosHook.sol @@ -0,0 +1,83 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +/*@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@ HYPERLANE @@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ +@@@@@@@@@ @@@@@@@@*/ + +// ============ Internal Imports ============ +import {AbstractMessageIdAuthHook} from "./libs/AbstractMessageIdAuthHook.sol"; +import {StandardHookMetadata} from "./libs/StandardHookMetadata.sol"; +import {TypeCasts} from "../libs/TypeCasts.sol"; +import {Message} from "../libs/Message.sol"; +import {IPostDispatchHook} from "../interfaces/hooks/IPostDispatchHook.sol"; + +// ============ External Imports ============ +import {FxBaseRootTunnel} from "fx-portal/contracts/tunnel/FxBaseRootTunnel.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; + +/** + * @title PolygonPosHook + * @notice Message hook to inform the PolygonPosIsm of messages published through + * the native PoS bridge. + */ +contract PolygonPosHook is AbstractMessageIdAuthHook, FxBaseRootTunnel { + using StandardHookMetadata for bytes; + + // ============ Constructor ============ + + constructor( + address _mailbox, + uint32 _destinationDomain, + bytes32 _ism, + address _cpManager, + address _fxRoot + ) + AbstractMessageIdAuthHook(_mailbox, _destinationDomain, _ism) + FxBaseRootTunnel(_cpManager, _fxRoot) + { + require( + Address.isContract(_cpManager), + "PolygonPosHook: invalid cpManager contract" + ); + require( + Address.isContract(_fxRoot), + "PolygonPosHook: invalid fxRoot contract" + ); + } + + // ============ Internal functions ============ + function _quoteDispatch( + bytes calldata, + bytes calldata + ) internal pure override returns (uint256) { + return 0; + } + + /// @inheritdoc AbstractMessageIdAuthHook + function _sendMessageId( + bytes calldata metadata, + bytes memory payload + ) internal override { + require( + metadata.msgValue(0) == 0, + "PolygonPosHook: does not support msgValue" + ); + require(msg.value == 0, "PolygonPosHook: does not support msgValue"); + _sendMessageToChild(payload); + } + + bytes public latestData; + + function _processMessageFromChild(bytes memory data) internal override { + latestData = data; + } +} diff --git a/solidity/contracts/isms/hook/PolygonPosIsm.sol b/solidity/contracts/isms/hook/PolygonPosIsm.sol new file mode 100644 index 0000000000..34a40360ee --- /dev/null +++ b/solidity/contracts/isms/hook/PolygonPosIsm.sol @@ -0,0 +1,58 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity >=0.8.0; + +/*@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@ HYPERLANE @@@@@@@ + @@@@@@@@@@@@@@@@@@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ + @@@@@@@@@ @@@@@@@@@ +@@@@@@@@@ @@@@@@@@*/ + +// ============ Internal Imports ============ + +import {IInterchainSecurityModule} from "../../interfaces/IInterchainSecurityModule.sol"; +import {Message} from "../../libs/Message.sol"; +import {TypeCasts} from "../../libs/TypeCasts.sol"; +import {AbstractMessageIdAuthorizedIsm} from "./AbstractMessageIdAuthorizedIsm.sol"; + +// ============ External Imports ============ +import {CrossChainEnabledPolygonChild} from "@openzeppelin/contracts/crosschain/polygon/CrossChainEnabledPolygonChild.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; + +/** + * @title PolygonPosIsm + * @notice Uses the native Polygon Pos Fx Portal Bridge to verify interchain messages. + */ +contract PolygonPosIsm is + CrossChainEnabledPolygonChild, + AbstractMessageIdAuthorizedIsm +{ + // ============ Constants ============ + + uint8 public constant moduleType = + uint8(IInterchainSecurityModule.Types.NULL); + + // ============ Constructor ============ + + constructor(address _fxChild) CrossChainEnabledPolygonChild(_fxChild) { + require( + Address.isContract(_fxChild), + "PolygonPosIsm: invalid FxChild contract" + ); + } + + // ============ Internal function ============ + + /** + * @notice Check if sender is authorized to message `verifyMessageId`. + */ + function _isAuthorized() internal view override returns (bool) { + return + _crossChainSender() == TypeCasts.bytes32ToAddress(authorizedHook); + } +} diff --git a/solidity/foundry.toml b/solidity/foundry.toml index 33127dbd58..8180d9b58f 100644 --- a/solidity/foundry.toml +++ b/solidity/foundry.toml @@ -10,7 +10,11 @@ solc_version = '0.8.22' evm_version= 'paris' optimizer = true optimizer_runs = 999_999 -fs_permissions = [{ access = "read-write", path = "./"}] +fs_permissions = [ + { access = "read", path = "./script/avs/"}, + { access = "write", path = "./fixtures" } +] +ignored_warnings_from = ['fx-portal'] [profile.ci] verbosity = 4 @@ -18,8 +22,8 @@ verbosity = 4 [rpc_endpoints] mainnet = "https://eth.merkle.io" optimism = "https://mainnet.optimism.io " - +polygon = "https://rpc.ankr.com/polygon" [fuzz] runs = 50 -dictionary_weight = 80 \ No newline at end of file +dictionary_weight = 80 diff --git a/solidity/hardhat.config.cts b/solidity/hardhat.config.cts index 4505567e3f..6d64d971e6 100644 --- a/solidity/hardhat.config.cts +++ b/solidity/hardhat.config.cts @@ -2,6 +2,7 @@ import '@nomiclabs/hardhat-ethers'; import '@nomiclabs/hardhat-waffle'; import '@typechain/hardhat'; import 'hardhat-gas-reporter'; +import 'hardhat-ignore-warnings'; import 'solidity-coverage'; /** @@ -30,4 +31,10 @@ module.exports = { bail: true, import: 'tsx', }, + warnings: { + // turn off all warnings for libs: + 'fx-portal/**/*': { + default: 'off', + }, + }, }; diff --git a/solidity/lib/fx-portal b/solidity/lib/fx-portal new file mode 160000 index 0000000000..ebd046507d --- /dev/null +++ b/solidity/lib/fx-portal @@ -0,0 +1 @@ +Subproject commit ebd046507d76cd03fa2b2559257091471a259ed7 diff --git a/solidity/package.json b/solidity/package.json index 5ebd4c07e1..96077c4339 100644 --- a/solidity/package.json +++ b/solidity/package.json @@ -7,7 +7,8 @@ "@hyperlane-xyz/utils": "3.11.1", "@layerzerolabs/lz-evm-oapp-v2": "2.0.2", "@openzeppelin/contracts": "^4.9.3", - "@openzeppelin/contracts-upgradeable": "^v4.9.3" + "@openzeppelin/contracts-upgradeable": "^v4.9.3", + "fx-portal": "^1.0.3" }, "devDependencies": { "@layerzerolabs/solidity-examples": "^1.1.0", @@ -20,6 +21,7 @@ "ethers": "^5.7.2", "hardhat": "^2.22.2", "hardhat-gas-reporter": "^1.0.9", + "hardhat-ignore-warnings": "^0.2.11", "prettier": "^2.8.8", "prettier-plugin-solidity": "^1.1.3", "solhint": "^4.5.4", @@ -59,14 +61,14 @@ "scripts": { "build": "yarn hardhat-esm compile && tsc && ./exportBuildArtifact.sh", "lint": "solhint contracts/**/*.sol", - "clean": "yarn hardhat-esm clean && rm -rf ./dist ./cache ./types ./coverage ./out ./forge-cache", + "clean": "yarn hardhat-esm clean && rm -rf ./dist ./cache ./types ./coverage ./out ./forge-cache ./fixtures", "coverage": "./coverage.sh", "docs": "forge doc", "hardhat-esm": "NODE_OPTIONS='--experimental-loader ts-node/esm/transpile-only --no-warnings=ExperimentalWarning' hardhat --config hardhat.config.cts", "prettier": "prettier --write ./contracts ./test", "test": "yarn hardhat-esm test && yarn test:forge", "test:hardhat": "yarn hardhat-esm test", - "test:forge": "forge test -vvv", + "test:forge": "mkdir -p ./fixtures/aggregation ./fixtures/multisig && forge test -vvv", "test:ci": "yarn test:hardhat && yarn test:forge --no-match-test testFork", "gas": "forge snapshot", "gas-ci": "yarn gas --check --tolerance 2 || (echo 'Manually update gas snapshot' && exit 1)", diff --git a/solidity/remappings.txt b/solidity/remappings.txt index 1492dd8d72..e85474338a 100644 --- a/solidity/remappings.txt +++ b/solidity/remappings.txt @@ -3,3 +3,4 @@ @eth-optimism=../node_modules/@eth-optimism ds-test/=lib/forge-std/lib/ds-test/src/ forge-std/=lib/forge-std/src/ +fx-portal/=lib/fx-portal/ \ No newline at end of file diff --git a/solidity/test/isms/AggregationIsm.t.sol b/solidity/test/isms/AggregationIsm.t.sol index 889789be0d..8806d9b66c 100644 --- a/solidity/test/isms/AggregationIsm.t.sol +++ b/solidity/test/isms/AggregationIsm.t.sol @@ -3,12 +3,19 @@ pragma solidity ^0.8.13; import "forge-std/Test.sol"; +import "@openzeppelin/contracts/utils/Strings.sol"; + import {IAggregationIsm} from "../../contracts/interfaces/isms/IAggregationIsm.sol"; import {StaticAggregationIsmFactory} from "../../contracts/isms/aggregation/StaticAggregationIsmFactory.sol"; import {AggregationIsmMetadata} from "../../contracts/isms/libs/AggregationIsmMetadata.sol"; import {TestIsm, ThresholdTestUtils} from "./IsmTestUtils.sol"; contract AggregationIsmTest is Test { + using Strings for uint256; + using Strings for uint8; + + string constant fixtureKey = "fixture"; + StaticAggregationIsmFactory factory; IAggregationIsm ism; @@ -16,6 +23,24 @@ contract AggregationIsmTest is Test { factory = new StaticAggregationIsmFactory(); } + function fixtureAppendMetadata( + uint256 index, + bytes memory metadata + ) internal { + vm.serializeBytes(fixtureKey, index.toString(), metadata); + } + + function fixtureAppendNull(uint256 index) internal { + vm.serializeString(fixtureKey, index.toString(), "null"); + } + + function writeFixture(bytes memory metadata, uint8 m) internal { + string memory path = string( + abi.encodePacked("./fixtures/aggregation/", m.toString(), ".json") + ); + vm.writeJson(vm.serializeBytes(fixtureKey, "encoded", metadata), path); + } + function deployIsms( uint8 m, uint8 n, @@ -32,34 +57,37 @@ contract AggregationIsmTest is Test { return isms; } - function getMetadata( - uint8 m, - bytes32 seed - ) private view returns (bytes memory) { + function getMetadata(uint8 m, bytes32 seed) private returns (bytes memory) { (address[] memory choices, ) = ism.modulesAndThreshold(""); address[] memory chosen = ThresholdTestUtils.choose(m, choices, seed); bytes memory offsets; uint32 start = 8 * uint32(choices.length); bytes memory metametadata; + for (uint256 i = 0; i < choices.length; i++) { bool included = false; for (uint256 j = 0; j < chosen.length; j++) { included = included || choices[i] == chosen[j]; } if (included) { - bytes memory requiredMetadata = TestIsm(choices[i]) - .requiredMetadata(); - uint32 end = start + uint32(requiredMetadata.length); + bytes memory metadata = TestIsm(choices[i]).requiredMetadata(); + uint32 end = start + uint32(metadata.length); uint64 offset = (uint64(start) << 32) | uint64(end); offsets = bytes.concat(offsets, abi.encodePacked(offset)); start = end; - metametadata = abi.encodePacked(metametadata, requiredMetadata); + metametadata = abi.encodePacked(metametadata, metadata); + fixtureAppendMetadata(i, metadata); } else { - uint64 offset = 0; - offsets = bytes.concat(offsets, abi.encodePacked(offset)); + offsets = bytes.concat(offsets, abi.encodePacked(uint64(0))); + fixtureAppendNull(i); } } - return abi.encodePacked(offsets, metametadata); + + bytes memory encoded = abi.encodePacked(offsets, metametadata); + + writeFixture(encoded, m); + + return encoded; } function testVerify(uint8 m, uint8 n, bytes32 seed) public { diff --git a/solidity/test/isms/MultisigIsm.t.sol b/solidity/test/isms/MultisigIsm.t.sol index 5c34be5f3b..6cf8c7036e 100644 --- a/solidity/test/isms/MultisigIsm.t.sol +++ b/solidity/test/isms/MultisigIsm.t.sol @@ -3,6 +3,8 @@ pragma solidity ^0.8.13; import "forge-std/Test.sol"; +import "@openzeppelin/contracts/utils/Strings.sol"; + import {IMultisigIsm} from "../../contracts/interfaces/isms/IMultisigIsm.sol"; import {TestMailbox} from "../../contracts/test/TestMailbox.sol"; import {StaticMerkleRootMultisigIsmFactory, StaticMessageIdMultisigIsmFactory} from "../../contracts/isms/multisig/StaticMultisigIsm.sol"; @@ -20,6 +22,13 @@ import {ThresholdTestUtils} from "./IsmTestUtils.sol"; abstract contract AbstractMultisigIsmTest is Test { using Message for bytes; using TypeCasts for address; + using Strings for uint256; + using Strings for uint8; + + string constant fixtureKey = "fixture"; + string constant signatureKey = "signature"; + string constant signaturesKey = "signatures"; + string constant prefixKey = "prefix"; uint32 constant ORIGIN = 11; StaticThresholdAddressSetFactory factory; @@ -30,7 +39,47 @@ abstract contract AbstractMultisigIsmTest is Test { function metadataPrefix( bytes memory message - ) internal view virtual returns (bytes memory); + ) internal virtual returns (bytes memory); + + function fixtureInit() internal { + vm.serializeUint(fixtureKey, "type", uint256(ism.moduleType())); + string memory prefix = vm.serializeString(prefixKey, "dummy", "dummy"); + vm.serializeString(fixtureKey, "prefix", prefix); + } + + function fixtureAppendSignature( + uint256 index, + uint8 v, + bytes32 r, + bytes32 s + ) internal { + vm.serializeUint(signatureKey, "v", uint256(v)); + vm.serializeBytes32(signatureKey, "r", r); + string memory signature = vm.serializeBytes32(signatureKey, "s", s); + vm.serializeString(signaturesKey, index.toString(), signature); + } + + function writeFixture(bytes memory metadata, uint8 m, uint8 n) internal { + vm.serializeString( + fixtureKey, + "signatures", + vm.serializeString(signaturesKey, "dummy", "dummy") + ); + + string memory fixturePath = string( + abi.encodePacked( + "./fixtures/multisig/", + m.toString(), + "-", + n.toString(), + ".json" + ) + ); + vm.writeJson( + vm.serializeBytes(fixtureKey, "encoded", metadata), + fixturePath + ); + } function getMetadata( uint8 m, @@ -38,24 +87,40 @@ abstract contract AbstractMultisigIsmTest is Test { bytes32 seed, bytes memory message ) internal returns (bytes memory) { - uint32 domain = mailbox.localDomain(); - uint256[] memory keys = addValidators(m, n, seed); - uint256[] memory signers = ThresholdTestUtils.choose(m, keys, seed); + bytes32 digest; + { + uint32 domain = mailbox.localDomain(); + (bytes32 root, uint32 index) = merkleTreeHook.latestCheckpoint(); + bytes32 messageId = message.id(); + bytes32 merkleTreeAddress = address(merkleTreeHook) + .addressToBytes32(); + digest = CheckpointLib.digest( + domain, + merkleTreeAddress, + root, + index, + messageId + ); + } - (bytes32 root, uint32 index) = merkleTreeHook.latestCheckpoint(); - bytes32 messageId = message.id(); - bytes32 digest = CheckpointLib.digest( - domain, - address(merkleTreeHook).addressToBytes32(), - root, - index, - messageId + uint256[] memory signers = ThresholdTestUtils.choose( + m, + addValidators(m, n, seed), + seed ); + bytes memory metadata = metadataPrefix(message); + fixtureInit(); + for (uint256 i = 0; i < m; i++) { (uint8 v, bytes32 r, bytes32 s) = vm.sign(signers[i], digest); metadata = abi.encodePacked(metadata, r, s, v); + + fixtureAppendSignature(i, v, r, s); } + + writeFixture(metadata, m, n); + return metadata; } @@ -125,6 +190,9 @@ abstract contract AbstractMultisigIsmTest is Test { contract MerkleRootMultisigIsmTest is AbstractMultisigIsmTest { using TypeCasts for address; using Message for bytes; + using Strings for uint256; + + string constant proofKey = "proof"; function setUp() public { mailbox = new TestMailbox(ORIGIN); @@ -135,17 +203,45 @@ contract MerkleRootMultisigIsmTest is AbstractMultisigIsmTest { mailbox.setRequiredHook(address(noopHook)); } + function fixturePrefix( + uint32 checkpointIndex, + bytes32 merkleTreeAddress, + bytes32 messageId, + bytes32[32] memory proof + ) internal { + vm.serializeUint(prefixKey, "index", uint256(checkpointIndex)); + vm.serializeBytes32(prefixKey, "merkleTree", merkleTreeAddress); + vm.serializeUint(prefixKey, "signedIndex", uint256(checkpointIndex)); + vm.serializeBytes32(prefixKey, "id", messageId); + + for (uint256 i = 0; i < 32; i++) { + vm.serializeBytes32(proofKey, i.toString(), proof[i]); + } + string memory proofString = vm.serializeString( + proofKey, + "dummy", + "dummy" + ); + vm.serializeString(prefixKey, "proof", proofString); + } + // TODO: test merkleIndex != signedIndex function metadataPrefix( bytes memory message - ) internal view override returns (bytes memory) { + ) internal override returns (bytes memory) { uint32 checkpointIndex = uint32(merkleTreeHook.count() - 1); + bytes32[32] memory proof = merkleTreeHook.proof(); + bytes32 messageId = message.id(); + bytes32 merkleTreeAddress = address(merkleTreeHook).addressToBytes32(); + + fixturePrefix(checkpointIndex, merkleTreeAddress, messageId, proof); + return abi.encodePacked( - address(merkleTreeHook).addressToBytes32(), + merkleTreeAddress, checkpointIndex, - message.id(), - merkleTreeHook.proof(), + messageId, + proof, checkpointIndex ); } @@ -164,15 +260,24 @@ contract MessageIdMultisigIsmTest is AbstractMultisigIsmTest { mailbox.setRequiredHook(address(noopHook)); } + function fixturePrefix( + bytes32 root, + uint32 index, + bytes32 merkleTreeAddress + ) internal { + vm.serializeBytes32(prefixKey, "root", root); + vm.serializeUint(prefixKey, "signedIndex", uint256(index)); + vm.serializeBytes32(prefixKey, "merkleTree", merkleTreeAddress); + } + function metadataPrefix( bytes memory - ) internal view override returns (bytes memory) { + ) internal override returns (bytes memory metadata) { (bytes32 root, uint32 index) = merkleTreeHook.latestCheckpoint(); - return - abi.encodePacked( - address(merkleTreeHook).addressToBytes32(), - root, - index - ); + bytes32 merkleTreeAddress = address(merkleTreeHook).addressToBytes32(); + + fixturePrefix(root, index, merkleTreeAddress); + + return abi.encodePacked(merkleTreeAddress, root, index); } } diff --git a/solidity/test/isms/PolygonPosIsm.t.sol b/solidity/test/isms/PolygonPosIsm.t.sol new file mode 100644 index 0000000000..fd26afea29 --- /dev/null +++ b/solidity/test/isms/PolygonPosIsm.t.sol @@ -0,0 +1,367 @@ +// SPDX-License-Identifier: MIT or Apache-2.0 +pragma solidity ^0.8.13; + +import {Test} from "forge-std/Test.sol"; + +import {LibBit} from "../../contracts/libs/LibBit.sol"; +import {TypeCasts} from "../../contracts/libs/TypeCasts.sol"; +import {StandardHookMetadata} from "../../contracts/hooks/libs/StandardHookMetadata.sol"; +import {StandardHookMetadata} from "../../contracts/hooks/libs/StandardHookMetadata.sol"; +import {AbstractMessageIdAuthorizedIsm} from "../../contracts/isms/hook/AbstractMessageIdAuthorizedIsm.sol"; +import {TestMailbox} from "../../contracts/test/TestMailbox.sol"; +import {Message} from "../../contracts/libs/Message.sol"; +import {MessageUtils} from "./IsmTestUtils.sol"; +import {PolygonPosIsm} from "../../contracts/isms/hook/PolygonPosIsm.sol"; +import {PolygonPosHook} from "../../contracts/hooks/PolygonPosHook.sol"; +import {TestRecipient} from "../../contracts/test/TestRecipient.sol"; + +import {NotCrossChainCall} from "@openzeppelin/contracts/crosschain/errors.sol"; + +interface IStateSender { + function counter() external view returns (uint256); +} + +interface FxChild { + function onStateReceive(uint256 stateId, bytes calldata data) external; +} + +contract PolygonPosIsmTest is Test { + using LibBit for uint256; + using TypeCasts for address; + using MessageUtils for bytes; + + uint256 internal mainnetFork; + uint256 internal polygonPosFork; + + address internal constant POLYGON_CROSSCHAIN_SYSTEM_ADDR = + 0x0000000000000000000000000000000000001001; + + address internal constant MUMBAI_FX_CHILD = + 0xCf73231F28B7331BBe3124B907840A94851f9f11; + address internal constant GOERLI_CHECKPOINT_MANAGER = + 0x2890bA17EfE978480615e330ecB65333b880928e; + address internal constant GOERLI_FX_ROOT = + 0x3d1d3E34f7fB6D26245E6640E1c50710eFFf15bA; + + address internal constant MAINNET_FX_CHILD = + 0x8397259c983751DAf40400790063935a11afa28a; + address internal constant MAINNET_CHECKPOINT_MANAGER = + 0x86E4Dc95c7FBdBf52e33D563BbDB00823894C287; + address internal constant MAINNET_FX_ROOT = + 0xfe5e5D361b2ad62c541bAb87C45a0B9B018389a2; + address internal constant MAINNET_STATE_SENDER = + 0x28e4F3a7f651294B9564800b2D01f35189A5bFbE; + + uint8 internal constant POLYGON_POS_VERSION = 0; + uint8 internal constant HYPERLANE_VERSION = 1; + + TestMailbox internal l1Mailbox; + PolygonPosIsm internal polygonPosISM; + PolygonPosHook internal polygonPosHook; + FxChild internal fxChild; + + TestRecipient internal testRecipient; + bytes internal testMessage = + abi.encodePacked("Hello from the other chain!"); + bytes internal testMetadata = + StandardHookMetadata.overrideRefundAddress(address(this)); + + bytes internal encodedMessage; + bytes32 internal messageId; + + uint32 internal constant MAINNET_DOMAIN = 1; + uint32 internal constant POLYGON_POS_DOMAIN = 137; + + event StateSynced( + uint256 indexed id, + address indexed contractAddress, + bytes data + ); + + event ReceivedMessage(bytes32 indexed messageId); + + function setUp() public { + // block numbers to fork from, chain data is cached to ../../forge-cache/ + mainnetFork = vm.createFork(vm.rpcUrl("mainnet"), 18_718_401); + polygonPosFork = vm.createFork(vm.rpcUrl("polygon"), 50_760_479); + + testRecipient = new TestRecipient(); + + encodedMessage = _encodeTestMessage(); + messageId = Message.id(encodedMessage); + } + + /////////////////////////////////////////////////////////////////// + /// SETUP /// + /////////////////////////////////////////////////////////////////// + + function deployPolygonPosHook() public { + vm.selectFork(mainnetFork); + + l1Mailbox = new TestMailbox(MAINNET_DOMAIN); + + polygonPosHook = new PolygonPosHook( + address(l1Mailbox), + POLYGON_POS_DOMAIN, + TypeCasts.addressToBytes32(address(polygonPosISM)), + MAINNET_CHECKPOINT_MANAGER, + MAINNET_FX_ROOT + ); + + polygonPosHook.setFxChildTunnel(address(polygonPosISM)); + + vm.makePersistent(address(polygonPosHook)); + } + + function deployPolygonPosIsm() public { + vm.selectFork(polygonPosFork); + + fxChild = FxChild(MAINNET_FX_CHILD); + polygonPosISM = new PolygonPosIsm(MAINNET_FX_CHILD); + + vm.makePersistent(address(polygonPosISM)); + } + + function deployAll() public { + deployPolygonPosIsm(); + deployPolygonPosHook(); + + vm.selectFork(polygonPosFork); + + polygonPosISM.setAuthorizedHook( + TypeCasts.addressToBytes32(address(polygonPosHook)) + ); + } + + /////////////////////////////////////////////////////////////////// + /// FORK TESTS /// + /////////////////////////////////////////////////////////////////// + + /* ============ hook.quoteDispatch ============ */ + + function testFork_quoteDispatch() public { + deployAll(); + + vm.selectFork(mainnetFork); + + assertEq(polygonPosHook.quoteDispatch(testMetadata, encodedMessage), 0); + } + + /* ============ hook.postDispatch ============ */ + + function testFork_postDispatch() public { + deployAll(); + + vm.selectFork(mainnetFork); + + bytes memory encodedHookData = abi.encodeCall( + AbstractMessageIdAuthorizedIsm.verifyMessageId, + (messageId) + ); + + l1Mailbox.updateLatestDispatchedId(messageId); + + IStateSender stateSender = IStateSender(MAINNET_STATE_SENDER); + + vm.expectEmit(true, false, false, true); + emit StateSynced( + (stateSender.counter() + 1), + MAINNET_FX_CHILD, + abi.encode( + TypeCasts.addressToBytes32(address(polygonPosHook)), + TypeCasts.addressToBytes32(address(polygonPosISM)), + encodedHookData + ) + ); + polygonPosHook.postDispatch(testMetadata, encodedMessage); + } + + function testFork_postDispatch_RevertWhen_ChainIDNotSupported() public { + deployAll(); + + vm.selectFork(mainnetFork); + + bytes memory message = MessageUtils.formatMessage( + POLYGON_POS_VERSION, + uint32(0), + MAINNET_DOMAIN, + TypeCasts.addressToBytes32(address(this)), + 11, // wrong domain + TypeCasts.addressToBytes32(address(testRecipient)), + testMessage + ); + + l1Mailbox.updateLatestDispatchedId(Message.id(message)); + vm.expectRevert( + "AbstractMessageIdAuthHook: invalid destination domain" + ); + polygonPosHook.postDispatch(testMetadata, message); + } + + function testFork_postDispatch_RevertWhen_TooMuchValue() public { + deployAll(); + + vm.selectFork(mainnetFork); + + // assign any value should revert + vm.deal(address(this), uint256(2 ** 255)); + bytes memory excessValueMetadata = StandardHookMetadata + .overrideMsgValue(uint256(2 ** 255)); + + l1Mailbox.updateLatestDispatchedId(messageId); + vm.expectRevert( + "AbstractMessageIdAuthHook: msgValue must be less than 2 ** 255" + ); + polygonPosHook.postDispatch(excessValueMetadata, encodedMessage); + } + + function testFork_postDispatch_RevertWhen_NotLastDispatchedMessage() + public + { + deployAll(); + + vm.selectFork(mainnetFork); + + vm.expectRevert( + "AbstractMessageIdAuthHook: message not latest dispatched" + ); + polygonPosHook.postDispatch(testMetadata, encodedMessage); + } + + /* ============ ISM.verifyMessageId ============ */ + + function testFork_verifyMessageId() public { + deployAll(); + + vm.selectFork(polygonPosFork); + + bytes memory encodedHookData = abi.encodeCall( + AbstractMessageIdAuthorizedIsm.verifyMessageId, + (messageId) + ); + + vm.startPrank(POLYGON_CROSSCHAIN_SYSTEM_ADDR); + + vm.expectEmit(true, false, false, false, address(polygonPosISM)); + emit ReceivedMessage(messageId); + // FIX: expect other events + + fxChild.onStateReceive( + 0, + abi.encode( + TypeCasts.addressToBytes32(address(polygonPosHook)), + TypeCasts.addressToBytes32(address(polygonPosISM)), + encodedHookData + ) + ); + + assertTrue(polygonPosISM.verifiedMessages(messageId).isBitSet(255)); + vm.stopPrank(); + } + + function testFork_verifyMessageId_RevertWhen_NotAuthorized() public { + deployAll(); + + vm.selectFork(polygonPosFork); + + // needs to be called by the fxchild on Polygon + vm.expectRevert(NotCrossChainCall.selector); + polygonPosISM.verifyMessageId(messageId); + + vm.startPrank(MAINNET_FX_CHILD); + + // needs to be called by the authorized hook contract on Ethereum + vm.expectRevert( + "AbstractMessageIdAuthorizedIsm: sender is not the hook" + ); + polygonPosISM.verifyMessageId(messageId); + } + + /* ============ ISM.verify ============ */ + + function testFork_verify() public { + deployAll(); + + vm.selectFork(polygonPosFork); + + orchestrateRelayMessage(messageId); + + bool verified = polygonPosISM.verify(new bytes(0), encodedMessage); + assertTrue(verified); + } + + // sending over invalid message + function testFork_verify_RevertWhen_HyperlaneInvalidMessage() public { + deployAll(); + + orchestrateRelayMessage(messageId); + + bytes memory invalidMessage = MessageUtils.formatMessage( + HYPERLANE_VERSION, + uint8(0), + MAINNET_DOMAIN, + TypeCasts.addressToBytes32(address(this)), + POLYGON_POS_DOMAIN, + TypeCasts.addressToBytes32(address(this)), // wrong recipient + testMessage + ); + bool verified = polygonPosISM.verify(new bytes(0), invalidMessage); + assertFalse(verified); + } + + // invalid messageID in postDispatch + function testFork_verify_RevertWhen_InvalidPolygonPosMessageID() public { + deployAll(); + vm.selectFork(polygonPosFork); + + bytes memory invalidMessage = MessageUtils.formatMessage( + HYPERLANE_VERSION, + uint8(0), + MAINNET_DOMAIN, + TypeCasts.addressToBytes32(address(this)), + POLYGON_POS_DOMAIN, + TypeCasts.addressToBytes32(address(this)), + testMessage + ); + bytes32 _messageId = Message.id(invalidMessage); + orchestrateRelayMessage(_messageId); + + bool verified = polygonPosISM.verify(new bytes(0), encodedMessage); + assertFalse(verified); + } + + /* ============ helper functions ============ */ + + function _encodeTestMessage() internal view returns (bytes memory) { + return + MessageUtils.formatMessage( + HYPERLANE_VERSION, + uint32(0), + MAINNET_DOMAIN, + TypeCasts.addressToBytes32(address(this)), + POLYGON_POS_DOMAIN, + TypeCasts.addressToBytes32(address(testRecipient)), + testMessage + ); + } + + function orchestrateRelayMessage(bytes32 _messageId) internal { + vm.selectFork(polygonPosFork); + + bytes memory encodedHookData = abi.encodeCall( + AbstractMessageIdAuthorizedIsm.verifyMessageId, + (_messageId) + ); + + vm.prank(POLYGON_CROSSCHAIN_SYSTEM_ADDR); + + fxChild.onStateReceive( + 0, + abi.encode( + TypeCasts.addressToBytes32(address(polygonPosHook)), + TypeCasts.addressToBytes32(address(polygonPosISM)), + encodedHookData + ) + ); + } +} diff --git a/typescript/cli/src/commands/options.ts b/typescript/cli/src/commands/options.ts index 1d5317656a..465abb09e0 100644 --- a/typescript/cli/src/commands/options.ts +++ b/typescript/cli/src/commands/options.ts @@ -1,3 +1,4 @@ +import os from 'os'; import { Options } from 'yargs'; import { DEFAULT_GITHUB_REGISTRY } from '@hyperlane-xyz/registry'; @@ -29,7 +30,7 @@ export const registryUriCommandOption: Options = { export const overrideRegistryUriCommandOption: Options = { type: 'string', description: 'Path to a local registry to override the default registry', - default: './', + default: `${os.homedir()}/.hyperlane`, }; export const skipConfirmationOption: Options = { diff --git a/typescript/cli/src/config/chain.ts b/typescript/cli/src/config/chain.ts index c47b8f1c7c..232205dc04 100644 --- a/typescript/cli/src/config/chain.ts +++ b/typescript/cli/src/config/chain.ts @@ -1,10 +1,12 @@ import { confirm, input } from '@inquirer/prompts'; +import { ethers } from 'ethers'; import { ChainMetadata, ChainMetadataSchema } from '@hyperlane-xyz/sdk'; import { ProtocolType } from '@hyperlane-xyz/utils'; import { CommandContext } from '../context/types.js'; import { errorRed, log, logBlue, logGreen } from '../logger.js'; +import { detectAndConfirmOrPrompt } from '../utils/chains.js'; import { readYamlOrJson } from '../utils/files.js'; export function readChainConfigs(filePath: string) { @@ -38,22 +40,52 @@ export async function createChainConfig({ context: CommandContext; }) { logBlue('Creating a new chain config'); - const name = await input({ - message: 'Enter chain name (one word, lower case)', - }); - const chainId = await input({ message: 'Enter chain id (number)' }); - const domainId = chainId; - const rpcUrl = await input({ message: 'Enter http or https rpc url' }); + + const rpcUrl = await detectAndConfirmOrPrompt( + async () => { + await new ethers.providers.JsonRpcProvider().getNetwork(); + return ethers.providers.JsonRpcProvider.defaultUrl(); + }, + 'rpc url', + 'Enter http or https', + ); + const provider = new ethers.providers.JsonRpcProvider(rpcUrl); + + const name = await detectAndConfirmOrPrompt( + async () => { + const clientName = await provider.send('web3_clientVersion', []); + const port = rpcUrl.split(':').slice(-1); + const client = clientName.split('/')[0]; + return `${client}${port}`; + }, + 'chain name', + 'Enter (one word, lower case)', + ); + + const chainId = parseInt( + await detectAndConfirmOrPrompt( + async () => { + const network = await provider.getNetwork(); + return network.chainId.toString(); + }, + 'chain id', + 'Enter a (number)', + ), + 10, + ); + const metadata: ChainMetadata = { name, - chainId: parseInt(chainId, 10), - domainId: parseInt(domainId, 10), + chainId, + domainId: chainId, protocol: ProtocolType.Ethereum, rpcUrls: [{ http: rpcUrl }], }; + const wantAdvancedConfig = await confirm({ + default: false, message: - 'Do you want to set block or gas properties for this chain config?(optional)', + 'Do you want to set block or gas properties for this chain config?', }); if (wantAdvancedConfig) { const wantBlockConfig = await confirm({ diff --git a/typescript/cli/src/utils/chains.ts b/typescript/cli/src/utils/chains.ts index 85f5c75628..470f0a20da 100644 --- a/typescript/cli/src/utils/chains.ts +++ b/typescript/cli/src/utils/chains.ts @@ -1,4 +1,4 @@ -import { Separator, checkbox } from '@inquirer/prompts'; +import { Separator, checkbox, confirm, input } from '@inquirer/prompts'; import select from '@inquirer/select'; import chalk from 'chalk'; @@ -73,3 +73,22 @@ function handleNewChain(chainNames: string[]) { process.exit(0); } } + +export async function detectAndConfirmOrPrompt( + detect: () => Promise, + label: string, + prompt: string, +): Promise { + let detectedValue: string | undefined; + try { + detectedValue = await detect(); + const confirmed = await confirm({ + message: `Detected ${label} as ${detectedValue}, is this correct?`, + }); + if (confirmed) { + return detectedValue; + } + // eslint-disable-next-line no-empty + } catch (e) {} + return input({ message: `${prompt} ${label}`, default: detectedValue }); +} diff --git a/typescript/sdk/src/core/CoreDeployer.hardhat-test.ts b/typescript/sdk/src/core/CoreDeployer.hardhat-test.ts index 90b3cfee43..41fd03add6 100644 --- a/typescript/sdk/src/core/CoreDeployer.hardhat-test.ts +++ b/typescript/sdk/src/core/CoreDeployer.hardhat-test.ts @@ -9,18 +9,18 @@ import { TestChainName, testChains } from '../consts/testChains.js'; import { HyperlaneContractsMap } from '../contracts/types.js'; import { HyperlaneProxyFactoryDeployer } from '../deploy/HyperlaneProxyFactoryDeployer.js'; import { HookConfig } from '../hook/types.js'; +import { DerivedIsmConfigWithAddress } from '../ism/EvmIsmReader.js'; import { HyperlaneIsmFactory } from '../ism/HyperlaneIsmFactory.js'; -import { DerivedIsmConfigWithAddress } from '../ism/read.js'; import { AggregationIsmConfig, IsmType } from '../ism/types.js'; import { MultiProvider } from '../providers/MultiProvider.js'; import { testCoreConfig } from '../test/testUtils.js'; import { ChainMap } from '../types.js'; +import { EvmCoreReader } from './EvmCoreReader.js'; import { HyperlaneCore } from './HyperlaneCore.js'; import { HyperlaneCoreChecker } from './HyperlaneCoreChecker.js'; import { HyperlaneCoreDeployer } from './HyperlaneCoreDeployer.js'; import { CoreFactories } from './contracts.js'; -import { EvmCoreReader } from './read.js'; import { CoreConfig } from './types.js'; describe('core', async () => { diff --git a/typescript/sdk/src/core/read.ts b/typescript/sdk/src/core/EvmCoreReader.ts similarity index 95% rename from typescript/sdk/src/core/read.ts rename to typescript/sdk/src/core/EvmCoreReader.ts index 8f3485eea9..c7edc135ee 100644 --- a/typescript/sdk/src/core/read.ts +++ b/typescript/sdk/src/core/EvmCoreReader.ts @@ -4,8 +4,8 @@ import { Mailbox__factory } from '@hyperlane-xyz/core'; import { Address, objMap, promiseObjAll } from '@hyperlane-xyz/utils'; import { DEFAULT_CONTRACT_READ_CONCURRENCY } from '../consts/concurrency.js'; -import { EvmHookReader } from '../hook/read.js'; -import { EvmIsmReader } from '../ism/read.js'; +import { EvmHookReader } from '../hook/EvmHookReader.js'; +import { EvmIsmReader } from '../ism/EvmIsmReader.js'; import { MultiProvider } from '../providers/MultiProvider.js'; import { ChainNameOrId } from '../types.js'; diff --git a/typescript/sdk/src/core/HyperlaneCore.ts b/typescript/sdk/src/core/HyperlaneCore.ts index 42d88863b4..f3cb21877c 100644 --- a/typescript/sdk/src/core/HyperlaneCore.ts +++ b/typescript/sdk/src/core/HyperlaneCore.ts @@ -19,7 +19,10 @@ import { HyperlaneApp } from '../app/HyperlaneApp.js'; import { appFromAddressesMapHelper } from '../contracts/contracts.js'; import { HyperlaneAddressesMap } from '../contracts/types.js'; import { OwnableConfig } from '../deploy/types.js'; -import { DerivedIsmConfigWithAddress, EvmIsmReader } from '../ism/read.js'; +import { + DerivedIsmConfigWithAddress, + EvmIsmReader, +} from '../ism/EvmIsmReader.js'; import { IsmType, ModuleType, ismTypeToModuleType } from '../ism/types.js'; import { MultiProvider } from '../providers/MultiProvider.js'; import { RouterConfig } from '../router/types.js'; diff --git a/typescript/sdk/src/hook/EvmHookModule.ts b/typescript/sdk/src/hook/EvmHookModule.ts index f5e37feb2a..c6e01bc87c 100644 --- a/typescript/sdk/src/hook/EvmHookModule.ts +++ b/typescript/sdk/src/hook/EvmHookModule.ts @@ -9,8 +9,8 @@ import { HyperlaneDeployer } from '../deploy/HyperlaneDeployer.js'; import { MultiProvider } from '../providers/MultiProvider.js'; import { EthersV5Transaction } from '../providers/ProviderType.js'; +import { EvmHookReader } from './EvmHookReader.js'; import { HookFactories } from './contracts.js'; -import { EvmHookReader } from './read.js'; import { HookConfig } from './types.js'; // WIP example implementation of EvmHookModule diff --git a/typescript/sdk/src/hook/read.test.ts b/typescript/sdk/src/hook/EvmHookReader.test.ts similarity index 99% rename from typescript/sdk/src/hook/read.test.ts rename to typescript/sdk/src/hook/EvmHookReader.test.ts index dd98039676..77af11b990 100644 --- a/typescript/sdk/src/hook/read.test.ts +++ b/typescript/sdk/src/hook/EvmHookReader.test.ts @@ -19,7 +19,7 @@ import { WithAddress } from '@hyperlane-xyz/utils'; import { TestChainName, test1 } from '../consts/testChains.js'; import { MultiProvider } from '../providers/MultiProvider.js'; -import { EvmHookReader } from './read.js'; +import { EvmHookReader } from './EvmHookReader.js'; import { HookType, MerkleTreeHookConfig, diff --git a/typescript/sdk/src/hook/read.ts b/typescript/sdk/src/hook/EvmHookReader.ts similarity index 100% rename from typescript/sdk/src/hook/read.ts rename to typescript/sdk/src/hook/EvmHookReader.ts diff --git a/typescript/sdk/src/index.ts b/typescript/sdk/src/index.ts index 99e388892b..cabe23127d 100644 --- a/typescript/sdk/src/index.ts +++ b/typescript/sdk/src/index.ts @@ -65,7 +65,7 @@ export { coreFactories, } from './core/contracts.js'; export { HyperlaneLifecyleEvent } from './core/events.js'; -export { EvmCoreReader } from './core/read.js'; +export { EvmCoreReader } from './core/EvmCoreReader.js'; export { CoreConfig, CoreViolationType, @@ -124,7 +124,7 @@ export { IgpViolationType, } from './gas/types.js'; export { HyperlaneHookDeployer } from './hook/HyperlaneHookDeployer.js'; -export { EvmHookReader } from './hook/read.js'; +export { EvmHookReader } from './hook/EvmHookReader.js'; export { AggregationHookConfig, DomainRoutingHookConfig, @@ -143,7 +143,7 @@ export { buildAggregationIsmConfigs, buildMultisigIsmConfigs, } from './ism/multisig.js'; -export { EvmIsmReader } from './ism/read.js'; +export { EvmIsmReader } from './ism/EvmIsmReader.js'; export { AggregationIsmConfig, DeployedIsm, diff --git a/typescript/sdk/src/ism/EvmIsmModule.ts b/typescript/sdk/src/ism/EvmIsmModule.ts index 4d277071d7..7d9921832b 100644 --- a/typescript/sdk/src/ism/EvmIsmModule.ts +++ b/typescript/sdk/src/ism/EvmIsmModule.ts @@ -12,7 +12,7 @@ import { EthersV5Transaction } from '../providers/ProviderType.js'; import { ChainNameOrId } from '../types.js'; import { EvmIsmCreator } from './EvmIsmCreator.js'; -import { EvmIsmReader } from './read.js'; +import { EvmIsmReader } from './EvmIsmReader.js'; import { IsmConfig } from './types.js'; export class EvmIsmModule extends HyperlaneModule< diff --git a/typescript/sdk/src/ism/read.test.ts b/typescript/sdk/src/ism/EvmIsmReader.test.ts similarity index 99% rename from typescript/sdk/src/ism/read.test.ts rename to typescript/sdk/src/ism/EvmIsmReader.test.ts index a50287de63..95368949b3 100644 --- a/typescript/sdk/src/ism/read.test.ts +++ b/typescript/sdk/src/ism/EvmIsmReader.test.ts @@ -21,7 +21,7 @@ import { WithAddress } from '@hyperlane-xyz/utils'; import { TestChainName } from '../consts/testChains.js'; import { MultiProvider } from '../providers/MultiProvider.js'; -import { EvmIsmReader } from './read.js'; +import { EvmIsmReader } from './EvmIsmReader.js'; import { IsmType, ModuleType, diff --git a/typescript/sdk/src/ism/read.ts b/typescript/sdk/src/ism/EvmIsmReader.ts similarity index 100% rename from typescript/sdk/src/ism/read.ts rename to typescript/sdk/src/ism/EvmIsmReader.ts diff --git a/typescript/sdk/src/ism/metadata/aggregation.test.ts b/typescript/sdk/src/ism/metadata/aggregation.test.ts new file mode 100644 index 0000000000..cb6c3818ca --- /dev/null +++ b/typescript/sdk/src/ism/metadata/aggregation.test.ts @@ -0,0 +1,41 @@ +import { expect } from 'chai'; +import { readFileSync, readdirSync } from 'fs'; + +import { + AggregationIsmMetadata, + AggregationIsmMetadataBuilder, +} from './aggregation.js'; +import { Fixture } from './types.test.js'; + +const path = '../../solidity/fixtures/aggregation'; +const files = readdirSync(path); +const fixtures: Fixture[] = files + .map((f) => JSON.parse(readFileSync(`${path}/${f}`, 'utf8'))) + .map((contents) => { + const { encoded, ...values } = contents; + return { + encoded, + decoded: { + submoduleMetadata: Object.values(values), + }, + }; + }); + +describe('AggregationMetadataBuilder', () => { + fixtures.forEach((fixture, i) => { + it(`should encode fixture ${i}`, () => { + expect(AggregationIsmMetadataBuilder.encode(fixture.decoded)).to.equal( + fixture.encoded, + ); + }); + + it(`should decode fixture ${i}`, () => { + expect( + AggregationIsmMetadataBuilder.decode( + fixture.encoded, + fixture.decoded.submoduleMetadata.length, + ), + ).to.deep.equal(fixture.decoded); + }); + }); +}); diff --git a/typescript/sdk/src/ism/metadata/aggregation.ts b/typescript/sdk/src/ism/metadata/aggregation.ts new file mode 100644 index 0000000000..8ddae22f65 --- /dev/null +++ b/typescript/sdk/src/ism/metadata/aggregation.ts @@ -0,0 +1,60 @@ +import { fromHexString, toHexString } from '@hyperlane-xyz/utils'; + +// null indicates that metadata is NOT INCLUDED for this submodule +// empty or 0x string indicates that metadata is INCLUDED but NULL +export interface AggregationIsmMetadata { + submoduleMetadata: Array; +} + +const RANGE_SIZE = 4; + +// adapted from rust/agents/relayer/src/msg/metadata/aggregation.rs +export class AggregationIsmMetadataBuilder { + static rangeIndex(index: number): number { + return index * 2 * RANGE_SIZE; + } + + static encode(metadata: AggregationIsmMetadata): string { + const rangeSize = this.rangeIndex(metadata.submoduleMetadata.length); + + let encoded = Buffer.alloc(rangeSize, 0); + metadata.submoduleMetadata.forEach((meta, index) => { + if (!meta) return; + + const start = encoded.length; + encoded = Buffer.concat([encoded, fromHexString(meta)]); + const end = encoded.length; + + const rangeStart = this.rangeIndex(index); + encoded.writeUint32BE(start, rangeStart); + encoded.writeUint32BE(end, rangeStart + RANGE_SIZE); + }); + + return toHexString(encoded); + } + + static metadataRange( + metadata: string, + index: number, + ): { start: number; end: number; encoded: string } { + const rangeStart = this.rangeIndex(index); + const encoded = fromHexString(metadata); + const start = encoded.readUint32BE(rangeStart); + const end = encoded.readUint32BE(rangeStart + RANGE_SIZE); + return { + start, + end, + encoded: toHexString(encoded.subarray(start, end)), + }; + } + + static decode(metadata: string, count: number): AggregationIsmMetadata { + const submoduleMetadata = []; + for (let i = 0; i < count; i++) { + const range = this.metadataRange(metadata, i); + const submeta = range.start > 0 ? range.encoded : null; + submoduleMetadata.push(submeta); + } + return { submoduleMetadata }; + } +} diff --git a/typescript/sdk/src/ism/metadata/multisig.test.ts b/typescript/sdk/src/ism/metadata/multisig.test.ts new file mode 100644 index 0000000000..1afca2bdb1 --- /dev/null +++ b/typescript/sdk/src/ism/metadata/multisig.test.ts @@ -0,0 +1,67 @@ +import { expect } from 'chai'; +import { readFileSync, readdirSync } from 'fs'; + +import { SignatureLike } from '@hyperlane-xyz/utils'; + +import { ModuleType } from '../types.js'; + +import { MultisigMetadata, MultisigMetadataBuilder } from './multisig.js'; +import { Fixture } from './types.test.js'; + +const path = '../../solidity/fixtures/multisig'; +const files = readdirSync(path); +const fixtures: Fixture[] = files + .map((f) => JSON.parse(readFileSync(`${path}/${f}`, 'utf8'))) + .map((contents) => { + const type = contents.type as MultisigMetadata['type']; + + const { dummy: _dummy, ...signatureValues } = contents.signatures; + const signatures = Object.values(signatureValues); + + let decoded: MultisigMetadata; + if (type === ModuleType.MERKLE_ROOT_MULTISIG) { + const { dummy: _dummy, ...branchValues } = contents.prefix.proof; + const branch = Object.values(branchValues); + decoded = { + type, + proof: { + branch, + leaf: contents.prefix.id, + index: contents.prefix.signedIndex, + }, + checkpoint: { + root: '', + index: contents.prefix.index, + merkle_tree_hook_address: contents.prefix.merkleTree, + }, + signatures, + }; + } else { + decoded = { + type, + checkpoint: { + root: contents.prefix.root, + index: contents.prefix.signedIndex, + merkle_tree_hook_address: contents.prefix.merkleTree, + }, + signatures, + }; + } + return { decoded, encoded: contents.encoded }; + }); + +describe('MultisigMetadataBuilder', () => { + fixtures.forEach((fixture, i) => { + it(`should encode fixture ${i}`, () => { + expect(MultisigMetadataBuilder.encode(fixture.decoded)).to.equal( + fixture.encoded, + ); + }); + + it(`should decode fixture ${i}`, () => { + expect( + MultisigMetadataBuilder.decode(fixture.encoded, fixture.decoded.type), + ).to.deep.equal(fixture.decoded); + }); + }); +}); diff --git a/typescript/sdk/src/ism/metadata/multisig.ts b/typescript/sdk/src/ism/metadata/multisig.ts new file mode 100644 index 0000000000..f27e7114fe --- /dev/null +++ b/typescript/sdk/src/ism/metadata/multisig.ts @@ -0,0 +1,156 @@ +import { joinSignature, splitSignature } from 'ethers/lib/utils.js'; + +import { + Checkpoint, + MerkleProof, + SignatureLike, + assert, + chunk, + ensure0x, + fromHexString, + strip0x, + toHexString, +} from '@hyperlane-xyz/utils'; + +import { ModuleType } from '../types.js'; + +interface MessageIdMultisigMetadata { + type: ModuleType.MESSAGE_ID_MULTISIG; + signatures: SignatureLike[]; + checkpoint: Omit; +} + +interface MerkleRootMultisigMetadata + extends Omit { + type: ModuleType.MERKLE_ROOT_MULTISIG; + proof: MerkleProof; +} + +const SIGNATURE_LENGTH = 65; + +export type MultisigMetadata = + | MessageIdMultisigMetadata + | MerkleRootMultisigMetadata; + +export class MultisigMetadataBuilder { + static encodeSimplePrefix(metadata: MessageIdMultisigMetadata): string { + const checkpoint = metadata.checkpoint; + const buf = Buffer.alloc(68); + buf.write(strip0x(checkpoint.merkle_tree_hook_address), 0, 32, 'hex'); + buf.write(strip0x(checkpoint.root), 32, 32, 'hex'); + buf.writeUInt32BE(checkpoint.index, 64); + return toHexString(buf); + } + + static decodeSimplePrefix(metadata: string) { + const buf = fromHexString(metadata); + const merkleTree = toHexString(buf.subarray(0, 32)); + const root = toHexString(buf.subarray(32, 64)); + const index = buf.readUint32BE(64); + const checkpoint = { + root, + index, + merkle_tree_hook_address: merkleTree, + }; + return { + signatureOffset: 68, + type: ModuleType.MESSAGE_ID_MULTISIG, + checkpoint, + }; + } + + static encodeProofPrefix(metadata: MerkleRootMultisigMetadata): string { + const checkpoint = metadata.checkpoint; + const buf = Buffer.alloc(1096); + buf.write(strip0x(checkpoint.merkle_tree_hook_address), 0, 32, 'hex'); + buf.writeUInt32BE(metadata.proof.index, 32); + buf.write(strip0x(metadata.proof.leaf.toString()), 36, 32, 'hex'); + const branchEncoded = metadata.proof.branch + .map((b) => strip0x(b.toString())) + .join(''); + buf.write(branchEncoded, 68, 32 * 32, 'hex'); + buf.writeUint32BE(checkpoint.index, 1092); + return toHexString(buf); + } + + static decodeProofPrefix(metadata: string) { + const buf = fromHexString(metadata); + const merkleTree = toHexString(buf.subarray(0, 32)); + const messageIndex = buf.readUint32BE(32); + const signedMessageId = toHexString(buf.subarray(36, 68)); + const branchEncoded = buf.subarray(68, 1092).toString('hex'); + const branch = chunk(branchEncoded, 32 * 2).map((v) => ensure0x(v)); + const signedIndex = buf.readUint32BE(1092); + const checkpoint = { + root: '', + index: messageIndex, + merkle_tree_hook_address: merkleTree, + }; + const proof: MerkleProof = { + branch, + leaf: signedMessageId, + index: signedIndex, + }; + return { + signatureOffset: 1096, + type: ModuleType.MERKLE_ROOT_MULTISIG, + checkpoint, + proof, + }; + } + + static encode(metadata: MultisigMetadata): string { + let encoded = + metadata.type === ModuleType.MESSAGE_ID_MULTISIG + ? this.encodeSimplePrefix(metadata) + : this.encodeProofPrefix(metadata); + + metadata.signatures.forEach((signature) => { + const encodedSignature = joinSignature(signature); + assert(fromHexString(encodedSignature).byteLength === SIGNATURE_LENGTH); + encoded += strip0x(encodedSignature); + }); + + return encoded; + } + + static signatureAt( + metadata: string, + offset: number, + index: number, + ): SignatureLike | undefined { + const buf = fromHexString(metadata); + const start = offset + index * SIGNATURE_LENGTH; + const end = start + SIGNATURE_LENGTH; + if (end > buf.byteLength) { + return undefined; + } + + return toHexString(buf.subarray(start, end)); + } + + static decode( + metadata: string, + type: ModuleType.MERKLE_ROOT_MULTISIG | ModuleType.MESSAGE_ID_MULTISIG, + ): MultisigMetadata { + const prefix: any = + type === ModuleType.MERKLE_ROOT_MULTISIG + ? this.decodeProofPrefix(metadata) + : this.decodeSimplePrefix(metadata); + + const { signatureOffset: offset, ...values } = prefix; + + const signatures: SignatureLike[] = []; + for (let i = 0; this.signatureAt(metadata, offset, i); i++) { + const { r, s, v } = splitSignature( + this.signatureAt(metadata, offset, i)!, + ); + signatures.push({ r, s, v }); + } + + return { + signatures, + ...values, + }; + } +} diff --git a/typescript/sdk/src/ism/metadata/types.test.ts b/typescript/sdk/src/ism/metadata/types.test.ts new file mode 100644 index 0000000000..b173f3ca35 --- /dev/null +++ b/typescript/sdk/src/ism/metadata/types.test.ts @@ -0,0 +1,4 @@ +export type Fixture = { + decoded: T; + encoded: string; +}; diff --git a/typescript/sdk/src/token/read.ts b/typescript/sdk/src/token/EvmERC20WarpRouteReader.ts similarity index 97% rename from typescript/sdk/src/token/read.ts rename to typescript/sdk/src/token/EvmERC20WarpRouteReader.ts index c6d8c78bb8..0699fe5c45 100644 --- a/typescript/sdk/src/token/read.ts +++ b/typescript/sdk/src/token/EvmERC20WarpRouteReader.ts @@ -8,8 +8,8 @@ import { ERC20Metadata, ERC20RouterConfig } from '@hyperlane-xyz/sdk'; import { Address } from '@hyperlane-xyz/utils'; import { DEFAULT_CONTRACT_READ_CONCURRENCY } from '../consts/concurrency.js'; -import { EvmHookReader } from '../hook/read.js'; -import { EvmIsmReader } from '../ism/read.js'; +import { EvmHookReader } from '../hook/EvmHookReader.js'; +import { EvmIsmReader } from '../ism/EvmIsmReader.js'; import { MultiProvider } from '../providers/MultiProvider.js'; import { ChainName } from '../types.js'; diff --git a/typescript/sdk/src/token/deploy.hardhat-test.ts b/typescript/sdk/src/token/deploy.hardhat-test.ts index 72a3028481..25ed9e50bf 100644 --- a/typescript/sdk/src/token/deploy.hardhat-test.ts +++ b/typescript/sdk/src/token/deploy.hardhat-test.ts @@ -17,6 +17,7 @@ import { HyperlaneIsmFactory } from '../ism/HyperlaneIsmFactory.js'; import { MultiProvider } from '../providers/MultiProvider.js'; import { ChainMap } from '../types.js'; +import { EvmERC20WarpRouteReader } from './EvmERC20WarpRouteReader.js'; import { HypERC20CollateralConfig, HypERC20Config, @@ -24,7 +25,6 @@ import { TokenType, } from './config.js'; import { HypERC20Deployer } from './deploy.js'; -import { EvmERC20WarpRouteReader } from './read.js'; import { WarpRouteDeployConfig } from './types.js'; describe('TokenDeployer', async () => { diff --git a/typescript/utils/package.json b/typescript/utils/package.json index 72475e014b..02c8cd29f9 100644 --- a/typescript/utils/package.json +++ b/typescript/utils/package.json @@ -27,6 +27,7 @@ "license": "Apache-2.0", "prepublish": "yarn build", "scripts": { + "dev": "tsc -w", "build": "tsc", "clean": "rm -rf ./dist", "check": "tsc --noEmit", diff --git a/typescript/utils/src/index.ts b/typescript/utils/src/index.ts index 6a1752a632..08b85a1c87 100644 --- a/typescript/utils/src/index.ts +++ b/typescript/utils/src/index.ts @@ -119,6 +119,8 @@ export { streamToString, toTitleCase, trimToLength, + fromHexString, + toHexString, } from './strings.js'; export { isNullish, isNumeric } from './typeof.js'; export { diff --git a/typescript/utils/src/logging.ts b/typescript/utils/src/logging.ts index caffe418ee..6ea8333dc5 100644 --- a/typescript/utils/src/logging.ts +++ b/typescript/utils/src/logging.ts @@ -9,6 +9,7 @@ import { safelyAccessEnvVar } from './env.js'; // A custom enum definition because pino does not export an enum // and because we use 'off' instead of 'silent' to match the agent options export enum LogLevel { + Trace = 'trace', Debug = 'debug', Info = 'info', Warn = 'warn', diff --git a/typescript/utils/src/strings.ts b/typescript/utils/src/strings.ts index 3690ace198..26d40838aa 100644 --- a/typescript/utils/src/strings.ts +++ b/typescript/utils/src/strings.ts @@ -1,3 +1,5 @@ +import { ensure0x, strip0x } from './addresses.js'; + export function toTitleCase(str: string) { return str.replace(/\w\S*/g, (txt) => { return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase(); @@ -38,3 +40,8 @@ export function errorToString(error: any, maxLength = 300) { if (typeof details === 'string') return trimToLength(details, maxLength); return trimToLength(JSON.stringify(details), maxLength); } + +export const fromHexString = (hexstr: string) => + Buffer.from(strip0x(hexstr), 'hex'); + +export const toHexString = (buf: Buffer) => ensure0x(buf.toString('hex')); diff --git a/yarn.lock b/yarn.lock index df546cb5c9..ef6de3e361 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5000,8 +5000,10 @@ __metadata: chai: "npm:^4.3.6" ethereum-waffle: "npm:^4.0.10" ethers: "npm:^5.7.2" + fx-portal: "npm:^1.0.3" hardhat: "npm:^2.22.2" hardhat-gas-reporter: "npm:^1.0.9" + hardhat-ignore-warnings: "npm:^0.2.11" prettier: "npm:^2.8.8" prettier-plugin-solidity: "npm:^1.1.3" solhint: "npm:^4.5.4" @@ -6842,6 +6844,13 @@ __metadata: languageName: node linkType: hard +"@openzeppelin/contracts@npm:^4.2.0": + version: 4.9.6 + resolution: "@openzeppelin/contracts@npm:4.9.6" + checksum: 71f45ad42e68c0559be4ba502115462a01c76fc805c08d3005c10b5550a093f1a2b00b2d7e9d6d1f331e147c50fd4ad832f71c4470ec5b34f5a2d0751cd19a47 + languageName: node + linkType: hard + "@openzeppelin/contracts@npm:^4.4.1": version: 4.9.5 resolution: "@openzeppelin/contracts@npm:4.9.5" @@ -14277,6 +14286,15 @@ __metadata: languageName: node linkType: hard +"fx-portal@npm:^1.0.3": + version: 1.0.3 + resolution: "fx-portal@npm:1.0.3" + dependencies: + "@openzeppelin/contracts": "npm:^4.2.0" + checksum: 89309e03da57238d153b41fd9fd492d582d41a90da51fc18b4cdd939a8713736572ed1ba034210888ad1b2e81596a860f157785f6911e6d265e2fd0730aa94c2 + languageName: node + linkType: hard + "ganache@npm:7.4.3": version: 7.4.3 resolution: "ganache@npm:7.4.3" @@ -14963,6 +14981,17 @@ __metadata: languageName: node linkType: hard +"hardhat-ignore-warnings@npm:^0.2.11": + version: 0.2.11 + resolution: "hardhat-ignore-warnings@npm:0.2.11" + dependencies: + minimatch: "npm:^5.1.0" + node-interval-tree: "npm:^2.0.1" + solidity-comments: "npm:^0.0.2" + checksum: b249e02dbc207a40cb3090577c0f972b52f233b062ebafed413e70454ad389f991f32a8fda582f32d87f6057886e529a9f58ff6abb604d3294f7f4697a6dcb16 + languageName: node + linkType: hard + "hardhat@npm:^2.22.2": version: 2.22.2 resolution: "hardhat@npm:2.22.2" @@ -18086,6 +18115,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:^5.1.0": + version: 5.1.6 + resolution: "minimatch@npm:5.1.6" + dependencies: + brace-expansion: "npm:^2.0.1" + checksum: 126b36485b821daf96d33b5c821dac600cc1ab36c87e7a532594f9b1652b1fa89a1eebcaad4dff17c764dce1a7ac1531327f190fed5f97d8f6e5f889c116c429 + languageName: node + linkType: hard + "minimist-options@npm:^4.0.2": version: 4.1.0 resolution: "minimist-options@npm:4.1.0" @@ -18875,6 +18913,15 @@ __metadata: languageName: node linkType: hard +"node-interval-tree@npm:^2.0.1": + version: 2.1.2 + resolution: "node-interval-tree@npm:2.1.2" + dependencies: + shallowequal: "npm:^1.1.0" + checksum: da3b54b12720fa20a939a24dfb890b54f29c1c939a837e89d6197cbb96701f65279786a05843ac99ded60e8362cc48fda4ac2466a2e65a34b3fb84b648487d9f + languageName: node + linkType: hard + "node-releases@npm:^2.0.14": version: 2.0.14 resolution: "node-releases@npm:2.0.14" @@ -21365,6 +21412,13 @@ __metadata: languageName: node linkType: hard +"shallowequal@npm:^1.1.0": + version: 1.1.0 + resolution: "shallowequal@npm:1.1.0" + checksum: f4c1de0837f106d2dbbfd5d0720a5d059d1c66b42b580965c8f06bb1db684be8783538b684092648c981294bf817869f743a066538771dbecb293df78f765e00 + languageName: node + linkType: hard + "shebang-command@npm:^1.2.0": version: 1.2.0 resolution: "shebang-command@npm:1.2.0" @@ -21666,6 +21720,20 @@ __metadata: languageName: node linkType: hard +"solidity-comments-darwin-arm64@npm:0.0.2": + version: 0.0.2 + resolution: "solidity-comments-darwin-arm64@npm:0.0.2" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"solidity-comments-darwin-x64@npm:0.0.2": + version: 0.0.2 + resolution: "solidity-comments-darwin-x64@npm:0.0.2" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "solidity-comments-extractor@npm:^0.0.7": version: 0.0.7 resolution: "solidity-comments-extractor@npm:0.0.7" @@ -21673,6 +21741,101 @@ __metadata: languageName: node linkType: hard +"solidity-comments-freebsd-x64@npm:0.0.2": + version: 0.0.2 + resolution: "solidity-comments-freebsd-x64@npm:0.0.2" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"solidity-comments-linux-arm64-gnu@npm:0.0.2": + version: 0.0.2 + resolution: "solidity-comments-linux-arm64-gnu@npm:0.0.2" + conditions: os=linux & cpu=arm64 & libc=glibc + languageName: node + linkType: hard + +"solidity-comments-linux-arm64-musl@npm:0.0.2": + version: 0.0.2 + resolution: "solidity-comments-linux-arm64-musl@npm:0.0.2" + conditions: os=linux & cpu=arm64 & libc=musl + languageName: node + linkType: hard + +"solidity-comments-linux-x64-gnu@npm:0.0.2": + version: 0.0.2 + resolution: "solidity-comments-linux-x64-gnu@npm:0.0.2" + conditions: os=linux & cpu=x64 & libc=glibc + languageName: node + linkType: hard + +"solidity-comments-linux-x64-musl@npm:0.0.2": + version: 0.0.2 + resolution: "solidity-comments-linux-x64-musl@npm:0.0.2" + conditions: os=linux & cpu=x64 & libc=musl + languageName: node + linkType: hard + +"solidity-comments-win32-arm64-msvc@npm:0.0.2": + version: 0.0.2 + resolution: "solidity-comments-win32-arm64-msvc@npm:0.0.2" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + +"solidity-comments-win32-ia32-msvc@npm:0.0.2": + version: 0.0.2 + resolution: "solidity-comments-win32-ia32-msvc@npm:0.0.2" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + +"solidity-comments-win32-x64-msvc@npm:0.0.2": + version: 0.0.2 + resolution: "solidity-comments-win32-x64-msvc@npm:0.0.2" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + +"solidity-comments@npm:^0.0.2": + version: 0.0.2 + resolution: "solidity-comments@npm:0.0.2" + dependencies: + solidity-comments-darwin-arm64: "npm:0.0.2" + solidity-comments-darwin-x64: "npm:0.0.2" + solidity-comments-freebsd-x64: "npm:0.0.2" + solidity-comments-linux-arm64-gnu: "npm:0.0.2" + solidity-comments-linux-arm64-musl: "npm:0.0.2" + solidity-comments-linux-x64-gnu: "npm:0.0.2" + solidity-comments-linux-x64-musl: "npm:0.0.2" + solidity-comments-win32-arm64-msvc: "npm:0.0.2" + solidity-comments-win32-ia32-msvc: "npm:0.0.2" + solidity-comments-win32-x64-msvc: "npm:0.0.2" + dependenciesMeta: + solidity-comments-darwin-arm64: + optional: true + solidity-comments-darwin-x64: + optional: true + solidity-comments-freebsd-x64: + optional: true + solidity-comments-linux-arm64-gnu: + optional: true + solidity-comments-linux-arm64-musl: + optional: true + solidity-comments-linux-x64-gnu: + optional: true + solidity-comments-linux-x64-musl: + optional: true + solidity-comments-win32-arm64-msvc: + optional: true + solidity-comments-win32-ia32-msvc: + optional: true + solidity-comments-win32-x64-msvc: + optional: true + checksum: a39b0340c964f2a13594bc34c36656072abfb3cac459f6ca3611aedbbb2ff82d2821e99bfa2cff083af10409b49396c844e55b13acde81ba2758363fa82a6ea8 + languageName: node + linkType: hard + "solidity-coverage@npm:^0.8.3": version: 0.8.3 resolution: "solidity-coverage@npm:0.8.3"