diff --git a/test/unit-tests/MultiBridgeMessageReceiver.t.sol b/test/unit-tests/MultiBridgeMessageReceiver.t.sol index 184c5b4..b075e4b 100644 --- a/test/unit-tests/MultiBridgeMessageReceiver.t.sol +++ b/test/unit-tests/MultiBridgeMessageReceiver.t.sol @@ -313,6 +313,28 @@ contract MultiBridgeMessageReceiverTest is Setup { assertTrue(receiver.isExecutionScheduled(msgId)); } + /// @dev cannot execute mismatched message params and hash + function test_schedule_message_hash_mismatch() public { + vm.startPrank(wormholeAdapterAddr); + + MessageLibrary.Message memory message = MessageLibrary.Message({ + srcChainId: SRC_CHAIN_ID, + dstChainId: DST_CHAIN_ID, + target: address(42), + nonce: 42, + callData: bytes("42"), + nativeValue: 0, + expiration: 0 + }); + bytes32 msgId = message.computeMsgId(); + + receiver.receiveMessage(message); + + message.nonce = 43; + vm.expectRevert(Error.EXEC_PARAMS_HASH_MISMATCH.selector); + receiver.scheduleMessageExecution(msgId, message.extractExecutionParams()); + } + /// @dev cannot schedule execution of message past deadline function test_schedule_message_execution_passed_deadline() public { vm.startPrank(wormholeAdapterAddr); @@ -701,4 +723,70 @@ contract MultiBridgeMessageReceiverTest is Setup { assertEq(successfulBridge.length, 1); assertEq(successfulBridge[0], WormholeReceiverAdapter(wormholeAdapterAddr).name()); } + + /// @dev should get message info for invalid message id + function test_get_message_info_invalid_message_id() public { + (bool isScheduled, uint256 msgCurrentVotes, string[] memory successfulBridge) = + receiver.getMessageInfo(bytes32(0)); + assertFalse(isScheduled); + assertEq(msgCurrentVotes, 0); + assertEq(successfulBridge.length, 0); + } + + /// @dev should get message info for partial delivery + function test_get_message_info_partial_delivery() public { + vm.startPrank(wormholeAdapterAddr); + + // Assuming there's a function to add executors or that multiple executors exist by default + + MessageLibrary.Message memory message = MessageLibrary.Message({ + srcChainId: SRC_CHAIN_ID, + dstChainId: DST_CHAIN_ID, + target: address(42), + nonce: 42, + callData: bytes("42"), + nativeValue: 0, + expiration: type(uint256).max + }); + bytes32 msgId = message.computeMsgId(); + + receiver.receiveMessage(message); + + // You may need a mechanism to simulate or enforce failed deliveries for certain executors + + (bool isScheduled, uint256 msgCurrentVotes, string[] memory successfulBridge) = receiver.getMessageInfo(msgId); + assertFalse(isScheduled); + // Adjust the following assertions as needed based on your setup + assertTrue(msgCurrentVotes > 0); // Ensure there's at least one successful delivery + assertEq(successfulBridge.length, msgCurrentVotes); + } + + /// @dev should get message info scheduled for execution + function test_get_message_info_scheduled_execution() public { + // Reduce quorum first + vm.startPrank(address(timelockAddr)); + receiver.updateQuorum(1); + vm.stopPrank(); + + vm.startPrank(wormholeAdapterAddr); + + MessageLibrary.Message memory message = MessageLibrary.Message({ + srcChainId: SRC_CHAIN_ID, + dstChainId: DST_CHAIN_ID, + target: address(42), + nonce: 42, + callData: bytes("42"), + nativeValue: 0, + expiration: type(uint256).max + }); + bytes32 msgId = message.computeMsgId(); + + receiver.receiveMessage(message); + receiver.scheduleMessageExecution(msgId, message.extractExecutionParams()); + + (bool isScheduled, uint256 msgCurrentVotes, string[] memory successfulBridge) = receiver.getMessageInfo(msgId); + assertTrue(isScheduled); + assertEq(msgCurrentVotes, 1); + assertEq(successfulBridge.length, 1); + } } diff --git a/test/unit-tests/MultiBridgeMessageSender.t.sol b/test/unit-tests/MultiBridgeMessageSender.t.sol index a61f997..f5c9887 100644 --- a/test/unit-tests/MultiBridgeMessageSender.t.sol +++ b/test/unit-tests/MultiBridgeMessageSender.t.sol @@ -16,6 +16,12 @@ import "src/libraries/Error.sol"; import "src/libraries/Message.sol"; import {MultiBridgeMessageSender} from "src/MultiBridgeMessageSender.sol"; +contract EthReceiverRevert { + receive() external payable { + revert(); + } +} + contract MultiBridgeMessageSenderTest is Setup { using MessageLibrary for MessageLibrary.Message; @@ -39,6 +45,8 @@ contract MultiBridgeMessageSenderTest is Setup { address wormholeAdapterAddr; address axelarAdapterAddr; + address revertingReceiver; + /// @dev initializes the setup function setUp() public override { super.setUp(); @@ -49,6 +57,8 @@ contract MultiBridgeMessageSenderTest is Setup { senderGAC = MessageSenderGAC(contractAddress[SRC_CHAIN_ID]["GAC"]); wormholeAdapterAddr = contractAddress[SRC_CHAIN_ID]["WORMHOLE_SENDER_ADAPTER"]; axelarAdapterAddr = contractAddress[SRC_CHAIN_ID]["AXELAR_SENDER_ADAPTER"]; + + revertingReceiver = address(new EthReceiverRevert()); } /// @dev constructor @@ -110,6 +120,36 @@ contract MultiBridgeMessageSenderTest is Setup { assertEq(sender.nonce(), 1); } + /// @dev perform remote call with invalid refund receiver + function test_remote_call_reverting_refund_receiver() public { + vm.startPrank(caller); + + // Wormhole requires exact fees to be passed in + (uint256 wormholeFee,) = IWormholeRelayer(POLYGON_RELAYER).quoteEVMDeliveryPrice( + _wormholeChainId(DST_CHAIN_ID), 0, senderGAC.msgDeliveryGasLimit() + ); + (, uint256[] memory fees) = + _sortTwoAdaptersWithFees(axelarAdapterAddr, wormholeAdapterAddr, 0.01 ether, wormholeFee); + bool[] memory adapterSuccess = new bool[](2); + adapterSuccess[0] = true; + adapterSuccess[1] = true; + + uint256 expiration = EXPIRATION_CONSTANT; + + vm.expectRevert("safeTransferETH: ETH transfer failed"); + sender.remoteCall{value: fees[0] + fees[1] + 1 ether}( + DST_CHAIN_ID, + address(42), + bytes("42"), + 0, + expiration, + revertingReceiver, + fees, + DEFAULT_SUCCESS_THRESHOLD, + new address[](0) + ); + } + /// @dev perform remote call, checking for refund function test_remote_call_refund() public { vm.startPrank(caller); diff --git a/test/unit-tests/adapters/axelar/libraries/StringAddressConversion.t.sol b/test/unit-tests/adapters/axelar/libraries/StringAddressConversion.t.sol new file mode 100644 index 0000000..8cea2b2 --- /dev/null +++ b/test/unit-tests/adapters/axelar/libraries/StringAddressConversion.t.sol @@ -0,0 +1,117 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity >=0.8.9; + +/// library imports +import {Test, Vm} from "forge-std/Test.sol"; + +/// local imports +import "src/adapters/axelar/libraries/StringAddressConversion.sol"; + +/// @dev helper for testing StringAddressConversion library +/// @dev library testing using foundry can only be done through helper contracts +/// @dev see https://github.com/foundry-rs/foundry/issues/2567 +contract StringAddressConversionTestClient { + function toString(address _addr) external pure returns (string memory) { + return StringAddressConversion.toString(_addr); + } + + function toAddress(string calldata _addressString) external pure returns (address) { + return StringAddressConversion.toAddress(_addressString); + } +} + +contract StringAddressConversionTest is Test { + StringAddressConversionTestClient public conversionHelper; + + /*/////////////////////////////////////////////////////////////// + SETUP + //////////////////////////////////////////////////////////////*/ + function setUp() public { + conversionHelper = new StringAddressConversionTestClient(); + } + + /*/////////////////////////////////////////////////////////////// + TEST CASES + //////////////////////////////////////////////////////////////*/ + + /// @dev tests conversion of address to string + function test_to_string() public { + address testAddr = address(0x1234567890123456789012345678901234567890); + string memory result = conversionHelper.toString(testAddr); + string memory expected = "0x1234567890123456789012345678901234567890"; + + assertEq(keccak256(bytes(result)), keccak256(bytes(expected))); + } + + /// @dev tests conversion of string to address + function test_to_address() public { + string memory testString = "0x1234567890123456789012345678901234567890"; + address result = conversionHelper.toAddress(testString); + address expected = address(0x1234567890123456789012345678901234567890); + + assertEq(result, expected); + } + + /// @dev tests invalid address conversion + function test_invalid_address_string_conversion() public { + string memory invalidAddressString = "1234567890123456789012345678901234567892"; + + bytes4 selector = StringAddressConversion.InvalidAddressString.selector; + vm.expectRevert(selector); + conversionHelper.toAddress(invalidAddressString); + } + + /// @dev tests short address string + function test_short_address_string_conversion() public { + string memory shortAddressString = "0x12345678901234567890123456789012345678"; + + bytes4 selector = StringAddressConversion.InvalidAddressString.selector; + vm.expectRevert(selector); + conversionHelper.toAddress(shortAddressString); + } + + /// @dev tests long address string + function test_long_address_string_conversion() public { + string memory longAddressString = "0x123456789012345678901234567890123456789012"; + + bytes4 selector = StringAddressConversion.InvalidAddressString.selector; + vm.expectRevert(selector); + conversionHelper.toAddress(longAddressString); + } + + /// @dev tests invalid prefix in address string + function test_invalid_prefix_address_string_conversion() public { + string memory invalidPrefixAddressString = "1x1234567890123456789012345678901234567890"; + + bytes4 selector = StringAddressConversion.InvalidAddressString.selector; + vm.expectRevert(selector); + conversionHelper.toAddress(invalidPrefixAddressString); + } + + /// @dev tests address string with invalid characters + function test_invalid_character_address_string_conversion() public { + string memory invalidCharacterAddressString = "0x12345678901234567890123456789012345678g0"; // 'g' is an invalid character + + bytes4 selector = StringAddressConversion.InvalidAddressString.selector; + vm.expectRevert(selector); + conversionHelper.toAddress(invalidCharacterAddressString); + } + + /// @dev tests conversion of string with lowercase hex characters to address + function test_lowercase_hex_character_to_address() public { + string memory testString = "0xabcdefabcdefabcdefabcdefabcdefabcdefabcd"; + address result = conversionHelper.toAddress(testString); + address expected = address(0xABcdEFABcdEFabcdEfAbCdefabcdeFABcDEFabCD); + + assertEq(result, expected); + } + + /// @dev tests conversion of string with uppercase hex characters to address + function test_ppercase_hex_character_to_address() public { + string memory testString = "0xABCDEFABCDEFABCDEFABCDEFABCDEFABCDEFABCD"; + address result = conversionHelper.toAddress(testString); + address expected = address(0xABcdEFABcdEFabcdEfAbCdefabcdeFABcDEFabCD); + + assertEq(result, expected); + } +} diff --git a/test/unit-tests/libraries/EIP5164/ExecutorAware.t.sol b/test/unit-tests/libraries/EIP5164/ExecutorAware.t.sol new file mode 100644 index 0000000..4fe9bae --- /dev/null +++ b/test/unit-tests/libraries/EIP5164/ExecutorAware.t.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity >=0.8.9; + +/// library imports +import {Test, Vm} from "forge-std/Test.sol"; + +/// local imports +import "src/libraries/EIP5164/ExecutorAware.sol"; + +/// @dev helper to test abstract contract +contract ExecutorAwareTestClient is ExecutorAware { + function addTrustedExecutor(address _executor) external returns (bool) { + return _addTrustedExecutor(_executor); + } + + function removeTrustedExecutor(address _executor) external returns (bool) { + return _removeTrustedExecutor(_executor); + } +} + +contract ExecutorAwareTest is Test { + ExecutorAwareTestClient public executorAware; + + /*/////////////////////////////////////////////////////////////// + SETUP + //////////////////////////////////////////////////////////////*/ + function setUp() public { + executorAware = new ExecutorAwareTestClient(); + } + + /*/////////////////////////////////////////////////////////////// + TEST CASES + //////////////////////////////////////////////////////////////*/ + + /// @dev tests adding a trusted executor + function test_add_trusted_executor() public { + address executor = address(0x1234567890123456789012345678901234567890); + bool added = executorAware.addTrustedExecutor(executor); + + assertEq(added, true); + assertEq(executorAware.isTrustedExecutor(executor), true); + } + + /// @dev tests removing a trusted executor + function test_remove_trusted_executor() public { + address executor = address(0x1234567890123456789012345678901234567890); + executorAware.addTrustedExecutor(executor); + + bool removed = executorAware.removeTrustedExecutor(executor); + + assertEq(removed, true); + assertEq(executorAware.isTrustedExecutor(executor), false); + } + + /// @dev tests retrieval of trusted executors + function test_get_trusted_executors() public { + address executor1 = address(420); + address executor2 = address(421); + executorAware.addTrustedExecutor(executor1); + executorAware.addTrustedExecutor(executor2); + + address[] memory executors = executorAware.getTrustedExecutors(); + + assertEq(executors.length == 2, true); + assertEq(executors[0], executor1); + assertEq(executors[1], executor2); + } + + /// @dev tests counting the number of trusted executors + function test_trusted_executors_count() public { + address executor1 = address(420); + address executor2 = address(421); + executorAware.addTrustedExecutor(executor1); + executorAware.addTrustedExecutor(executor2); + + uint256 count = executorAware.trustedExecutorsCount(); + + assertEq(count, 2); + } +} diff --git a/test/unit-tests/libraries/Message.t.sol b/test/unit-tests/libraries/Message.t.sol new file mode 100644 index 0000000..cd37884 --- /dev/null +++ b/test/unit-tests/libraries/Message.t.sol @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity >=0.8.9; + +/// library imports +import {Test, Vm} from "forge-std/Test.sol"; + +/// local imports +import "src/libraries/Message.sol"; + +/// @dev is a helper function to test library contracts +/// @dev library testing using foundry can only be done through helper contracts +/// @dev see https://github.com/foundry-rs/foundry/issues/2567 +contract MessageLibraryTestClient { + using MessageLibrary for MessageLibrary.Message; + + function computeMsgId(MessageLibrary.Message memory _message) external pure returns (bytes32) { + return _message.computeMsgId(); + } + + function extractExecutionParams(MessageLibrary.Message memory _message) + external + pure + returns (MessageLibrary.MessageExecutionParams memory) + { + return _message.extractExecutionParams(); + } + + function computeExecutionParamsHash(MessageLibrary.MessageExecutionParams memory _params) + external + pure + returns (bytes32) + { + return MessageLibrary.computeExecutionParamsHash(_params); + } + + function computeExecutionParamsHashFromMessage(MessageLibrary.Message memory _message) + external + pure + returns (bytes32) + { + return _message.computeExecutionParamsHash(); + } +} + +contract MessageLibraryTest is Test { + MessageLibraryTestClient messageLibraryTestClient; + + function setUp() public { + messageLibraryTestClient = new MessageLibraryTestClient(); + } + + /// @dev tests computation of message id + function test_compute_msg_id() public { + // convert the string literal to bytes constant + bytes memory callDataBytes = hex"abcdef"; + + MessageLibrary.Message memory message = MessageLibrary.Message({ + srcChainId: 1, + dstChainId: 2, + target: address(0x1234567890123456789012345678901234567890), + nonce: 123, + callData: callDataBytes, + nativeValue: 456, + expiration: 10000 + }); + + bytes32 computedId = messageLibraryTestClient.computeMsgId(message); + + // Update the expectedId calculation to use the bytes constant + bytes32 expectedId = keccak256( + abi.encodePacked( + uint256(1), + uint256(2), + uint256(123), + address(0x1234567890123456789012345678901234567890), + uint256(456), + uint256(10000), + callDataBytes + ) + ); + + assertEq(computedId, expectedId); + } + + /// @dev tests extraction of execution parameters from a message + function test_extract_execution_params() public { + MessageLibrary.Message memory message = MessageLibrary.Message({ + srcChainId: 1, + dstChainId: 2, + target: address(0x1234567890123456789012345678901234567890), + nonce: 123, + callData: hex"abcdef", + nativeValue: 456, + expiration: 10000 + }); + + MessageLibrary.MessageExecutionParams memory params = messageLibraryTestClient.extractExecutionParams(message); + + assertEq(params.target, address(0x1234567890123456789012345678901234567890)); + assertEq(keccak256(params.callData), keccak256(hex"abcdef")); + assertEq(params.value, 456); + assertEq(params.nonce, 123); + assertEq(params.expiration, 10000); + } + + /// @dev tests computation of execution parameters hash directly from parameters + function test_compute_execution_params_hash() public { + MessageLibrary.MessageExecutionParams memory params = MessageLibrary.MessageExecutionParams({ + target: address(0x1234567890123456789012345678901234567890), + callData: hex"abcdef", + value: 456, + nonce: 123, + expiration: 10000 + }); + + bytes32 computedHash = messageLibraryTestClient.computeExecutionParamsHash(params); + bytes32 expectedHash = keccak256( + abi.encodePacked( + address(0x1234567890123456789012345678901234567890), + hex"abcdef", + uint256(456), + uint256(123), + uint256(10000) + ) + ); + + assertEq(computedHash, expectedHash); + } + + /// @dev tests computation of execution parameters hash from a message + function test_compute_execution_params_hash_from_message() public { + MessageLibrary.Message memory message = MessageLibrary.Message({ + srcChainId: 1, + dstChainId: 2, + target: address(0x1234567890123456789012345678901234567890), + nonce: 123, + callData: hex"abcdef", + nativeValue: 456, + expiration: 10000 + }); + + bytes32 computedHash = messageLibraryTestClient.computeExecutionParamsHashFromMessage(message); + bytes32 expectedHash = keccak256( + abi.encodePacked( + address(0x1234567890123456789012345678901234567890), + hex"abcdef", + uint256(456), + uint256(123), + uint256(10000) + ) + ); + + assertEq(computedHash, expectedHash); + } +} diff --git a/test/unit-tests/libraries/TypeCasts.t.sol b/test/unit-tests/libraries/TypeCasts.t.sol new file mode 100644 index 0000000..483fe5d --- /dev/null +++ b/test/unit-tests/libraries/TypeCasts.t.sol @@ -0,0 +1,55 @@ +// SPDX-License-Identifier: GPL-3.0-only +pragma solidity >=0.8.9; + +/// library imports +import {Test, Vm} from "forge-std/Test.sol"; + +/// local imports +import "src/libraries/TypeCasts.sol"; + +/// @dev helper to test TypeCasts library +/// @dev library testing using foundry can only be done through helper contracts +/// @dev see https://github.com/foundry-rs/foundry/issues/2567 +contract TypeCastsLibraryTestClient { + function addressToBytes32(address _addr) external pure returns (bytes32) { + return TypeCasts.addressToBytes32(_addr); + } + + function bytes32ToAddress(bytes32 _buf) external pure returns (address) { + return TypeCasts.bytes32ToAddress(_buf); + } +} + +contract TypeCastsTest is Test { + TypeCastsLibraryTestClient public typeCastsLibraryTestClient; + + /*/////////////////////////////////////////////////////////////// + SETUP + //////////////////////////////////////////////////////////////*/ + function setUp() public { + typeCastsLibraryTestClient = new TypeCastsLibraryTestClient(); + } + + /*/////////////////////////////////////////////////////////////// + TEST CASES + //////////////////////////////////////////////////////////////*/ + + /// @dev tests conversion of address to bytes32 + function test_address_to_bytes32() public { + address testAddr = address(0x1234567890123456789012345678901234567890); + bytes32 expected = bytes32(uint256(uint160(testAddr))); // Correct casting here + bytes32 result = typeCastsLibraryTestClient.addressToBytes32(testAddr); + + assertEq(result, expected); + } + + /// @dev tests conversion of bytes32 to address + function test_bytes32_to_address() public { + bytes32 testBytes = bytes32(uint256(uint160(0x1234567890123456789012345678901234567890))); + + address expected = address(uint160(uint256(testBytes))); // Correct casting here + address result = typeCastsLibraryTestClient.bytes32ToAddress(testBytes); + + assertEq(result, expected); + } +}