From e312cb0dcb54f23ac4348a7c592683a49464d90c Mon Sep 17 00:00:00 2001 From: sujithsomraaj Date: Wed, 22 May 2024 12:46:02 +0530 Subject: [PATCH] chore: add additionalVAA support --- .../automatic-relayer/WormholeHelper.sol | 304 +++++++++++++++--- test/Wormhole.AutomaticRelayer.t.sol | 72 +++++ 2 files changed, 338 insertions(+), 38 deletions(-) diff --git a/src/wormhole/automatic-relayer/WormholeHelper.sol b/src/wormhole/automatic-relayer/WormholeHelper.sol index e6e0083..297d130 100644 --- a/src/wormhole/automatic-relayer/WormholeHelper.sol +++ b/src/wormhole/automatic-relayer/WormholeHelper.sol @@ -4,11 +4,13 @@ pragma solidity >=0.8.0; /// library imports import "forge-std/Test.sol"; -/// local imports +/// bridge specific imports import "./lib/PayloadDecoder.sol"; import "./lib/InternalStructs.sol"; +import "../specialized-relayer/lib/IWormhole.sol"; import {TypeCasts} from "../../libraries/TypeCasts.sol"; +import "forge-std/console.sol"; /// @dev interface that every wormhole receiver should implement /// @notice the helper will try to deliver the message to this interface @@ -23,30 +25,41 @@ interface IWormholeReceiver { } /// @title WormholeHelper +/// @author Sujith Somraaj +/// @dev wormhole bridge helper /// @notice supports only automatic relayer (not specialized relayers) /// MORE INFO: https://docs.wormhole.com/wormhole/quick-start/cross-chain-dev/automatic-relayer contract WormholeHelper is Test { - /// @dev is the default event selector if not specified by the user + /*/////////////////////////////////////////////////////////////// + CONSTANTS + //////////////////////////////////////////////////////////////*/ + + /// @dev LogMessagePublished (index_topic_1 address sender, uint64 sequence, uint32 nonce, bytes payload, uint8 consistencyLevel) bytes32 constant MESSAGE_EVENT_SELECTOR = 0x6eb224fb001ed210e379b335e35efe88672a8ce935d981a6896b27ffdf52a3b2; - ////////////////////////////////////////////////////////////// - // EXTERNAL FUNCTIONS // - ////////////////////////////////////////////////////////////// + /*/////////////////////////////////////////////////////////////// + STATE VARIABLES + //////////////////////////////////////////////////////////////*/ + uint256[] public indicesCache; + + /*/////////////////////////////////////////////////////////////// + EXTERNAL FUNCTIONS + //////////////////////////////////////////////////////////////*/ /// @dev single dst x default event selector - /// @param srcChainId represents the wormhole identifier of the source chain - /// @param dstForkId represents the dst fork id to deliver the message - /// @param dstRelayer represents wormhole's dst chain relayer - /// @param logs represents the logs after message dispatch on src chain + /// @param srcChainId is the wormhole identifier of the source chain + /// @param dstForkId is the dst fork id to deliver the message + /// @param dstRelayer is wormhole's dst chain relayer + /// @param logs is the logs after message dispatch on src chain function help(uint16 srcChainId, uint256 dstForkId, address dstRelayer, Vm.Log[] calldata logs) external { _help(srcChainId, dstForkId, address(0), dstRelayer, MESSAGE_EVENT_SELECTOR, logs); } /// @dev single dst x user-specific event selector - /// @param srcChainId represents the wormhole identifier of the source chain - /// @param dstForkId represents the dst fork id to deliver the message - /// @param dstRelayer represents wormhole's dst chain relayer - /// @param logs represents the logs after message dispatch on src chain + /// @param srcChainId is the wormhole identifier of the source chain + /// @param dstForkId is the dst fork id to deliver the message + /// @param dstRelayer is wormhole's dst chain relayer + /// @param logs is the logs after message dispatch on src chain function help( uint16 srcChainId, uint256 dstForkId, @@ -58,10 +71,10 @@ contract WormholeHelper is Test { } /// @dev multi dst x default event selector - /// @param srcChainId represents the wormhole identifier of the source chain - /// @param dstForkId represents the dst fork id to deliver the message - /// @param dstRelayer represents wormhole's dst chain relayer - /// @param logs represents the logs after message dispatch on src chain + /// @param srcChainId is the wormhole identifier of the source chain + /// @param dstForkId is the dst fork id to deliver the message + /// @param dstRelayer is wormhole's dst chain relayer + /// @param logs is the logs after message dispatch on src chain function help( uint16 srcChainId, uint256[] calldata dstForkId, @@ -79,10 +92,10 @@ contract WormholeHelper is Test { } /// @dev multi dst x user-specific event selector - /// @param srcChainId represents the wormhole identifier of the source chain - /// @param dstForkId represents the dst fork id to deliver the message - /// @param dstRelayer represents wormhole's dst chain relayer - /// @param logs represents the logs after message dispatch on src chain + /// @param srcChainId is the wormhole identifier of the source chain + /// @param dstForkId is the dst fork id to deliver the message + /// @param dstRelayer is wormhole's dst chain relayer + /// @param logs is the logs after message dispatch on src chain function help( uint16 srcChainId, uint256[] calldata dstForkId, @@ -100,17 +113,40 @@ contract WormholeHelper is Test { } } + /// @dev multi dst with additionalVAAs (use this for single dst as well) + /// @param srcChainId is the wormhole identifier for the source chain + /// @param dstForkId is the dst fork id to deliver the message + /// @param expDstAddress is the expected dst chain receiver + /// @param dstRelayer is the wormhole dst relayer address + /// @param dstWormhole is the wormhole dst core address + /// @param logs is the logs after message dispatch with additional VAAs + /// @notice considers the expectedDst to be unique in the logs (i.e., only supports one log for expectedDst per chain) + function helpWithAdditionalVAA( + uint16 srcChainId, + uint256[] calldata dstForkId, + address[] calldata expDstAddress, + address[] calldata dstRelayer, + address[] calldata dstWormhole, + Vm.Log[] calldata logs + ) external { + for (uint256 i; i < dstForkId.length;) { + _helpWithAddtionalVAAs( + srcChainId, dstForkId[i], expDstAddress[i], dstRelayer[i], dstWormhole[i], MESSAGE_EVENT_SELECTOR, logs + ); + unchecked { + ++i; + } + } + } + /// @dev helps find logs of `length` for default event selector - /// @param logs represents the logs after message dispatch on src chain - /// @param length represents the expected number of logs - /// @return HLLogs array of found logs function findLogs(Vm.Log[] calldata logs, uint256 length) external pure returns (Vm.Log[] memory HLLogs) { return _findLogs(logs, MESSAGE_EVENT_SELECTOR, length); } - ////////////////////////////////////////////////////////////// - // INTERNAL FUNCTIONS // - ////////////////////////////////////////////////////////////// + /*/////////////////////////////////////////////////////////////// + INTERNAL/HELPER FUNCTIONS + //////////////////////////////////////////////////////////////*/ struct LocalVars { uint256 prevForkId; @@ -118,15 +154,22 @@ contract WormholeHelper is Test { uint32 nonce; bytes payload; address dstAddress; + bytes[] additionalVAAs; + uint256 currIndex; + uint256 deliveryIndex; + DeliveryInstruction instruction; + } + + struct PrepareDeliverVars { + uint256 prevForkId; + uint64 sequence; + uint32 nonce; + bytes payload; + uint8 consistencyLevel; + address dstAddress; } /// @dev helper to process cross-chain messages - /// @param srcChainId represents the wormhole identifier of the source chain - /// @param dstForkId represents the dst fork id to deliver the message - /// @param expDstAddress represents the expected destination address - /// @param dstRelayer represents wormhole's dst chain relayer - /// @param eventSelector represents the event selector - /// @param logs represents the logs after message dispatch on src chain function _help( uint16 srcChainId, uint256 dstForkId, @@ -157,9 +200,9 @@ contract WormholeHelper is Test { new bytes[](0), instruction.senderAddress, srcChainId, + /// @dev generating some random hash keccak256(abi.encodePacked(v.sequence, v.nonce)) ); - /// @dev generating some random hash } } } @@ -168,11 +211,77 @@ contract WormholeHelper is Test { vm.selectFork(v.prevForkId); } + function _helpWithAddtionalVAAs( + uint16 srcChainId, + uint256 dstForkId, + address expDstAddress, + address dstRelayer, + address dstWormhole, + bytes32 eventSelector, + Vm.Log[] calldata logs + ) internal { + LocalVars memory v; + v.prevForkId = vm.activeFork(); + + vm.selectFork(dstForkId); + vm.startBroadcast(dstRelayer); + + Vm.Log memory log; + console.log("Total Log Length:", logs.length); + + /// @dev calculates the valid indices + for (uint256 i; i < logs.length; i++) { + log = logs[i]; + if (log.topics[0] == eventSelector) indicesCache.push(i); + } + + /// @dev if valid indices > 1, then it has additional VAAs to be delivered + /// @dev constructs the additional VAAs in that case + v.additionalVAAs = new bytes[](indicesCache.length - 1); + v.currIndex; + + console.log("Total matching VAAs:", indicesCache.length); + + if (indicesCache.length > 1 && expDstAddress != address(0)) { + for (uint256 j; j < indicesCache.length; j++) { + log = logs[indicesCache[j]]; + + if (TypeCasts.bytes32ToAddress(log.topics[1]) != dstRelayer) { + v.additionalVAAs[v.currIndex] = _generateSignedVAA(srcChainId, dstWormhole, log.topics[1], log.data); + v.currIndex++; + } else { + v.deliveryIndex = indicesCache[j]; + } + } + } + + log = logs[v.currIndex == 0 ? indicesCache[0] : v.deliveryIndex]; + + (v.sequence, v.nonce, v.payload,) = abi.decode(log.data, (uint64, uint32, bytes, uint8)); + + DeliveryInstruction memory instruction = PayloadDecoder.decodeDeliveryInstruction(v.payload); + + v.dstAddress = TypeCasts.bytes32ToAddress(instruction.targetAddress); + + if (expDstAddress == address(0) || expDstAddress == v.dstAddress) { + IWormholeReceiver(v.dstAddress).receiveWormholeMessages( + instruction.payload, + v.additionalVAAs, + instruction.senderAddress, + srcChainId, + /// @dev generating some random hash + keccak256(abi.encodePacked(v.sequence, v.nonce)) + ); + } + + /// @dev reset indices cache + delete indicesCache; + + vm.stopBroadcast(); + vm.selectFork(v.prevForkId); + } + /// @dev helper to get logs - /// @param logs represents the logs after message dispatch on src chain - /// @param dispatchSelector represents the event selector - /// @param length represents the expected number of logs - /// @return WormholeLogs array of found logs function _findLogs(Vm.Log[] memory logs, bytes32 dispatchSelector, uint256 length) internal pure @@ -193,4 +302,123 @@ contract WormholeHelper is Test { } } } + + /// @dev prepares a valid VAA in the wormhole core by overriding the guardian set + /// @param srcChainId is the source chain + /// @param dstWormhole is the wormhole core on dst chain + /// @param emitter is the wormhole core on source chain + /// @param logData is the data emitted in the log + function _generateSignedVAA(uint16 srcChainId, address dstWormhole, bytes32 emitter, bytes memory logData) + internal + returns (bytes memory) + { + PrepareDeliverVars memory v; + + (v.sequence, v.nonce, v.payload, v.consistencyLevel) = abi.decode(logData, (uint64, uint32, bytes, uint8)); + + /// @dev overrides wormhole guardian set to a preferred set + _prepareWormhole(dstWormhole); + + bytes memory encodedVAA = _generateVAA( + srcChainId, + v.nonce, + TypeCasts.bytes32ToAddress(emitter), + v.sequence, + v.consistencyLevel, + v.payload, + dstWormhole + ); + + return encodedVAA; + } + + /// @dev generates the encoded vaa + /// @param srcChainId represents the wormhole identifier of the source chain + /// @param nonce represents the nonce of the message + /// @param emitterAddress represents the emitter address + /// @param sequence represents the sequence of the message + /// @param consistencyLevel represents the consistency level + /// @param payload represents the message payload + /// @param dstWormhole represents the wormhole core contract on dst chain + /// @return encodedVAA the encoded VAA + function _generateVAA( + uint16 srcChainId, + uint32 nonce, + address emitterAddress, + uint64 sequence, + uint8 consistencyLevel, + bytes memory payload, + address dstWormhole + ) internal view returns (bytes memory) { + IWormhole wormhole = IWormhole(dstWormhole); + + /// @dev generates vaa hash + IWormhole.VM memory vaa = IWormhole.VM( + uint8(1), + /// version = 1 + uint32(block.timestamp), + nonce, + srcChainId, + TypeCasts.addressToBytes32(emitterAddress), + sequence, + consistencyLevel, + payload, + wormhole.getCurrentGuardianSetIndex(), + new IWormhole.Signature[](19), + bytes32(0) + ); + + bytes memory body = abi.encodePacked( + vaa.timestamp, + vaa.nonce, + vaa.emitterChainId, + vaa.emitterAddress, + vaa.sequence, + vaa.consistencyLevel, + vaa.payload + ); + + vaa.hash = keccak256(abi.encodePacked(keccak256(body))); + uint256 lastKey = 420; + + for (uint256 i; i < 19; i++) { + (uint8 v, bytes32 r, bytes32 s) = vm.sign(lastKey, vaa.hash); + vaa.signatures[i] = IWormhole.Signature(r, s, v, uint8(i)); + ++lastKey; + } + + bytes memory encodedVaa = abi.encodePacked(vaa.version, vaa.guardianSetIndex, uint8(19)); + for (uint256 i; i < 19; i++) { + encodedVaa = abi.encodePacked( + encodedVaa, + vaa.signatures[i].guardianIndex, + vaa.signatures[i].r, + vaa.signatures[i].s, + vaa.signatures[i].v - 27 + ); + } + + return abi.encodePacked(encodedVaa, body); + } + + /// @dev overrides the guardian set by choosing slot + /// @notice overrides the current guardian set with a set of known guardian set + /// @param dstWormhole represents the wormhole core contract on dst chain + function _prepareWormhole(address dstWormhole) internal { + IWormhole wormhole = IWormhole(dstWormhole); + + uint32 currentGuardianSet = wormhole.getCurrentGuardianSetIndex(); + bytes32 guardianSetSlot = keccak256(abi.encode(currentGuardianSet, 2)); + uint256 numGuardians = uint256(vm.load(address(wormhole), guardianSetSlot)); + + bytes32 lastSlot = bytes32(uint256(keccak256(abi.encodePacked(guardianSetSlot)))); + uint256 lastKey = 420; + + /// @dev updates the storage slot to update the guardian set + for (uint256 i; i < numGuardians; i++) { + vm.store(address(wormhole), bytes32(lastSlot), TypeCasts.addressToBytes32(vm.addr(lastKey))); + lastSlot = bytes32(uint256(lastSlot) + 1); + ++lastKey; + } + } } diff --git a/test/Wormhole.AutomaticRelayer.t.sol b/test/Wormhole.AutomaticRelayer.t.sol index d93e0ec..6a9ba87 100644 --- a/test/Wormhole.AutomaticRelayer.t.sol +++ b/test/Wormhole.AutomaticRelayer.t.sol @@ -6,6 +6,7 @@ import "forge-std/Test.sol"; /// local imports import "src/wormhole/automatic-relayer/WormholeHelper.sol"; +import "src/wormhole/specialized-relayer/lib/IWormhole.sol"; interface IWormholeRelayerSend { function sendPayloadToEvm( @@ -26,6 +27,15 @@ interface IWormholeRelayerSend { address refundAddress ) external payable returns (uint64 sequence); + function sendVaasToEvm( + uint16 targetChain, + address targetAddress, + bytes memory payload, + uint256 receiverValue, + uint256 gasLimit, + VaaKey[] memory vaaKeys + ) external payable returns (uint64 sequence); + function quoteEVMDeliveryPrice(uint16 targetChain, uint256 receiverValue, uint256 gasLimit) external view @@ -48,6 +58,22 @@ contract Target is IWormholeReceiver { } } +contract AdditionalVAATarget is IWormholeReceiver { + uint256 public value; + uint256 public vaalen; + + function receiveWormholeMessages( + bytes memory payload, + bytes[] memory additionalVaas, + bytes32 sourceAddress, + uint16 sourceChain, + bytes32 deliveryHash + ) external payable { + value = abi.decode(payload, (uint256)); + vaalen = additionalVaas.length; + } +} + contract AnotherTarget { uint256 public value; address public kevin; @@ -72,11 +98,13 @@ contract AnotherTarget { } contract WormholeAutomaticRelayerHelperTest is Test { + IWormhole wormhole = IWormhole(0x98f3c9e6E3fAce36bAAd05FE09d375Ef1464288B); WormholeHelper wormholeHelper; Target target; Target altTarget; AnotherTarget anotherTarget; + AdditionalVAATarget addVaaTarget; uint256 L1_FORK_ID; uint256 POLYGON_FORK_ID; @@ -109,6 +137,7 @@ contract WormholeAutomaticRelayerHelperTest is Test { POLYGON_FORK_ID = vm.createSelectFork(RPC_POLYGON_MAINNET); target = new Target(); + addVaaTarget = new AdditionalVAATarget(); anotherTarget = new AnotherTarget(L1_CHAIN_ID); ARBITRUM_FORK_ID = vm.createSelectFork(RPC_ARBITRUM_MAINNET, 38063686); @@ -201,6 +230,49 @@ contract WormholeAutomaticRelayerHelperTest is Test { assertEq(altTarget.value(), CROSS_CHAIN_MESSAGE); } + /// @dev test multi-dst wormhole helper with additional VAAs + function testMultiDstWormholeWithAdditionalVAA() external { + vm.selectFork(L1_FORK_ID); + vm.recordLogs(); + + _aMostFancyCrossChainFunctionInYourContract(L2_1_CHAIN_ID, address(addVaaTarget)); + + uint256[] memory dstForkId = new uint256[](1); + dstForkId[0] = POLYGON_FORK_ID; + + address[] memory dstAddress = new address[](1); + dstAddress[0] = address(addVaaTarget); + + address[] memory dstRelayers = new address[](1); + dstRelayers[0] = L2_1_RELAYER; + + address[] memory dstWormhole = new address[](1); + dstWormhole[0] = 0x7A4B5a56256163F07b2C80A7cA55aBE66c4ec4d7; + + wormholeHelper.helpWithAdditionalVAA( + L1_CHAIN_ID, dstForkId, dstAddress, dstRelayers, dstWormhole, vm.getRecordedLogs() + ); + + vm.selectFork(POLYGON_FORK_ID); + assertEq(addVaaTarget.value(), CROSS_CHAIN_MESSAGE); + assertEq(addVaaTarget.vaalen(), 1); + } + + function _aMostFancyCrossChainFunctionInYourContract(uint16 dstChainId, address receiver) internal { + IWormholeRelayer relayer = IWormholeRelayer(L1_RELAYER); + + (uint256 msgValue,) = relayer.quoteEVMDeliveryPrice(dstChainId, 0, 500000); + + uint64 sequence = wormhole.publishMessage{value: wormhole.messageFee()}(0, abi.encode(CROSS_CHAIN_MESSAGE), 0); + + VaaKey[] memory vaaKeys = new VaaKey[](1); + vaaKeys[0] = VaaKey(L1_CHAIN_ID, TypeCasts.addressToBytes32(address(this)), sequence); + + relayer.sendVaasToEvm{value: msgValue}( + dstChainId, receiver, abi.encode(CROSS_CHAIN_MESSAGE), 0, 500000, vaaKeys + ); + } + function _aMoreFancyCrossChainFunctionInYourContract(uint16 dstChainId, address receiver) internal { IWormholeRelayer relayer = IWormholeRelayer(L1_RELAYER);