The home for Hyperlane core contracts, sdk packages, and other infrastructure
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 
hyperlane-monorepo/solidity/test/isms/ExternalBridgeTest.sol

333 lines
12 KiB

// SPDX-License-Identifier: MIT or Apache-2.0
pragma solidity ^0.8.13;
import {Test} from "forge-std/Test.sol";
import {TypeCasts} from "../../contracts/libs/TypeCasts.sol";
import {StandardHookMetadata} from "../../contracts/hooks/libs/StandardHookMetadata.sol";
import {TestMailbox} from "../../contracts/test/TestMailbox.sol";
import {Message} from "../../contracts/libs/Message.sol";
import {MessageUtils} from "./IsmTestUtils.sol";
import {TestRecipient} from "../../contracts/test/TestRecipient.sol";
import {AbstractMessageIdAuthorizedIsm} from "../../contracts/isms/hook/AbstractMessageIdAuthorizedIsm.sol";
import {AbstractMessageIdAuthHook} from "../../contracts/hooks/libs/AbstractMessageIdAuthHook.sol";
abstract contract ExternalBridgeTest is Test {
using TypeCasts for address;
using MessageUtils for bytes;
uint8 internal constant HYPERLANE_VERSION = 1;
uint32 internal constant ORIGIN_DOMAIN = 1;
uint32 internal constant DESTINATION_DOMAIN = 2;
uint256 internal constant MSG_VALUE = 1 ether;
uint256 internal constant MAX_MSG_VALUE = 2 ** 255 - 1;
uint256 internal GAS_QUOTE;
TestMailbox internal originMailbox;
TestRecipient internal testRecipient;
AbstractMessageIdAuthHook internal hook;
AbstractMessageIdAuthorizedIsm internal ism;
bytes internal testMessage =
abi.encodePacked("Hello from the other chain!");
bytes internal testMetadata =
StandardHookMetadata.overrideRefundAddress(address(this));
bytes internal encodedMessage;
bytes32 internal messageId;
function setUp() public virtual {
testRecipient = new TestRecipient();
encodedMessage = _encodeTestMessage();
messageId = Message.id(encodedMessage);
}
/* ============ hook.quoteDispatch ============ */
function test_quoteDispatch() public view {
assertEq(hook.quoteDispatch(testMetadata, encodedMessage), GAS_QUOTE);
}
/* ============ Hook.postDispatch ============ */
function test_postDispatch() public {
bytes memory hookMetadata = testMetadata;
bytes memory encodedHookData = _encodeHookData(messageId, 0);
originMailbox.updateLatestDispatchedId(messageId);
_expectOriginExternalBridgeCall(encodedHookData);
uint256 quote = hook.quoteDispatch(testMetadata, encodedMessage);
hook.postDispatch{value: quote}(testMetadata, encodedMessage);
}
function test_postDispatch_revertWhen_chainIDNotSupported() public {
bytes memory message = originMailbox.buildOutboundMessage(
3,
TypeCasts.addressToBytes32(address(this)),
testMessage
);
originMailbox.updateLatestDispatchedId(Message.id(message));
vm.expectRevert(
"AbstractMessageIdAuthHook: invalid destination domain"
);
hook.postDispatch(testMetadata, message);
}
function test_postDispatch_revertWhen_notLastDispatchedMessage() public {
vm.expectRevert(
"AbstractMessageIdAuthHook: message not latest dispatched"
);
hook.postDispatch(testMetadata, encodedMessage);
}
function test_postDispatch_revertWhen_tooMuchValue() public {
vm.deal(address(this), uint256(MAX_MSG_VALUE + 1));
bytes memory excessValueMetadata = StandardHookMetadata
.overrideMsgValue(uint256(2 ** 255 + 1));
originMailbox.updateLatestDispatchedId(messageId);
vm.expectRevert(
"AbstractMessageIdAuthHook: msgValue must be less than 2 ** 255"
);
hook.postDispatch(excessValueMetadata, encodedMessage);
}
function testFuzz_postDispatch_refundsExtraValue(
uint256 extraValue
) public virtual {
vm.assume(extraValue < MAX_MSG_VALUE);
vm.deal(address(this), address(this).balance + extraValue);
uint256 valueBefore = address(this).balance;
bytes memory encodedHookData = _encodeHookData(messageId, 0);
originMailbox.updateLatestDispatchedId(messageId);
_expectOriginExternalBridgeCall(encodedHookData);
uint256 quote = hook.quoteDispatch(testMetadata, encodedMessage);
hook.postDispatch{value: quote + extraValue}(
testMetadata,
encodedMessage
);
assertEq(address(this).balance, valueBefore - quote);
}
function test_postDispatch_revertWhen_insufficientValue() public {
bytes memory encodedHookData = _encodeHookData(messageId, 0);
originMailbox.updateLatestDispatchedId(messageId);
_expectOriginExternalBridgeCall(encodedHookData);
uint256 quote = hook.quoteDispatch(testMetadata, encodedMessage);
vm.expectRevert(); //arithmetic underflow
hook.postDispatch{value: quote - 1}(testMetadata, encodedMessage);
}
/* ============ ISM.preVerifyMessage ============ */
function test_preVerifyMessage_asyncCall() public {
bytes memory encodedHookData = _encodeHookData(messageId, 0);
_externalBridgeDestinationCall(encodedHookData, 0);
assertTrue(ism.isVerified(encodedMessage));
}
function test_preVerifyMessage_externalBridgeCall() public virtual {
bytes memory externalCalldata = _encodeExternalDestinationBridgeCall(
address(hook),
address(ism),
0,
messageId
);
assertTrue(ism.verify(externalCalldata, encodedMessage));
assertTrue(ism.isVerified(encodedMessage));
}
/* ============ ISM.verify ============ */
function test_verify_revertWhen_invalidMetadata() public virtual {
vm.expectRevert();
assertFalse(ism.verify(new bytes(0), encodedMessage));
}
function test_verify_msgValue_asyncCall() public virtual {
bytes memory encodedHookData = _encodeHookData(messageId, MSG_VALUE);
_externalBridgeDestinationCall(encodedHookData, MSG_VALUE);
assertTrue(ism.verify(new bytes(0), encodedMessage));
assertEq(address(testRecipient).balance, MSG_VALUE);
}
function test_verify_msgValue_externalBridgeCall() public virtual {
bytes memory externalCalldata = _encodeExternalDestinationBridgeCall(
address(hook),
address(ism),
MSG_VALUE,
messageId
);
assertTrue(ism.verify(externalCalldata, encodedMessage));
assertEq(address(testRecipient).balance, 1 ether);
}
function test_verify_revertsWhen_invalidIsm() public virtual {
bytes memory externalCalldata = _encodeExternalDestinationBridgeCall(
address(hook),
address(hook),
0,
messageId
);
vm.expectRevert();
assertFalse(ism.verify(externalCalldata, encodedMessage));
}
function test_verify_revertsWhen_notAuthorizedHook() public virtual {
bytes memory unauthorizedHookErrorMsg = _setExternalOriginSender(
address(this)
);
bytes memory externalCalldata = _encodeExternalDestinationBridgeCall(
address(this),
address(ism),
0,
messageId
);
// external call
vm.expectRevert(unauthorizedHookErrorMsg);
assertFalse(ism.verify(externalCalldata, encodedMessage));
// async call vm.expectRevert(NotCrossChainCall.selector);
vm.expectRevert();
_externalBridgeDestinationCall(externalCalldata, 0);
assertFalse(ism.isVerified(encodedMessage));
}
function test_verify_revertsWhen_incorrectMessageId() public virtual {
bytes32 incorrectMessageId = keccak256("incorrect message id");
bytes memory externalCalldata = _encodeExternalDestinationBridgeCall(
address(hook),
address(ism),
0,
incorrectMessageId
);
// external call
vm.expectRevert();
assertFalse(ism.verify(externalCalldata, encodedMessage));
// async call - native bridges might have try catch block to prevent revert
try
this.externalBridgeDestinationCallWrapper(
_encodeHookData(incorrectMessageId, 0),
0
)
{} catch {}
assertFalse(ism.isVerified(testMessage));
}
/// forge-config: default.fuzz.runs = 10
function test_verify_valueAlreadyClaimed(uint256 _msgValue) public virtual {
_msgValue = bound(_msgValue, 0, MAX_MSG_VALUE);
_externalBridgeDestinationCall(
_encodeHookData(messageId, _msgValue),
_msgValue
);
bool verified = ism.verify(new bytes(0), encodedMessage);
assertTrue(verified);
assertEq(address(ism).balance, 0);
assertEq(address(testRecipient).balance, _msgValue);
// send more value to the ISM
vm.deal(address(ism), _msgValue);
// verified still true
verified = ism.verify(new bytes(0), encodedMessage);
assertTrue(verified);
// value which was already sent
assertEq(address(ism).balance, _msgValue);
assertEq(address(testRecipient).balance, _msgValue);
}
function test_verify_override_msgValue() public virtual {
bytes memory encodedHookData = _encodeHookData(messageId, MSG_VALUE);
_externalBridgeDestinationCall(encodedHookData, MSG_VALUE);
vm.expectRevert("AbstractMessageIdAuthorizedIsm: invalid msg.value");
_externalBridgeDestinationCall(encodedHookData, 0);
assertTrue(ism.verify(new bytes(0), encodedMessage));
assertEq(address(testRecipient).balance, MSG_VALUE);
}
function test_verify_false_arbitraryCall() public virtual {
bytes memory incorrectCalldata = _encodeExternalDestinationBridgeCall(
address(hook),
address(this),
0,
messageId
);
vm.expectRevert();
ism.verify(incorrectCalldata, encodedMessage);
assertFalse(ism.isVerified(encodedMessage));
}
/* ============ helper functions ============ */
function _encodeTestMessage() internal view returns (bytes memory) {
return
originMailbox.buildOutboundMessage(
DESTINATION_DOMAIN,
TypeCasts.addressToBytes32(address(testRecipient)),
testMessage
);
}
function _encodeHookData(
bytes32 _messageId,
uint256 _msgValue
) internal pure returns (bytes memory) {
return
abi.encodeCall(
AbstractMessageIdAuthorizedIsm.preVerifyMessage,
(_messageId, _msgValue)
);
}
// wrapper function needed for _externalBridgeDestinationCall because try catch cannot call an internal function
function externalBridgeDestinationCallWrapper(
bytes memory _encodedHookData,
uint256 _msgValue
) external {
_externalBridgeDestinationCall(_encodedHookData, _msgValue);
}
function _expectOriginExternalBridgeCall(
bytes memory _encodedHookData
) internal virtual;
function _externalBridgeDestinationCall(
bytes memory _encodedHookData,
uint256 _msgValue
) internal virtual;
function _encodeExternalDestinationBridgeCall(
address _from,
address _to,
uint256 _msgValue,
bytes32 _messageId
) internal virtual returns (bytes memory);
function _setExternalOriginSender(
address _sender
) internal virtual returns (bytes memory) {}
receive() external payable {}
// meant to be mock an arbitrary successful call made by the external bridge
function preVerifyMessage(bytes32 /*messageId*/) public payable {}
}