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
parent
cc8731985b
commit
c9c5d37bab
@ -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 |
||||
|
@ -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); |
||||
} |
||||
} |
@ -0,0 +1 @@ |
||||
Subproject commit ebd046507d76cd03fa2b2559257091471a259ed7 |
@ -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 |
||||
) |
||||
); |
||||
} |
||||
} |
Loading…
Reference in new issue