add polygon hook and ism (#3038)

### Description

- Add native polygon bridge hook and isms

### Related issues

- Fixes #2847 
- Documented with https://github.com/hyperlane-xyz/v3-docs/pull/31

### Backward compatibility

Yes

### Testing

Unit Tests

---------

Co-authored-by: NOOMA-42 <suncloud2203357667@gmail.com>
kunal/avs-contract-deployment
Paul-T.C-Yu 7 months ago committed by GitHub
parent cc8731985b
commit c9c5d37bab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 3
      .gitmodules
  2. 83
      solidity/contracts/hooks/PolygonPosHook.sol
  3. 58
      solidity/contracts/isms/hook/PolygonPosIsm.sol
  4. 2
      solidity/foundry.toml
  5. 1
      solidity/lib/fx-portal
  6. 3
      solidity/package.json
  7. 1
      solidity/remappings.txt
  8. 367
      solidity/test/isms/PolygonPosIsm.t.sol
  9. 17
      yarn.lock

3
.gitmodules vendored

@ -1,3 +1,6 @@
[submodule "solidity/lib/forge-std"] [submodule "solidity/lib/forge-std"]
path = solidity/lib/forge-std path = solidity/lib/forge-std
url = https://github.com/foundry-rs/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

@ -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;
}
}

@ -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);
}
}

@ -18,7 +18,7 @@ verbosity = 4
[rpc_endpoints] [rpc_endpoints]
mainnet = "https://eth.merkle.io" mainnet = "https://eth.merkle.io"
optimism = "https://mainnet.optimism.io " optimism = "https://mainnet.optimism.io "
polygon = "https://rpc.ankr.com/polygon"
[fuzz] [fuzz]
runs = 50 runs = 50

@ -0,0 +1 @@
Subproject commit ebd046507d76cd03fa2b2559257091471a259ed7

@ -7,7 +7,8 @@
"@hyperlane-xyz/utils": "3.11.1", "@hyperlane-xyz/utils": "3.11.1",
"@layerzerolabs/lz-evm-oapp-v2": "2.0.2", "@layerzerolabs/lz-evm-oapp-v2": "2.0.2",
"@openzeppelin/contracts": "^4.9.3", "@openzeppelin/contracts": "^4.9.3",
"@openzeppelin/contracts-upgradeable": "^v4.9.3" "@openzeppelin/contracts-upgradeable": "^v4.9.3",
"fx-portal": "^1.0.3"
}, },
"devDependencies": { "devDependencies": {
"@layerzerolabs/solidity-examples": "^1.1.0", "@layerzerolabs/solidity-examples": "^1.1.0",

@ -3,3 +3,4 @@
@eth-optimism=../node_modules/@eth-optimism @eth-optimism=../node_modules/@eth-optimism
ds-test/=lib/forge-std/lib/ds-test/src/ ds-test/=lib/forge-std/lib/ds-test/src/
forge-std/=lib/forge-std/src/ forge-std/=lib/forge-std/src/
fx-portal/=lib/fx-portal/

@ -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
)
);
}
}

@ -5000,6 +5000,7 @@ __metadata:
chai: "npm:^4.3.6" chai: "npm:^4.3.6"
ethereum-waffle: "npm:^4.0.10" ethereum-waffle: "npm:^4.0.10"
ethers: "npm:^5.7.2" ethers: "npm:^5.7.2"
fx-portal: "npm:^1.0.3"
hardhat: "npm:^2.22.2" hardhat: "npm:^2.22.2"
hardhat-gas-reporter: "npm:^1.0.9" hardhat-gas-reporter: "npm:^1.0.9"
prettier: "npm:^2.8.8" prettier: "npm:^2.8.8"
@ -6842,6 +6843,13 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "@openzeppelin/contracts@npm:^4.4.1":
version: 4.9.5 version: 4.9.5
resolution: "@openzeppelin/contracts@npm:4.9.5" resolution: "@openzeppelin/contracts@npm:4.9.5"
@ -14277,6 +14285,15 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "ganache@npm:7.4.3":
version: 7.4.3 version: 7.4.3
resolution: "ganache@npm:7.4.3" resolution: "ganache@npm:7.4.3"

Loading…
Cancel
Save