diff --git a/src/wormhole/automatic-relayer/WormholeHelper.sol b/src/wormhole/automatic-relayer/WormholeHelper.sol index c66733b..b449587 100644 --- a/src/wormhole/automatic-relayer/WormholeHelper.sol +++ b/src/wormhole/automatic-relayer/WormholeHelper.sol @@ -24,12 +24,20 @@ interface IWormholeReceiver { ) external payable; } +interface IMessageTransmitter { + function attesterManager() external view returns (address); + function enableAttester(address newAttester) external; + function setSignatureThreshold(uint256 newSignatureThreshold) external; + function receiveMessage(bytes calldata message, bytes calldata attestation) external; +} + /// @title WormholeHelper /// @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 bytes32 constant MESSAGE_EVENT_SELECTOR = 0x6eb224fb001ed210e379b335e35efe88672a8ce935d981a6896b27ffdf52a3b2; + bytes32 constant CCTP_MESSAGE_EVENT_SELECTOR = 0x8c5261668696ce22758910d05bab8f186d6eb247ceac2af2e82c7dc17669b036; ////////////////////////////////////////////////////////////// // EXTERNAL FUNCTIONS // @@ -128,6 +136,89 @@ contract WormholeHelper is Test { } } + struct LocalCCTPVars { + uint256 prevForkId; + bytes cctpMessage; + bytes[] additionalMessage; + bytes32 digest; + uint8 v; + bytes32 r; + bytes32 s; + Vm.Log log; + uint64 sequence; + uint32 nonce; + bytes payload; + address dstAddress; + } + + /// @dev is a helper for https://docs.wormhole.com/wormhole/quick-start/tutorials/cctp + /// @param srcChainId represents the wormhole identifier for the source chain + /// @param dstForkId represents the dst fork id to deliver the message + /// @param expDstAddress represents the expected dst chain receiver of wormhole message + /// @param dstRelayer represents the wormhole dst relayer address + /// @param dstTransmitter represents the cctp dst transmitter address + /// @param logs represents the logs after message dispatch using sendToEvm + /// @notice supports only one CCTP transfer and sendToEvm per log + function helpWithCctpAndWormhole( + uint16 srcChainId, + uint256 dstForkId, + address expDstAddress, + address dstRelayer, + address dstTransmitter, + Vm.Log[] calldata logs + ) external { + LocalCCTPVars memory v; + v.prevForkId = vm.activeFork(); + v.additionalMessage = new bytes[](1); + vm.selectFork(dstForkId); + + /// @dev identifies the cctp transfer + for (uint256 i; i < logs.length; ++i) { + v.log = logs[i]; + if (v.log.topics[0] == CCTP_MESSAGE_EVENT_SELECTOR) { + v.cctpMessage = abi.decode(logs[i].data, (bytes)); + /// @dev prepare circle transmitter on dst chain + IMessageTransmitter messageTransmitter = IMessageTransmitter(dstTransmitter); + + vm.startPrank(messageTransmitter.attesterManager()); + messageTransmitter.enableAttester(vm.addr(420)); + messageTransmitter.setSignatureThreshold(1); + vm.stopPrank(); + + v.digest = keccak256(v.cctpMessage); + (v.v, v.r, v.s) = vm.sign(420, v.digest); + v.additionalMessage[0] = abi.encode(v.cctpMessage, abi.encodePacked(v.r, v.s, v.v)); + } + } + + /// @dev identifies and delivers the wormhole message + vm.startBroadcast(dstRelayer); + for (uint256 j; j < logs.length; ++j) { + v.log = logs[j]; + + if (v.log.topics[0] == MESSAGE_EVENT_SELECTOR) { + (v.sequence, v.nonce, v.payload,) = abi.decode(v.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.additionalMessage, + instruction.senderAddress, + srcChainId, + /// @dev generating some random hash + keccak256(abi.encodePacked(v.sequence, v.nonce)) + ); + } + } + } + vm.stopBroadcast(); + vm.selectFork(v.prevForkId); + } + /// @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 diff --git a/test/Wormhole.AutomaticRelayer.t.sol b/test/Wormhole.AutomaticRelayer.t.sol index 6a9ba87..000677d 100644 --- a/test/Wormhole.AutomaticRelayer.t.sol +++ b/test/Wormhole.AutomaticRelayer.t.sol @@ -7,8 +7,14 @@ import "forge-std/Test.sol"; /// local imports import "src/wormhole/automatic-relayer/WormholeHelper.sol"; import "src/wormhole/specialized-relayer/lib/IWormhole.sol"; +import "solady/src/tokens/ERC20.sol"; interface IWormholeRelayerSend { + struct MessageKey { + uint8 keyType; + bytes encodedKey; + } + function sendPayloadToEvm( uint16 targetChain, address targetAddress, @@ -36,14 +42,40 @@ interface IWormholeRelayerSend { VaaKey[] memory vaaKeys ) external payable returns (uint64 sequence); + function sendToEvm( + uint16 targetChain, + address targetAddress, + bytes memory payload, + uint256 receiverValue, + uint256 paymentForExtraReceiverValue, + uint256 gasLimit, + uint16 refundChain, + address refundAddress, + address deliveryProviderAddress, + MessageKey[] memory messageKeys, + uint8 consistencyLevel + ) external payable returns (uint64 sequence); + function quoteEVMDeliveryPrice(uint16 targetChain, uint256 receiverValue, uint256 gasLimit) external view returns (uint256 nativePriceQuote, uint256 targetChainRefundPerGasUnused); + + function getDefaultDeliveryProvider() external view returns (address); } interface IWormholeRelayer is IWormholeRelayerSend {} +interface ITokenManager { + function depositForBurnWithCaller( + uint256 amount, + uint32 destinationDomain, + bytes32 mintRecipient, + address burnToken, + bytes32 destinationCaller + ) external returns (uint64 nonce); +} + contract Target is IWormholeReceiver { uint256 public value; @@ -97,11 +129,35 @@ contract AnotherTarget { } } +contract CCTPTarget { + IMessageTransmitter transmitter; + + constructor(IMessageTransmitter transmitter_) { + transmitter = transmitter_; + } + + function receiveWormholeMessages( + bytes memory payload, + bytes[] memory additionalVaas, + bytes32 sourceAddress, + uint16 sourceChain, + bytes32 deliveryHash + ) external payable { + (bytes memory message, bytes memory attestation) = abi.decode(additionalVaas[0], (bytes, bytes)); + transmitter.receiveMessage(message, attestation); + } +} + contract WormholeAutomaticRelayerHelperTest is Test { IWormhole wormhole = IWormhole(0x98f3c9e6E3fAce36bAAd05FE09d375Ef1464288B); + ITokenManager tokenMessenger = ITokenManager(0xBd3fa81B58Ba92a82136038B25aDec7066af3155); + ERC20 USDC = ERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48); + ERC20 USDC_POLYGON = ERC20(0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359); + WormholeHelper wormholeHelper; Target target; Target altTarget; + CCTPTarget cctpTarget; AnotherTarget anotherTarget; AdditionalVAATarget addVaaTarget; @@ -120,6 +176,8 @@ contract WormholeAutomaticRelayerHelperTest is Test { address constant L2_1_RELAYER = 0x27428DD2d3DD32A4D7f7C497eAaa23130d894911; address constant L2_2_RELAYER = 0x27428DD2d3DD32A4D7f7C497eAaa23130d894911; + address constant MESSAGE_TRANSMITTER_POLYGON = 0xF3be9355363857F3e001be68856A2f96b4C39Ba9; + address[] public allDstRelayers; uint16[] public allDstChainIds; uint256[] public allDstForks; @@ -139,6 +197,7 @@ contract WormholeAutomaticRelayerHelperTest is Test { target = new Target(); addVaaTarget = new AdditionalVAATarget(); anotherTarget = new AnotherTarget(L1_CHAIN_ID); + cctpTarget = new CCTPTarget(IMessageTransmitter(MESSAGE_TRANSMITTER_POLYGON)); ARBITRUM_FORK_ID = vm.createSelectFork(RPC_ARBITRUM_MAINNET, 38063686); altTarget = new Target(); @@ -258,6 +317,61 @@ contract WormholeAutomaticRelayerHelperTest is Test { assertEq(addVaaTarget.vaalen(), 1); } + /// @dev test single dst cctp transfers with wormhole + function testCctpWormhole() external { + vm.selectFork(L1_FORK_ID); + address bridgoor = address(32145); + + vm.deal(bridgoor, 2 ether); + deal(address(USDC), bridgoor, 100e6); + vm.startPrank(bridgoor); + + USDC.approve(address(tokenMessenger), 100e6); + + vm.recordLogs(); + uint64 nonce = tokenMessenger.depositForBurnWithCaller( + 100e6, + 7, + bytes32(uint256(uint160(address(cctpTarget)))), + address(USDC), + bytes32(uint256(uint160(address(cctpTarget)))) + ); + + IWormholeRelayer relayer = IWormholeRelayer(L1_RELAYER); + + IWormholeRelayerSend.MessageKey[] memory messageKeys = new IWormholeRelayerSend.MessageKey[](1); + messageKeys[0] = IWormholeRelayerSend.MessageKey(2, abi.encodePacked(uint32(7), nonce)); + + (uint256 msgValue,) = relayer.quoteEVMDeliveryPrice(L2_1_CHAIN_ID, 0, 500000); + + relayer.sendToEvm{value: msgValue}( + L2_1_CHAIN_ID, + address(cctpTarget), + bytes(""), + 0, + 0, + 500000, + L2_1_CHAIN_ID, + address(0), + relayer.getDefaultDeliveryProvider(), + messageKeys, + 1 + ); + + wormholeHelper.helpWithCctpAndWormhole( + L1_CHAIN_ID, + POLYGON_FORK_ID, + address(cctpTarget), + L2_1_RELAYER, + 0xF3be9355363857F3e001be68856A2f96b4C39Ba9, + vm.getRecordedLogs() + ); + vm.stopPrank(); + + vm.selectFork(POLYGON_FORK_ID); + assertEq(USDC_POLYGON.balanceOf(address(cctpTarget)), 100e6); + } + function _aMostFancyCrossChainFunctionInYourContract(uint16 dstChainId, address receiver) internal { IWormholeRelayer relayer = IWormholeRelayer(L1_RELAYER);