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.
333 lines
12 KiB
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 {}
|
|
}
|
|
|