From 38630b6547fd5112fcbc31c8ab0770ecc877a46d Mon Sep 17 00:00:00 2001 From: derpy-duck <115193320+derpy-duck@users.noreply.github.com> Date: Tue, 29 Aug 2023 16:11:48 -0400 Subject: [PATCH 01/23] cctp support WIP --- src/Base.sol | 51 +++ src/CCTPAndTokenBridgeBase.sol | 112 +++++++ src/TokenBase.sol | 182 +++++++++++ src/WormholeRelayerSDK.sol | 220 +------------ .../CCTPInterfaces/IMessageTransmitter.sol | 27 ++ src/interfaces/CCTPInterfaces/IReceiver.sol | 33 ++ src/interfaces/CCTPInterfaces/IRelayer.sol | 71 +++++ .../CCTPInterfaces/ITokenMessenger.sol | 34 ++ src/interfaces/IWormholeRelayer.sol | 294 +++++++++++++----- 9 files changed, 720 insertions(+), 304 deletions(-) create mode 100644 src/Base.sol create mode 100644 src/CCTPAndTokenBridgeBase.sol create mode 100644 src/TokenBase.sol create mode 100644 src/interfaces/CCTPInterfaces/IMessageTransmitter.sol create mode 100644 src/interfaces/CCTPInterfaces/IReceiver.sol create mode 100644 src/interfaces/CCTPInterfaces/IRelayer.sol create mode 100644 src/interfaces/CCTPInterfaces/ITokenMessenger.sol diff --git a/src/Base.sol b/src/Base.sol new file mode 100644 index 0000000..3cc44c2 --- /dev/null +++ b/src/Base.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "./interfaces/IWormholeReceiver.sol"; +import "./interfaces/IWormholeRelayer.sol"; +import "./interfaces/IWormhole.sol"; +import "./Utils.sol"; + +abstract contract Base { + IWormholeRelayer public immutable wormholeRelayer; + IWormhole public immutable wormhole; + + mapping(bytes32 => bool) public seenDeliveryVaaHashes; + + address registrationOwner; + mapping(uint16 => bytes32) registeredSenders; + + constructor(address _wormholeRelayer, address _wormhole) { + wormholeRelayer = IWormholeRelayer(_wormholeRelayer); + wormhole = IWormhole(_wormhole); + registrationOwner = msg.sender; + } + + modifier onlyWormholeRelayer() { + require(msg.sender == address(wormholeRelayer), "Msg.sender is not Wormhole Relayer"); + _; + } + + modifier replayProtect(bytes32 deliveryHash) { + require(!seenDeliveryVaaHashes[deliveryHash], "Message already processed"); + seenDeliveryVaaHashes[deliveryHash] = true; + _; + } + + modifier isRegisteredSender(uint16 sourceChain, bytes32 sourceAddress) { + require(registeredSenders[sourceChain] == sourceAddress, "Not registered sender"); + _; + } + + /** + * Sets the registered address for 'sourceChain' to 'sourceAddress' + * So that for messages from 'sourceChain', only ones from 'sourceAddress' are valid + * + * Assumes only one sender per chain is valid + * Sender is the address that called 'send' on the Wormhole Relayer contract on the source chain) + */ + function setRegisteredSender(uint16 sourceChain, bytes32 sourceAddress) public { + require(msg.sender == registrationOwner, "Not allowed to set registered sender"); + registeredSenders[sourceChain] = sourceAddress; + } +} diff --git a/src/CCTPAndTokenBridgeBase.sol b/src/CCTPAndTokenBridgeBase.sol new file mode 100644 index 0000000..e825208 --- /dev/null +++ b/src/CCTPAndTokenBridgeBase.sol @@ -0,0 +1,112 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "./interfaces/IWormholeReceiver.sol"; +import "./interfaces/IWormholeRelayer.sol"; +import "./interfaces/ITokenBridge.sol"; +import {IERC20} from "./interfaces/IERC20.sol"; +import "./interfaces/CCTPInterfaces/ITokenMessenger.sol"; +import "./interfaces/CCTPInterfaces/IMessageTransmitter.sol"; + +import "./Utils.sol"; +import "./Base.sol"; + + + +abstract contract CCTPAndTokenBridgeBase is Base { + ITokenBridge immutable tokenBridge; + ITokenMessenger immutable circleTokenMessenger; + IMessageTransmitter immutable circleMessageTransmitter; + + constructor(address _wormholeRelayer, address _tokenBridge, address _wormhole, address _circleMessageTransmitter, address _circleTokenMessenger) Base(_wormholeRelayer, _wormhole) { + tokenBridge = ITokenBridge(_tokenBridge); + circleTokenMessenger = ITokenMessenger(_circleTokenMessenger); + circleMessageTransmitter = IMessageTransmitter(_circleMessageTransmitter); + } + + function getCCTPDomain(uint16 targetChain) internal pure returns (uint32) { + if(targetChain == 2) { + return 0; + } else if(targetChain == 6) { + return 1; + } else { // TODO: Add arbitrum and optimism { + revert("Wrong CCTP Domain"); + } + } + +} + + +abstract contract CCTPSender is CCTPAndTokenBridgeBase { + uint8 internal constant CONSISTENCY_LEVEL_FINALIZED = 15; + /** + * transferTokens wraps common boilerplate for sending tokens to another chain using IWormholeRelayer + * - approves tokenBridge to spend 'amount' of 'token' + * - emits token transfer VAA + * - returns VAA key for inclusion in WormholeRelayer `additionalVaas` argument + * + * Note: this requires that only the targetAddress can redeem transfers. + * + */ + function transferCircle(address token, uint256 amount, uint16 targetChain, address targetAddress) + internal + returns (MessageKey memory) + { + uint64 nonce = circleTokenMessenger.depositForBurnWithCaller( + amount, + getCCTPDomain(targetChain), + toWormholeFormat(targetAddress), + token, + toWormholeFormat(targetAddress) + ); + return MessageKey( + 2, abi.encodePacked(nonce, getCCTPDomain(wormhole.chainId())) // fix this encoding TODO! + ); + } + + function sendCircleWithPayloadToEvm( + uint16 targetChain, + address targetAddress, + bytes memory payload, + uint256 receiverValue, + uint256 gasLimit, + address token, + uint256 amount + ) internal returns (uint64) { + MessageKey[] memory messageKeys = new MessageKey[](1); + messageKeys[0] = transferCircle(token, amount, targetChain, targetAddress); + + (uint256 cost,) = wormholeRelayer.quoteEVMDeliveryPrice(targetChain, receiverValue, gasLimit); + return wormholeRelayer.sendToEvm{value: cost}( + targetChain, targetAddress, payload, receiverValue, 0, gasLimit, targetChain, address(0x0), wormholeRelayer.getDefaultDeliveryProvider(), messageKeys, CONSISTENCY_LEVEL_FINALIZED + ); + } +} + +abstract contract CCTPReceiver is CCTPAndTokenBridgeBase { + struct TokenReceived { + bytes32 tokenHomeAddress; + uint16 tokenHomeChain; + address tokenAddress; // wrapped address if tokenHomeChain !== this chain, else tokenHomeAddress (in evm address format) + uint256 amount; + uint256 amountNormalized; // if decimals > 8, normalized to 8 decimal places + } + + function receiveWormholeMessages( + bytes memory payload, + bytes[] memory additionalVaas, + bytes32 sourceAddress, + uint16 sourceChain, + bytes32 deliveryHash + ) external payable { + + } + + function receivePayloadAndTokens( + bytes memory payload, + TokenReceived[] memory receivedTokens, + bytes32 sourceAddress, + uint16 sourceChain, + bytes32 deliveryHash + ) internal virtual {} +} diff --git a/src/TokenBase.sol b/src/TokenBase.sol new file mode 100644 index 0000000..bddad2f --- /dev/null +++ b/src/TokenBase.sol @@ -0,0 +1,182 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "./interfaces/IWormholeReceiver.sol"; +import "./interfaces/IWormholeRelayer.sol"; +import "./interfaces/ITokenBridge.sol"; +import {IERC20} from "./interfaces/IERC20.sol"; +import {Base} from "./WormholeRelayerSDK.sol"; + +import "./Utils.sol"; + +abstract contract TokenBase is Base { + ITokenBridge public immutable tokenBridge; + + constructor(address _wormholeRelayer, address _tokenBridge, address _wormhole) Base(_wormholeRelayer, _wormhole) { + tokenBridge = ITokenBridge(_tokenBridge); + } + + function getDecimals(address tokenAddress) internal view returns (uint8 decimals) { + // query decimals + (, bytes memory queriedDecimals) = address(tokenAddress).staticcall(abi.encodeWithSignature("decimals()")); + decimals = abi.decode(queriedDecimals, (uint8)); + } + + function getTokenAddressOnThisChain(uint16 tokenHomeChain, bytes32 tokenHomeAddress) + internal + view + returns (address tokenAddressOnThisChain) + { + return tokenHomeChain == wormhole.chainId() + ? fromWormholeFormat(tokenHomeAddress) + : tokenBridge.wrappedAsset(tokenHomeChain, tokenHomeAddress); + } +} + +abstract contract TokenSender is TokenBase { + /** + * transferTokens wraps common boilerplate for sending tokens to another chain using IWormholeRelayer + * - approves tokenBridge to spend 'amount' of 'token' + * - emits token transfer VAA + * - returns VAA key for inclusion in WormholeRelayer `additionalVaas` argument + * + * Note: this function uses transferTokensWithPayload instead of transferTokens since the former requires that only the targetAddress + * can redeem transfers. Otherwise it's possible for another address to redeem the transfer before the targetContract is invoked by + * the offchain relayer and the target contract would have to be hardened against this. + * + */ + function transferTokens(address token, uint256 amount, uint16 targetChain, address targetAddress) + internal + returns (VaaKey memory) + { + return transferTokens(token, amount, targetChain, targetAddress, bytes("")); + } + + /** + * transferTokens wraps common boilerplate for sending tokens to another chain using IWormholeRelayer. + * A payload can be included in the transfer vaa. By including a payload here instead of the deliveryVaa, + * fewer trust assumptions are placed on the WormholeRelayer contract. + * + * - approves tokenBridge to spend 'amount' of 'token' + * - emits token transfer VAA + * - returns VAA key for inclusion in WormholeRelayer `additionalVaas` argument + * + * Note: this function uses transferTokensWithPayload instead of transferTokens since the former requires that only the targetAddress + * can redeem transfers. Otherwise it's possible for another address to redeem the transfer before the targetContract is invoked by + * the offchain relayer and the target contract would have to be hardened against this. + */ + function transferTokens( + address token, + uint256 amount, + uint16 targetChain, + address targetAddress, + bytes memory payload + ) internal returns (VaaKey memory) { + IERC20(token).approve(address(tokenBridge), amount); + uint64 sequence = tokenBridge.transferTokensWithPayload{value: wormhole.messageFee()}( + token, amount, targetChain, toWormholeFormat(targetAddress), 0, payload + ); + return VaaKey({ + emitterAddress: toWormholeFormat(address(tokenBridge)), + chainId: wormhole.chainId(), + sequence: sequence + }); + } + + function sendTokenWithPayloadToEvm( + uint16 targetChain, + address targetAddress, + bytes memory payload, + uint256 receiverValue, + uint256 gasLimit, + address token, + uint256 amount + ) internal returns (uint64) { + VaaKey[] memory vaaKeys = new VaaKey[](1); + vaaKeys[0] = transferTokens(token, amount, targetChain, targetAddress); + + (uint256 cost,) = wormholeRelayer.quoteEVMDeliveryPrice(targetChain, receiverValue, gasLimit); + return wormholeRelayer.sendVaasToEvm{value: cost}( + targetChain, targetAddress, payload, receiverValue, gasLimit, vaaKeys + ); + } + + function sendTokenWithPayloadToEvm( + uint16 targetChain, + address targetAddress, + bytes memory payload, + uint256 receiverValue, + uint256 gasLimit, + address token, + uint256 amount, + uint16 refundChain, + address refundAddress + ) internal returns (uint64) { + VaaKey[] memory vaaKeys = new VaaKey[](1); + vaaKeys[0] = transferTokens(token, amount, targetChain, targetAddress); + + (uint256 cost,) = wormholeRelayer.quoteEVMDeliveryPrice(targetChain, receiverValue, gasLimit); + return wormholeRelayer.sendVaasToEvm{value: cost}( + targetChain, targetAddress, payload, receiverValue, gasLimit, vaaKeys, refundChain, refundAddress + ); + } + +} + +abstract contract TokenReceiver is TokenBase { + struct TokenReceived { + bytes32 tokenHomeAddress; + uint16 tokenHomeChain; + address tokenAddress; // wrapped address if tokenHomeChain !== this chain, else tokenHomeAddress (in evm address format) + uint256 amount; + uint256 amountNormalized; // if decimals > 8, normalized to 8 decimal places + } + + function receiveWormholeMessages( + bytes memory payload, + bytes[] memory additionalVaas, + bytes32 sourceAddress, + uint16 sourceChain, + bytes32 deliveryHash + ) external payable { + TokenReceived[] memory receivedTokens = new TokenReceived[](additionalVaas.length); + + for (uint256 i = 0; i < additionalVaas.length; ++i) { + IWormhole.VM memory parsed = wormhole.parseVM(additionalVaas[i]); + require( + parsed.emitterAddress == tokenBridge.bridgeContracts(parsed.emitterChainId), "Not a Token Bridge VAA" + ); + ITokenBridge.TransferWithPayload memory transfer = tokenBridge.parseTransferWithPayload(parsed.payload); + require( + transfer.to == toWormholeFormat(address(this)) && transfer.toChain == wormhole.chainId(), + "Token was not sent to this address" + ); + + tokenBridge.completeTransferWithPayload(additionalVaas[i]); + + address thisChainTokenAddress = getTokenAddressOnThisChain(transfer.tokenChain, transfer.tokenAddress); + uint8 decimals = getDecimals(thisChainTokenAddress); + uint256 denormalizedAmount = transfer.amount; + if (decimals > 8) denormalizedAmount *= uint256(10) ** (decimals - 8); + + receivedTokens[i] = TokenReceived({ + tokenHomeAddress: transfer.tokenAddress, + tokenHomeChain: transfer.tokenChain, + tokenAddress: thisChainTokenAddress, + amount: denormalizedAmount, + amountNormalized: transfer.amount + }); + } + + // call into overriden method + receivePayloadAndTokens(payload, receivedTokens, sourceAddress, sourceChain, deliveryHash); + } + + function receivePayloadAndTokens( + bytes memory payload, + TokenReceived[] memory receivedTokens, + bytes32 sourceAddress, + uint16 sourceChain, + bytes32 deliveryHash + ) internal virtual {} +} diff --git a/src/WormholeRelayerSDK.sol b/src/WormholeRelayerSDK.sol index 269ef31..7470973 100644 --- a/src/WormholeRelayerSDK.sol +++ b/src/WormholeRelayerSDK.sol @@ -3,222 +3,6 @@ pragma solidity ^0.8.13; import "./interfaces/IWormholeReceiver.sol"; import "./interfaces/IWormholeRelayer.sol"; -import "./interfaces/ITokenBridge.sol"; -import {IERC20} from "./interfaces/IERC20.sol"; - import "./Utils.sol"; - -abstract contract Base { - IWormholeRelayer public immutable wormholeRelayer; - IWormhole public immutable wormhole; - - mapping(bytes32 => bool) public seenDeliveryVaaHashes; - - address registrationOwner; - mapping(uint16 => bytes32) registeredSenders; - - constructor(address _wormholeRelayer, address _wormhole) { - wormholeRelayer = IWormholeRelayer(_wormholeRelayer); - wormhole = IWormhole(_wormhole); - registrationOwner = msg.sender; - } - - modifier onlyWormholeRelayer() { - require(msg.sender == address(wormholeRelayer), "Msg.sender is not Wormhole Relayer"); - _; - } - - modifier replayProtect(bytes32 deliveryHash) { - require(!seenDeliveryVaaHashes[deliveryHash], "Message already processed"); - seenDeliveryVaaHashes[deliveryHash] = true; - _; - } - - modifier isRegisteredSender(uint16 sourceChain, bytes32 sourceAddress) { - require(registeredSenders[sourceChain] == sourceAddress, "Not registered sender"); - _; - } - - /** - * Sets the registered address for 'sourceChain' to 'sourceAddress' - * So that for messages from 'sourceChain', only ones from 'sourceAddress' are valid - * - * Assumes only one sender per chain is valid - * Sender is the address that called 'send' on the Wormhole Relayer contract on the source chain) - */ - function setRegisteredSender(uint16 sourceChain, bytes32 sourceAddress) public { - require(msg.sender == registrationOwner, "Not allowed to set registered sender"); - registeredSenders[sourceChain] = sourceAddress; - } -} - -abstract contract TokenBase is Base { - ITokenBridge public immutable tokenBridge; - - constructor(address _wormholeRelayer, address _tokenBridge, address _wormhole) Base(_wormholeRelayer, _wormhole) { - tokenBridge = ITokenBridge(_tokenBridge); - } - - function getDecimals(address tokenAddress) internal view returns (uint8 decimals) { - // query decimals - (, bytes memory queriedDecimals) = address(tokenAddress).staticcall(abi.encodeWithSignature("decimals()")); - decimals = abi.decode(queriedDecimals, (uint8)); - } - - function getTokenAddressOnThisChain(uint16 tokenHomeChain, bytes32 tokenHomeAddress) - internal - view - returns (address tokenAddressOnThisChain) - { - return tokenHomeChain == wormhole.chainId() - ? fromWormholeFormat(tokenHomeAddress) - : tokenBridge.wrappedAsset(tokenHomeChain, tokenHomeAddress); - } -} - -abstract contract TokenSender is TokenBase { - /** - * transferTokens wraps common boilerplate for sending tokens to another chain using IWormholeRelayer - * - approves tokenBridge to spend 'amount' of 'token' - * - emits token transfer VAA - * - returns VAA key for inclusion in WormholeRelayer `additionalVaas` argument - * - * Note: this function uses transferTokensWithPayload instead of transferTokens since the former requires that only the targetAddress - * can redeem transfers. Otherwise it's possible for another address to redeem the transfer before the targetContract is invoked by - * the offchain relayer and the target contract would have to be hardened against this. - * - */ - function transferTokens(address token, uint256 amount, uint16 targetChain, address targetAddress) - internal - returns (VaaKey memory) - { - return transferTokens(token, amount, targetChain, targetAddress, bytes("")); - } - - /** - * transferTokens wraps common boilerplate for sending tokens to another chain using IWormholeRelayer. - * A payload can be included in the transfer vaa. By including a payload here instead of the deliveryVaa, - * fewer trust assumptions are placed on the WormholeRelayer contract. - * - * - approves tokenBridge to spend 'amount' of 'token' - * - emits token transfer VAA - * - returns VAA key for inclusion in WormholeRelayer `additionalVaas` argument - * - * Note: this function uses transferTokensWithPayload instead of transferTokens since the former requires that only the targetAddress - * can redeem transfers. Otherwise it's possible for another address to redeem the transfer before the targetContract is invoked by - * the offchain relayer and the target contract would have to be hardened against this. - */ - function transferTokens( - address token, - uint256 amount, - uint16 targetChain, - address targetAddress, - bytes memory payload - ) internal returns (VaaKey memory) { - IERC20(token).approve(address(tokenBridge), amount); - uint64 sequence = tokenBridge.transferTokensWithPayload{value: wormhole.messageFee()}( - token, amount, targetChain, toWormholeFormat(targetAddress), 0, payload - ); - return VaaKey({ - emitterAddress: toWormholeFormat(address(tokenBridge)), - chainId: wormhole.chainId(), - sequence: sequence - }); - } - - function sendTokenWithPayloadToEvm( - uint16 targetChain, - address targetAddress, - bytes memory payload, - uint256 receiverValue, - uint256 gasLimit, - address token, - uint256 amount - ) internal returns (uint64) { - VaaKey[] memory vaaKeys = new VaaKey[](1); - vaaKeys[0] = transferTokens(token, amount, targetChain, targetAddress); - - (uint256 cost,) = wormholeRelayer.quoteEVMDeliveryPrice(targetChain, receiverValue, gasLimit); - return wormholeRelayer.sendVaasToEvm{value: cost}( - targetChain, targetAddress, payload, receiverValue, gasLimit, vaaKeys - ); - } - - function sendTokenWithPayloadToEvm( - uint16 targetChain, - address targetAddress, - bytes memory payload, - uint256 receiverValue, - uint256 gasLimit, - address token, - uint256 amount, - uint16 refundChain, - address refundAddress - ) internal returns (uint64) { - VaaKey[] memory vaaKeys = new VaaKey[](1); - vaaKeys[0] = transferTokens(token, amount, targetChain, targetAddress); - - (uint256 cost,) = wormholeRelayer.quoteEVMDeliveryPrice(targetChain, receiverValue, gasLimit); - return wormholeRelayer.sendVaasToEvm{value: cost}( - targetChain, targetAddress, payload, receiverValue, gasLimit, vaaKeys, refundChain, refundAddress - ); - } -} - -abstract contract TokenReceiver is TokenBase { - struct TokenReceived { - bytes32 tokenHomeAddress; - uint16 tokenHomeChain; - address tokenAddress; // wrapped address if tokenHomeChain !== this chain, else tokenHomeAddress (in evm address format) - uint256 amount; - uint256 amountNormalized; // if decimals > 8, normalized to 8 decimal places - } - - function receiveWormholeMessages( - bytes memory payload, - bytes[] memory additionalVaas, - bytes32 sourceAddress, - uint16 sourceChain, - bytes32 deliveryHash - ) external payable { - TokenReceived[] memory receivedTokens = new TokenReceived[](additionalVaas.length); - - for (uint256 i = 0; i < additionalVaas.length; ++i) { - IWormhole.VM memory parsed = wormhole.parseVM(additionalVaas[i]); - require( - parsed.emitterAddress == tokenBridge.bridgeContracts(parsed.emitterChainId), "Not a Token Bridge VAA" - ); - ITokenBridge.TransferWithPayload memory transfer = tokenBridge.parseTransferWithPayload(parsed.payload); - require( - transfer.to == toWormholeFormat(address(this)) && transfer.toChain == wormhole.chainId(), - "Token was not sent to this address" - ); - - tokenBridge.completeTransferWithPayload(additionalVaas[i]); - - address thisChainTokenAddress = getTokenAddressOnThisChain(transfer.tokenChain, transfer.tokenAddress); - uint8 decimals = getDecimals(thisChainTokenAddress); - uint256 denormalizedAmount = transfer.amount; - if (decimals > 8) denormalizedAmount *= uint256(10) ** (decimals - 8); - - receivedTokens[i] = TokenReceived({ - tokenHomeAddress: transfer.tokenAddress, - tokenHomeChain: transfer.tokenChain, - tokenAddress: thisChainTokenAddress, - amount: denormalizedAmount, - amountNormalized: transfer.amount - }); - } - - // call into overriden method - receivePayloadAndTokens(payload, receivedTokens, sourceAddress, sourceChain, deliveryHash); - } - - function receivePayloadAndTokens( - bytes memory payload, - TokenReceived[] memory receivedTokens, - bytes32 sourceAddress, - uint16 sourceChain, - bytes32 deliveryHash - ) internal virtual {} -} +import {Base} from "./Base.sol"; +import {TokenBase, TokenReceiver, TokenSender} from "./TokenBase.sol"; diff --git a/src/interfaces/CCTPInterfaces/IMessageTransmitter.sol b/src/interfaces/CCTPInterfaces/IMessageTransmitter.sol new file mode 100644 index 0000000..c1a12b3 --- /dev/null +++ b/src/interfaces/CCTPInterfaces/IMessageTransmitter.sol @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2022, Circle Internet Financial Limited. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity ^0.8.0; + +import "./IRelayer.sol"; +import "./IReceiver.sol"; + +/** + * @title IMessageTransmitter + * @notice Interface for message transmitters, which both relay and receive messages. + */ +interface IMessageTransmitter is IRelayer, IReceiver { + +} diff --git a/src/interfaces/CCTPInterfaces/IReceiver.sol b/src/interfaces/CCTPInterfaces/IReceiver.sol new file mode 100644 index 0000000..ff8821d --- /dev/null +++ b/src/interfaces/CCTPInterfaces/IReceiver.sol @@ -0,0 +1,33 @@ +/* + * Copyright (c) 2022, Circle Internet Financial Limited. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity ^0.8.0; + +/** + * @title IReceiver + * @notice Receives messages on destination chain and forwards them to IMessageDestinationHandler + */ +interface IReceiver { + /** + * @notice Receives an incoming message, validating the header and passing + * the body to application-specific handler. + * @param message The message raw bytes + * @param signature The message signature + * @return success bool, true if successful + */ + function receiveMessage(bytes calldata message, bytes calldata signature) + external + returns (bool success); +} diff --git a/src/interfaces/CCTPInterfaces/IRelayer.sol b/src/interfaces/CCTPInterfaces/IRelayer.sol new file mode 100644 index 0000000..a7f50d5 --- /dev/null +++ b/src/interfaces/CCTPInterfaces/IRelayer.sol @@ -0,0 +1,71 @@ +/* + * Copyright (c) 2022, Circle Internet Financial Limited. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pragma solidity ^0.8.0; + +/** + * @title IRelayer + * @notice Sends messages from source domain to destination domain + */ +interface IRelayer { + /** + * @notice Sends an outgoing message from the source domain. + * @dev Increment nonce, format the message, and emit `MessageSent` event with message information. + * @param destinationDomain Domain of destination chain + * @param recipient Address of message recipient on destination domain as bytes32 + * @param messageBody Raw bytes content of message + * @return nonce reserved by message + */ + function sendMessage( + uint32 destinationDomain, + bytes32 recipient, + bytes calldata messageBody + ) external returns (uint64); + + /** + * @notice Sends an outgoing message from the source domain, with a specified caller on the + * destination domain. + * @dev Increment nonce, format the message, and emit `MessageSent` event with message information. + * WARNING: if the `destinationCaller` does not represent a valid address as bytes32, then it will not be possible + * to broadcast the message on the destination domain. This is an advanced feature, and the standard + * sendMessage() should be preferred for use cases where a specific destination caller is not required. + * @param destinationDomain Domain of destination chain + * @param recipient Address of message recipient on destination domain as bytes32 + * @param destinationCaller caller on the destination domain, as bytes32 + * @param messageBody Raw bytes content of message + * @return nonce reserved by message + */ + function sendMessageWithCaller( + uint32 destinationDomain, + bytes32 recipient, + bytes32 destinationCaller, + bytes calldata messageBody + ) external returns (uint64); + + /** + * @notice Replace a message with a new message body and/or destination caller. + * @dev The `originalAttestation` must be a valid attestation of `originalMessage`. + * @param originalMessage original message to replace + * @param originalAttestation attestation of `originalMessage` + * @param newMessageBody new message body of replaced message + * @param newDestinationCaller the new destination caller + */ + function replaceMessage( + bytes calldata originalMessage, + bytes calldata originalAttestation, + bytes calldata newMessageBody, + bytes32 newDestinationCaller + ) external; +} diff --git a/src/interfaces/CCTPInterfaces/ITokenMessenger.sol b/src/interfaces/CCTPInterfaces/ITokenMessenger.sol new file mode 100644 index 0000000..29e700e --- /dev/null +++ b/src/interfaces/CCTPInterfaces/ITokenMessenger.sol @@ -0,0 +1,34 @@ + +pragma solidity ^0.8.0; + +interface ITokenMessenger { + /** + * @notice Deposits and burns tokens from sender to be minted on destination domain. The mint + * on the destination domain must be called by `destinationCaller`. + * WARNING: if the `destinationCaller` does not represent a valid address as bytes32, then it will not be possible + * to broadcast the message on the destination domain. This is an advanced feature, and the standard + * depositForBurn() should be preferred for use cases where a specific destination caller is not required. + * Emits a `DepositForBurn` event. + * @dev reverts if: + * - given destinationCaller is zero address + * - given burnToken is not supported + * - given destinationDomain has no TokenMessenger registered + * - transferFrom() reverts. For example, if sender's burnToken balance or approved allowance + * to this contract is less than `amount`. + * - burn() reverts. For example, if `amount` is 0. + * - MessageTransmitter returns false or reverts. + * @param amount amount of tokens to burn + * @param destinationDomain destination domain + * @param mintRecipient address of mint recipient on destination domain + * @param burnToken address of contract to burn deposited tokens, on local domain + * @param destinationCaller caller on the destination domain, as bytes32 + * @return nonce unique nonce reserved by message + */ + function depositForBurnWithCaller( + uint256 amount, + uint32 destinationDomain, + bytes32 mintRecipient, + address burnToken, + bytes32 destinationCaller + ) external returns (uint64 nonce); +} diff --git a/src/interfaces/IWormholeRelayer.sol b/src/interfaces/IWormholeRelayer.sol index a316758..b0c28be 100644 --- a/src/interfaces/IWormholeRelayer.sol +++ b/src/interfaces/IWormholeRelayer.sol @@ -4,11 +4,11 @@ pragma solidity ^0.8.0; /** * @title WormholeRelayer - * @author - * @notice This project allows developers to build cross-chain applications powered by Wormhole without needing to + * @author + * @notice This project allows developers to build cross-chain applications powered by Wormhole without needing to * write and run their own relaying infrastructure - * - * We implement the IWormholeRelayer interface that allows users to request a delivery provider to relay a payload (and/or additional VAAs) + * + * We implement the IWormholeRelayer interface that allows users to request a delivery provider to relay a payload (and/or additional VAAs) * to a chain and address of their choice. */ @@ -25,10 +25,37 @@ struct VaaKey { uint64 sequence; } +// 0-127 are reserved for standardized KeyTypes, 128-255 are for custom use +uint8 constant VAA_KEY_TYPE = 1; + +struct MessageKey { + uint8 keyType; // 0-127 are reserved for standardized KeyTypes, 128-255 are for custom use + bytes encodedKey; +} + + interface IWormholeRelayerBase { - event SendEvent(uint64 indexed sequence, uint256 deliveryQuote, uint256 paymentForExtraReceiverValue); + event SendEvent( + uint64 indexed sequence, uint256 deliveryQuote, uint256 paymentForExtraReceiverValue + ); function getRegisteredWormholeRelayerContract(uint16 chainId) external view returns (bytes32); + + /** + * @notice Returns true if a delivery has been attempted for the given deliveryHash + * Note: invalid deliveries where the tx reverts are not considered attempted + */ + function deliveryAttempted(bytes32 deliveryHash) external view returns (bool attempted); + + /** + * @notice block number at which a delivery was successfully executed + */ + function deliverySuccessBlock(bytes32 deliveryHash) external view returns (uint256 blockNumber); + + /** + * @notice block number of the latest attempt to execute a delivery that failed + */ + function deliveryFailureBlock(bytes32 deliveryHash) external view returns (uint256 blockNumber); } /** @@ -36,20 +63,21 @@ interface IWormholeRelayerBase { * @notice The interface to request deliveries */ interface IWormholeRelayerSend is IWormholeRelayerBase { + /** * @notice Publishes an instruction for the default delivery provider - * to relay a payload to the address `targetAddress` on chain `targetChain` + * to relay a payload to the address `targetAddress` on chain `targetChain` * with gas limit `gasLimit` and `msg.value` equal to `receiverValue` - * + * * `targetAddress` must implement the IWormholeReceiver interface - * + * * This function must be called with `msg.value` equal to `quoteEVMDeliveryPrice(targetChain, receiverValue, gasLimit)` - * - * Any refunds (from leftover gas) will be paid to the delivery provider. In order to receive the refunds, use the `sendPayloadToEvm` function + * + * Any refunds (from leftover gas) will be paid to the delivery provider. In order to receive the refunds, use the `sendPayloadToEvm` function * with `refundChain` and `refundAddress` as parameters - * + * * @param targetChain in Wormhole Chain ID format - * @param targetAddress address to call on targetChain (that implements IWormholeReceiver) + * @param targetAddress address to call on targetChain (that implements IWormholeReceiver) * @param payload arbitrary bytes to pass in as parameter in call to `targetAddress` * @param receiverValue msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units) * @param gasLimit gas limit with which to call `targetAddress`. @@ -65,16 +93,16 @@ interface IWormholeRelayerSend is IWormholeRelayerBase { /** * @notice Publishes an instruction for the default delivery provider - * to relay a payload to the address `targetAddress` on chain `targetChain` + * to relay a payload to the address `targetAddress` on chain `targetChain` * with gas limit `gasLimit` and `msg.value` equal to `receiverValue` - * + * * Any refunds (from leftover gas) will be sent to `refundAddress` on chain `refundChain` * `targetAddress` must implement the IWormholeReceiver interface - * + * * This function must be called with `msg.value` equal to `quoteEVMDeliveryPrice(targetChain, receiverValue, gasLimit)` - * + * * @param targetChain in Wormhole Chain ID format - * @param targetAddress address to call on targetChain (that implements IWormholeReceiver) + * @param targetAddress address to call on targetChain (that implements IWormholeReceiver) * @param payload arbitrary bytes to pass in as parameter in call to `targetAddress` * @param receiverValue msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units) * @param gasLimit gas limit with which to call `targetAddress`. Any units of gas unused will be refunded according to the @@ -95,21 +123,21 @@ interface IWormholeRelayerSend is IWormholeRelayerBase { /** * @notice Publishes an instruction for the default delivery provider - * to relay a payload and VAAs specified by `vaaKeys` to the address `targetAddress` on chain `targetChain` + * to relay a payload and VAAs specified by `vaaKeys` to the address `targetAddress` on chain `targetChain` * with gas limit `gasLimit` and `msg.value` equal to `receiverValue` - * + * * `targetAddress` must implement the IWormholeReceiver interface - * + * * This function must be called with `msg.value` equal to `quoteEVMDeliveryPrice(targetChain, receiverValue, gasLimit)` - * - * Any refunds (from leftover gas) will be paid to the delivery provider. In order to receive the refunds, use the `sendVaasToEvm` function + * + * Any refunds (from leftover gas) will be paid to the delivery provider. In order to receive the refunds, use the `sendVaasToEvm` function * with `refundChain` and `refundAddress` as parameters - * + * * @param targetChain in Wormhole Chain ID format - * @param targetAddress address to call on targetChain (that implements IWormholeReceiver) + * @param targetAddress address to call on targetChain (that implements IWormholeReceiver) * @param payload arbitrary bytes to pass in as parameter in call to `targetAddress` * @param receiverValue msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units) - * @param gasLimit gas limit with which to call `targetAddress`. + * @param gasLimit gas limit with which to call `targetAddress`. * @param vaaKeys Additional VAAs to pass in as parameter in call to `targetAddress` * @return sequence sequence number of published VAA containing delivery instructions */ @@ -124,19 +152,19 @@ interface IWormholeRelayerSend is IWormholeRelayerBase { /** * @notice Publishes an instruction for the default delivery provider - * to relay a payload and VAAs specified by `vaaKeys` to the address `targetAddress` on chain `targetChain` + * to relay a payload and VAAs specified by `vaaKeys` to the address `targetAddress` on chain `targetChain` * with gas limit `gasLimit` and `msg.value` equal to `receiverValue` - * + * * Any refunds (from leftover gas) will be sent to `refundAddress` on chain `refundChain` * `targetAddress` must implement the IWormholeReceiver interface - * + * * This function must be called with `msg.value` equal to `quoteEVMDeliveryPrice(targetChain, receiverValue, gasLimit)` - * + * * @param targetChain in Wormhole Chain ID format - * @param targetAddress address to call on targetChain (that implements IWormholeReceiver) + * @param targetAddress address to call on targetChain (that implements IWormholeReceiver) * @param payload arbitrary bytes to pass in as parameter in call to `targetAddress` * @param receiverValue msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units) - * @param gasLimit gas limit with which to call `targetAddress`. Any units of gas unused will be refunded according to the + * @param gasLimit gas limit with which to call `targetAddress`. Any units of gas unused will be refunded according to the * `targetChainRefundPerGasUnused` rate quoted by the delivery provider * @param vaaKeys Additional VAAs to pass in as parameter in call to `targetAddress` * @param refundChain The chain to deliver any refund to, in Wormhole Chain ID format @@ -155,30 +183,30 @@ interface IWormholeRelayerSend is IWormholeRelayerBase { ) external payable returns (uint64 sequence); /** - * @notice Publishes an instruction for the delivery provider at `deliveryProviderAddress` - * to relay a payload and VAAs specified by `vaaKeys` to the address `targetAddress` on chain `targetChain` - * with gas limit `gasLimit` and `msg.value` equal to + * @notice Publishes an instruction for the delivery provider at `deliveryProviderAddress` + * to relay a payload and VAAs specified by `vaaKeys` to the address `targetAddress` on chain `targetChain` + * with gas limit `gasLimit` and `msg.value` equal to * receiverValue + (arbitrary amount that is paid for by paymentForExtraReceiverValue of this chain's wei) in targetChain wei. - * + * * Any refunds (from leftover gas) will be sent to `refundAddress` on chain `refundChain` * `targetAddress` must implement the IWormholeReceiver interface - * - * This function must be called with `msg.value` equal to + * + * This function must be called with `msg.value` equal to * quoteEVMDeliveryPrice(targetChain, receiverValue, gasLimit, deliveryProviderAddress) + paymentForExtraReceiverValue - * + * * @param targetChain in Wormhole Chain ID format - * @param targetAddress address to call on targetChain (that implements IWormholeReceiver) + * @param targetAddress address to call on targetChain (that implements IWormholeReceiver) * @param payload arbitrary bytes to pass in as parameter in call to `targetAddress` * @param receiverValue msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units) - * @param paymentForExtraReceiverValue amount (in current chain currency units) to spend on extra receiverValue + * @param paymentForExtraReceiverValue amount (in current chain currency units) to spend on extra receiverValue * (in addition to the `receiverValue` specified) - * @param gasLimit gas limit with which to call `targetAddress`. Any units of gas unused will be refunded according to the + * @param gasLimit gas limit with which to call `targetAddress`. Any units of gas unused will be refunded according to the * `targetChainRefundPerGasUnused` rate quoted by the delivery provider * @param refundChain The chain to deliver any refund to, in Wormhole Chain ID format * @param refundAddress The address on `refundChain` to deliver any refund to * @param deliveryProviderAddress The address of the desired delivery provider's implementation of IDeliveryProvider * @param vaaKeys Additional VAAs to pass in as parameter in call to `targetAddress` - * @param consistencyLevel Consistency level with which to publish the delivery instructions - see + * @param consistencyLevel Consistency level with which to publish the delivery instructions - see * https://book.wormhole.com/wormhole/3_coreLayerContracts.html?highlight=consistency#consistency-levels * @return sequence sequence number of published VAA containing delivery instructions */ @@ -197,22 +225,67 @@ interface IWormholeRelayerSend is IWormholeRelayerBase { ) external payable returns (uint64 sequence); /** - * @notice Publishes an instruction for the delivery provider at `deliveryProviderAddress` - * to relay a payload and VAAs specified by `vaaKeys` to the address `targetAddress` on chain `targetChain` - * with `msg.value` equal to + * @notice Publishes an instruction for the delivery provider at `deliveryProviderAddress` + * to relay a payload and external messages specified by `messageKeys` to the address `targetAddress` on chain `targetChain` + * with gas limit `gasLimit` and `msg.value` equal to * receiverValue + (arbitrary amount that is paid for by paymentForExtraReceiverValue of this chain's wei) in targetChain wei. - * + * * Any refunds (from leftover gas) will be sent to `refundAddress` on chain `refundChain` * `targetAddress` must implement the IWormholeReceiver interface + * + * This function must be called with `msg.value` equal to + * quoteEVMDeliveryPrice(targetChain, receiverValue, gasLimit, deliveryProviderAddress) + paymentForExtraReceiverValue * - * This function must be called with `msg.value` equal to - * quoteDeliveryPrice(targetChain, receiverValue, encodedExecutionParameters, deliveryProviderAddress) + paymentForExtraReceiverValue - * + * MessageKeys can specify wormhole messages (VaaKeys) or other types of messages (ex. USDC CCTP attestations). Ensure the selected + * Note: DeliveryProvider supports all the MessageKey.keyType values specified or it will not be delivered! + * + * @param targetChain in Wormhole Chain ID format + * @param targetAddress address to call on targetChain (that implements IWormholeReceiver) + * @param payload arbitrary bytes to pass in as parameter in call to `targetAddress` + * @param receiverValue msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units) + * @param paymentForExtraReceiverValue amount (in current chain currency units) to spend on extra receiverValue + * (in addition to the `receiverValue` specified) + * @param gasLimit gas limit with which to call `targetAddress`. Any units of gas unused will be refunded according to the + * `targetChainRefundPerGasUnused` rate quoted by the delivery provider + * @param refundChain The chain to deliver any refund to, in Wormhole Chain ID format + * @param refundAddress The address on `refundChain` to deliver any refund to + * @param deliveryProviderAddress The address of the desired delivery provider's implementation of IDeliveryProvider + * @param messageKeys Additional messagess to pass in as parameter in call to `targetAddress` + * @param consistencyLevel Consistency level with which to publish the delivery instructions - see + * https://book.wormhole.com/wormhole/3_coreLayerContracts.html?highlight=consistency#consistency-levels + * @return sequence sequence number of published VAA containing delivery instructions + */ + 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); + + /** + * @notice Publishes an instruction for the delivery provider at `deliveryProviderAddress` + * to relay a payload and VAAs specified by `vaaKeys` to the address `targetAddress` on chain `targetChain` + * with `msg.value` equal to + * receiverValue + (arbitrary amount that is paid for by paymentForExtraReceiverValue of this chain's wei) in targetChain wei. + * + * Any refunds (from leftover gas) will be sent to `refundAddress` on chain `refundChain` + * `targetAddress` must implement the IWormholeReceiver interface + * + * This function must be called with `msg.value` equal to + * quoteDeliveryPrice(targetChain, receiverValue, encodedExecutionParameters, deliveryProviderAddress) + paymentForExtraReceiverValue + * * @param targetChain in Wormhole Chain ID format * @param targetAddress address to call on targetChain (that implements IWormholeReceiver), in Wormhole bytes32 format * @param payload arbitrary bytes to pass in as parameter in call to `targetAddress` * @param receiverValue msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units) - * @param paymentForExtraReceiverValue amount (in current chain currency units) to spend on extra receiverValue + * @param paymentForExtraReceiverValue amount (in current chain currency units) to spend on extra receiverValue * (in addition to the `receiverValue` specified) * @param encodedExecutionParameters encoded information on how to execute delivery that may impact pricing * e.g. for version EVM_V1, this is a struct that encodes the `gasLimit` with which to call `targetAddress` @@ -220,7 +293,7 @@ interface IWormholeRelayerSend is IWormholeRelayerBase { * @param refundAddress The address on `refundChain` to deliver any refund to, in Wormhole bytes32 format * @param deliveryProviderAddress The address of the desired delivery provider's implementation of IDeliveryProvider * @param vaaKeys Additional VAAs to pass in as parameter in call to `targetAddress` - * @param consistencyLevel Consistency level with which to publish the delivery instructions - see + * @param consistencyLevel Consistency level with which to publish the delivery instructions - see * https://book.wormhole.com/wormhole/3_coreLayerContracts.html?highlight=consistency#consistency-levels * @return sequence sequence number of published VAA containing delivery instructions */ @@ -239,22 +312,67 @@ interface IWormholeRelayerSend is IWormholeRelayerBase { ) external payable returns (uint64 sequence); /** - * @notice Requests a previously published delivery instruction to be redelivered + * @notice Publishes an instruction for the delivery provider at `deliveryProviderAddress` + * to relay a payload and VAAs specified by `vaaKeys` to the address `targetAddress` on chain `targetChain` + * with `msg.value` equal to + * receiverValue + (arbitrary amount that is paid for by paymentForExtraReceiverValue of this chain's wei) in targetChain wei. + * + * Any refunds (from leftover gas) will be sent to `refundAddress` on chain `refundChain` + * `targetAddress` must implement the IWormholeReceiver interface + * + * This function must be called with `msg.value` equal to + * quoteDeliveryPrice(targetChain, receiverValue, encodedExecutionParameters, deliveryProviderAddress) + paymentForExtraReceiverValue + * + * MessageKeys can specify wormhole messages (VaaKeys) or other types of messages (ex. USDC CCTP attestations). Ensure the selected + * Note: DeliveryProvider supports all the MessageKey.keyType values specified or it will not be delivered! + * + * @param targetChain in Wormhole Chain ID format + * @param targetAddress address to call on targetChain (that implements IWormholeReceiver), in Wormhole bytes32 format + * @param payload arbitrary bytes to pass in as parameter in call to `targetAddress` + * @param receiverValue msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units) + * @param paymentForExtraReceiverValue amount (in current chain currency units) to spend on extra receiverValue + * (in addition to the `receiverValue` specified) + * @param encodedExecutionParameters encoded information on how to execute delivery that may impact pricing + * e.g. for version EVM_V1, this is a struct that encodes the `gasLimit` with which to call `targetAddress` + * @param refundChain The chain to deliver any refund to, in Wormhole Chain ID format + * @param refundAddress The address on `refundChain` to deliver any refund to, in Wormhole bytes32 format + * @param deliveryProviderAddress The address of the desired delivery provider's implementation of IDeliveryProvider + * @param messageKeys Additional messagess to pass in as parameter in call to `targetAddress` + * @param consistencyLevel Consistency level with which to publish the delivery instructions - see + * https://book.wormhole.com/wormhole/3_coreLayerContracts.html?highlight=consistency#consistency-levels + * @return sequence sequence number of published VAA containing delivery instructions + */ + function send( + uint16 targetChain, + bytes32 targetAddress, + bytes memory payload, + uint256 receiverValue, + uint256 paymentForExtraReceiverValue, + bytes memory encodedExecutionParameters, + uint16 refundChain, + bytes32 refundAddress, + address deliveryProviderAddress, + MessageKey[] memory messageKeys, + uint8 consistencyLevel + ) external payable returns (uint64 sequence); + + /** + * @notice Requests a previously published delivery instruction to be redelivered * (e.g. with a different delivery provider) * - * This function must be called with `msg.value` equal to + * This function must be called with `msg.value` equal to * quoteEVMDeliveryPrice(targetChain, newReceiverValue, newGasLimit, newDeliveryProviderAddress) - * + * * @notice *** This will only be able to succeed if the following is true ** * - newGasLimit >= gas limit of the old instruction * - newReceiverValue >= receiver value of the old instruction * - newDeliveryProvider's `targetChainRefundPerGasUnused` >= old relay provider's `targetChainRefundPerGasUnused` - * - * @param deliveryVaaKey VaaKey identifying the wormhole message containing the + * + * @param deliveryVaaKey VaaKey identifying the wormhole message containing the * previously published delivery instructions * @param targetChain The target chain that the original delivery targeted. Must match targetChain from original delivery instructions * @param newReceiverValue new msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units) - * @param newGasLimit gas limit with which to call `targetAddress`. Any units of gas unused will be refunded according to the + * @param newGasLimit gas limit with which to call `targetAddress`. Any units of gas unused will be refunded according to the * `targetChainRefundPerGasUnused` rate quoted by the delivery provider, to the refund chain and address specified in the original request * @param newDeliveryProviderAddress The address of the desired delivery provider's implementation of IDeliveryProvider * @return sequence sequence number of published VAA containing redelivery instructions @@ -273,13 +391,13 @@ interface IWormholeRelayerSend is IWormholeRelayerBase { ) external payable returns (uint64 sequence); /** - * @notice Requests a previously published delivery instruction to be redelivered + * @notice Requests a previously published delivery instruction to be redelivered + * * - * - * This function must be called with `msg.value` equal to + * This function must be called with `msg.value` equal to * quoteDeliveryPrice(targetChain, newReceiverValue, newEncodedExecutionParameters, newDeliveryProviderAddress) - * - * @param deliveryVaaKey VaaKey identifying the wormhole message containing the + * + * @param deliveryVaaKey VaaKey identifying the wormhole message containing the * previously published delivery instructions * @param targetChain The target chain that the original delivery targeted. Must match targetChain from original delivery instructions * @param newReceiverValue new msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units) @@ -287,7 +405,7 @@ interface IWormholeRelayerSend is IWormholeRelayerBase { * e.g. for version EVM_V1, this is a struct that encodes the `gasLimit` with which to call `targetAddress` * @param newDeliveryProviderAddress The address of the desired delivery provider's implementation of IDeliveryProvider * @return sequence sequence number of published VAA containing redelivery instructions - * + * * @notice *** This will only be able to succeed if the following is true ** * - (For EVM_V1) newGasLimit >= gas limit of the old instruction * - newReceiverValue >= receiver value of the old instruction @@ -303,28 +421,29 @@ interface IWormholeRelayerSend is IWormholeRelayerBase { /** * @notice Returns the price to request a relay to chain `targetChain`, using the default delivery provider - * + * * @param targetChain in Wormhole Chain ID format * @param receiverValue msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units) - * @param gasLimit gas limit with which to call `targetAddress`. + * @param gasLimit gas limit with which to call `targetAddress`. * @return nativePriceQuote Price, in units of current chain currency, that the delivery provider charges to perform the relay - * @return targetChainRefundPerGasUnused amount of target chain currency that will be refunded per unit of gas unused, + * @return targetChainRefundPerGasUnused amount of target chain currency that will be refunded per unit of gas unused, * if a refundAddress is specified */ - function quoteEVMDeliveryPrice(uint16 targetChain, uint256 receiverValue, uint256 gasLimit) - external - view - returns (uint256 nativePriceQuote, uint256 targetChainRefundPerGasUnused); + function quoteEVMDeliveryPrice( + uint16 targetChain, + uint256 receiverValue, + uint256 gasLimit + ) external view returns (uint256 nativePriceQuote, uint256 targetChainRefundPerGasUnused); /** * @notice Returns the price to request a relay to chain `targetChain`, using delivery provider `deliveryProviderAddress` - * + * * @param targetChain in Wormhole Chain ID format * @param receiverValue msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units) - * @param gasLimit gas limit with which to call `targetAddress`. + * @param gasLimit gas limit with which to call `targetAddress`. * @param deliveryProviderAddress The address of the desired delivery provider's implementation of IDeliveryProvider * @return nativePriceQuote Price, in units of current chain currency, that the delivery provider charges to perform the relay - * @return targetChainRefundPerGasUnused amount of target chain currency that will be refunded per unit of gas unused, + * @return targetChainRefundPerGasUnused amount of target chain currency that will be refunded per unit of gas unused, * if a refundAddress is specified */ function quoteEVMDeliveryPrice( @@ -336,7 +455,7 @@ interface IWormholeRelayerSend is IWormholeRelayerBase { /** * @notice Returns the price to request a relay to chain `targetChain`, using delivery provider `deliveryProviderAddress` - * + * * @param targetChain in Wormhole Chain ID format * @param receiverValue msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units) * @param encodedExecutionParameters encoded information on how to execute delivery that may impact pricing @@ -345,7 +464,7 @@ interface IWormholeRelayerSend is IWormholeRelayerBase { * @return nativePriceQuote Price, in units of current chain currency, that the delivery provider charges to perform the relay * @return encodedExecutionInfo encoded information on how the delivery will be executed * e.g. for version EVM_V1, this is a struct that encodes the `gasLimit` and `targetChainRefundPerGasUnused` - * (which is the amount of target chain currency that will be refunded per unit of gas unused, + * (which is the amount of target chain currency that will be refunded per unit of gas unused, * if a refundAddress is specified) */ function quoteDeliveryPrice( @@ -358,17 +477,18 @@ interface IWormholeRelayerSend is IWormholeRelayerBase { /** * @notice Returns the (extra) amount of target chain currency that `targetAddress` * will be called with, if the `paymentForExtraReceiverValue` field is set to `currentChainAmount` - * + * * @param targetChain in Wormhole Chain ID format * @param currentChainAmount The value that `paymentForExtraReceiverValue` will be set to * @param deliveryProviderAddress The address of the desired delivery provider's implementation of IDeliveryProvider * @return targetChainAmount The amount such that if `targetAddress` will be called with `msg.value` equal to * receiverValue + targetChainAmount */ - function quoteNativeForChain(uint16 targetChain, uint256 currentChainAmount, address deliveryProviderAddress) - external - view - returns (uint256 targetChainAmount); + function quoteNativeForChain( + uint16 targetChain, + uint256 currentChainAmount, + address deliveryProviderAddress + ) external view returns (uint256 targetChainAmount); /** * @notice Returns the address of the current default delivery provider @@ -380,7 +500,7 @@ interface IWormholeRelayerSend is IWormholeRelayerBase { /** * @title IWormholeRelayerDelivery - * @notice The interface to execute deliveries. Only relevant for Delivery Providers + * @notice The interface to execute deliveries. Only relevant for Delivery Providers */ interface IWormholeRelayerDelivery is IWormholeRelayerBase { enum DeliveryStatus { @@ -404,7 +524,7 @@ interface IWormholeRelayerDelivery is IWormholeRelayerBase { * corresponding to this delivery request * @custom:member deliveryVaaHash - The hash of the delivery VAA corresponding to this delivery * request - * @custom:member gasUsed - The amount of gas that was used to call your target contract + * @custom:member gasUsed - The amount of gas that was used to call your target contract * @custom:member status: * - RECEIVER_FAILURE, if the target contract reverts * - SUCCESS, if the target contract doesn't revert @@ -433,14 +553,14 @@ interface IWormholeRelayerDelivery is IWormholeRelayerBase { /** * @notice The delivery provider calls `deliver` to relay messages as described by one delivery instruction - * + * * The delivery provider must pass in the specified (by VaaKeys[]) signed wormhole messages (VAAs) from the source chain * as well as the signed wormhole message with the delivery instructions (the delivery VAA) * * The messages will be relayed to the target address (with the specified gas limit and receiver value) iff the following checks are met: * - the delivery VAA has a valid signature * - the delivery VAA's emitter is one of these WormholeRelayer contracts - * - the delivery provider passed in at least enough of this chain's currency as msg.value (enough meaning the maximum possible refund) + * - the delivery provider passed in at least enough of this chain's currency as msg.value (enough meaning the maximum possible refund) * - the instruction's target chain is this chain * - the relayed signed VAAs match the descriptions in container.messages (the VAA hashes match, or the emitter address, sequence number pair matches, depending on the description given) * @@ -479,6 +599,7 @@ error RequestedGasLimitTooLow(); error DeliveryProviderDoesNotSupportTargetChain(address relayer, uint16 chainId); error DeliveryProviderCannotReceivePayment(); +error DeliveryProviderDoesNotSupportMessageKeyType(uint8 keyType); //When calling `delivery()` a second time even though a delivery is already in progress error ReentrantDelivery(address msgSender, address lockedBy); @@ -486,12 +607,13 @@ error ReentrantDelivery(address msgSender, address lockedBy); error InvalidPayloadId(uint8 parsed, uint8 expected); error InvalidPayloadLength(uint256 received, uint256 expected); error InvalidVaaKeyType(uint8 parsed); +error TooManyMessageKeys(uint256 numMessageKeys); error InvalidDeliveryVaa(string reason); //When the delivery VAA (signed wormhole message with delivery instructions) was not emitted by the // registered WormholeRelayer contract error InvalidEmitter(bytes32 emitter, bytes32 registered, uint16 chainId); -error VaaKeysLengthDoesNotMatchVaasLength(uint256 keys, uint256 vaas); +error MessageKeysLengthDoesNotMatchMessagesLength(uint256 keys, uint256 vaas); error VaaKeysDoNotMatchVaas(uint8 index); //When someone tries to call an external function of the WormholeRelayer that is only intended to be // called by the WormholeRelayer itself (to allow retroactive reverts for atomicity) @@ -512,4 +634,4 @@ error InsufficientRelayerFunds(uint256 msgValue, uint256 minimum); //When a bytes32 field can't be converted into a 20 byte EVM address, because the 12 padding bytes // are non-zero (duplicated from Utils.sol) -error NotAnEvmAddress(bytes32); +error NotAnEvmAddress(bytes32); \ No newline at end of file From d354d63a4f1a5cd745fea4fd532d23df14865a0f Mon Sep 17 00:00:00 2001 From: Joe Howarth Date: Tue, 29 Aug 2023 16:51:22 -0700 Subject: [PATCH 02/23] save --- src/CCTPAndTokenBridgeBase.sol | 112 -------------------- src/CCTPBase.sol | 148 +++++++++++++++++++++++++++ src/WormholeRelayerSDK.sol | 1 + test/CCTPBase.t.sol | 180 +++++++++++++++++++++++++++++++++ test/Fork.t.sol | 2 - 5 files changed, 329 insertions(+), 114 deletions(-) delete mode 100644 src/CCTPAndTokenBridgeBase.sol create mode 100644 src/CCTPBase.sol create mode 100644 test/CCTPBase.t.sol diff --git a/src/CCTPAndTokenBridgeBase.sol b/src/CCTPAndTokenBridgeBase.sol deleted file mode 100644 index e825208..0000000 --- a/src/CCTPAndTokenBridgeBase.sol +++ /dev/null @@ -1,112 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import "./interfaces/IWormholeReceiver.sol"; -import "./interfaces/IWormholeRelayer.sol"; -import "./interfaces/ITokenBridge.sol"; -import {IERC20} from "./interfaces/IERC20.sol"; -import "./interfaces/CCTPInterfaces/ITokenMessenger.sol"; -import "./interfaces/CCTPInterfaces/IMessageTransmitter.sol"; - -import "./Utils.sol"; -import "./Base.sol"; - - - -abstract contract CCTPAndTokenBridgeBase is Base { - ITokenBridge immutable tokenBridge; - ITokenMessenger immutable circleTokenMessenger; - IMessageTransmitter immutable circleMessageTransmitter; - - constructor(address _wormholeRelayer, address _tokenBridge, address _wormhole, address _circleMessageTransmitter, address _circleTokenMessenger) Base(_wormholeRelayer, _wormhole) { - tokenBridge = ITokenBridge(_tokenBridge); - circleTokenMessenger = ITokenMessenger(_circleTokenMessenger); - circleMessageTransmitter = IMessageTransmitter(_circleMessageTransmitter); - } - - function getCCTPDomain(uint16 targetChain) internal pure returns (uint32) { - if(targetChain == 2) { - return 0; - } else if(targetChain == 6) { - return 1; - } else { // TODO: Add arbitrum and optimism { - revert("Wrong CCTP Domain"); - } - } - -} - - -abstract contract CCTPSender is CCTPAndTokenBridgeBase { - uint8 internal constant CONSISTENCY_LEVEL_FINALIZED = 15; - /** - * transferTokens wraps common boilerplate for sending tokens to another chain using IWormholeRelayer - * - approves tokenBridge to spend 'amount' of 'token' - * - emits token transfer VAA - * - returns VAA key for inclusion in WormholeRelayer `additionalVaas` argument - * - * Note: this requires that only the targetAddress can redeem transfers. - * - */ - function transferCircle(address token, uint256 amount, uint16 targetChain, address targetAddress) - internal - returns (MessageKey memory) - { - uint64 nonce = circleTokenMessenger.depositForBurnWithCaller( - amount, - getCCTPDomain(targetChain), - toWormholeFormat(targetAddress), - token, - toWormholeFormat(targetAddress) - ); - return MessageKey( - 2, abi.encodePacked(nonce, getCCTPDomain(wormhole.chainId())) // fix this encoding TODO! - ); - } - - function sendCircleWithPayloadToEvm( - uint16 targetChain, - address targetAddress, - bytes memory payload, - uint256 receiverValue, - uint256 gasLimit, - address token, - uint256 amount - ) internal returns (uint64) { - MessageKey[] memory messageKeys = new MessageKey[](1); - messageKeys[0] = transferCircle(token, amount, targetChain, targetAddress); - - (uint256 cost,) = wormholeRelayer.quoteEVMDeliveryPrice(targetChain, receiverValue, gasLimit); - return wormholeRelayer.sendToEvm{value: cost}( - targetChain, targetAddress, payload, receiverValue, 0, gasLimit, targetChain, address(0x0), wormholeRelayer.getDefaultDeliveryProvider(), messageKeys, CONSISTENCY_LEVEL_FINALIZED - ); - } -} - -abstract contract CCTPReceiver is CCTPAndTokenBridgeBase { - struct TokenReceived { - bytes32 tokenHomeAddress; - uint16 tokenHomeChain; - address tokenAddress; // wrapped address if tokenHomeChain !== this chain, else tokenHomeAddress (in evm address format) - uint256 amount; - uint256 amountNormalized; // if decimals > 8, normalized to 8 decimal places - } - - function receiveWormholeMessages( - bytes memory payload, - bytes[] memory additionalVaas, - bytes32 sourceAddress, - uint16 sourceChain, - bytes32 deliveryHash - ) external payable { - - } - - function receivePayloadAndTokens( - bytes memory payload, - TokenReceived[] memory receivedTokens, - bytes32 sourceAddress, - uint16 sourceChain, - bytes32 deliveryHash - ) internal virtual {} -} diff --git a/src/CCTPBase.sol b/src/CCTPBase.sol new file mode 100644 index 0000000..6155c44 --- /dev/null +++ b/src/CCTPBase.sol @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "./interfaces/IWormholeReceiver.sol"; +import "./interfaces/IWormholeRelayer.sol"; +import "./interfaces/ITokenBridge.sol"; +import {IERC20} from "./interfaces/IERC20.sol"; +import "./interfaces/CCTPInterfaces/ITokenMessenger.sol"; +import "./interfaces/CCTPInterfaces/IMessageTransmitter.sol"; + +import "./Utils.sol"; +import "./TokenBase.sol"; + +library CCTPMessageLib { + uint8 constant CCTP_KEY_TYPE = 2; + + // encoded using standard abi.encode + struct CCTPKey { + uint32 domain; + uint64 nonce; + } + + // encoded using standard abi.encode + struct CCTPMessage { + bytes message; + bytes signature; + } +} + +abstract contract CCTPBase is TokenBase { + ITokenMessenger immutable circleTokenMessenger; + IMessageTransmitter immutable circleMessageTransmitter; + address immutable USDC; + + constructor( + address _wormholeRelayer, + address _tokenBridge, + address _wormhole, + address _circleMessageTransmitter, + address _circleTokenMessenger, + address _USDC + ) TokenBase(_wormholeRelayer, _tokenBridge, _wormhole) { + circleTokenMessenger = ITokenMessenger(_circleTokenMessenger); + circleMessageTransmitter = IMessageTransmitter(_circleMessageTransmitter); + USDC = _USDC; + } + + function getCCTPDomain(uint16 targetChain) internal pure returns (uint32) { + if (targetChain == 2) { + return 0; + } else if (targetChain == 6) { + return 1; + } else { + // TODO: Add arbitrum and optimism { + revert("Wrong CCTP Domain"); + } + } + + function redeemUSDC(bytes memory cctpMessage) internal returns (uint256 amount) { + CCTPMessageLib.CCTPMessage memory message = abi.decode(cctpMessage, (CCTPMessageLib.CCTPMessage)); + + uint256 beforeBalance = IERC20(USDC).balanceOf(address(this)); + circleMessageTransmitter.receiveMessage(message.message, message.signature); + return IERC20(USDC).balanceOf(address(this)) - beforeBalance; + } +} + +abstract contract CCTPSender is CCTPBase { + uint8 internal constant CONSISTENCY_LEVEL_FINALIZED = 15; + + using CCTPMessageLib for *; + + /** + * transferTokens wraps common boilerplate for sending tokens to another chain using IWormholeRelayer + * - approves tokenBridge to spend 'amount' of 'token' + * - emits token transfer VAA + * - returns VAA key for inclusion in WormholeRelayer `additionalVaas` argument + * + * Note: this requires that only the targetAddress can redeem transfers. + * + */ + + function transferUSDC(uint256 amount, uint16 targetChain, address targetAddress) + internal + returns (MessageKey memory) + { + uint64 nonce = circleTokenMessenger.depositForBurnWithCaller( + amount, getCCTPDomain(targetChain), toWormholeFormat(targetAddress), USDC, toWormholeFormat(targetAddress) + ); + return MessageKey( + CCTPMessageLib.CCTP_KEY_TYPE, abi.encode(CCTPMessageLib.CCTPKey(getCCTPDomain(targetChain), nonce)) + ); + } + + function sendUSDCWithPayloadToEvm( + uint16 targetChain, + address targetAddress, + bytes memory payload, + uint256 receiverValue, + uint256 gasLimit, + uint256 amount + ) internal returns (uint64) { + MessageKey[] memory messageKeys = new MessageKey[](1); + messageKeys[0] = transferUSDC(amount, targetChain, targetAddress); + + (uint256 cost,) = wormholeRelayer.quoteEVMDeliveryPrice(targetChain, receiverValue, gasLimit); + return wormholeRelayer.sendToEvm{value: cost}( + targetChain, + targetAddress, + payload, + receiverValue, + 0, + gasLimit, + targetChain, + address(0x0), + wormholeRelayer.getDefaultDeliveryProvider(), + messageKeys, + CONSISTENCY_LEVEL_FINALIZED + ); + } +} + +abstract contract CCTPReceiver is CCTPBase { + function receiveWormholeMessages( + bytes memory payload, + bytes[] memory additionalMessages, + bytes32 sourceAddress, + uint16 sourceChain, + bytes32 deliveryHash + ) external payable { + require(additionalMessages.length > 1, "CCTP: At most one Message is supported"); + + uint256 amountUSDCReceived; + if (additionalMessages.length == 1) { + amountUSDCReceived = redeemUSDC(additionalMessages[0]); + } + + receivePayloadAndUSDC(payload, amountUSDCReceived, sourceAddress, sourceChain, deliveryHash); + } + + function receivePayloadAndUSDC( + bytes memory payload, + uint256 amountUSDCReceived, + bytes32 sourceAddress, + uint16 sourceChain, + bytes32 deliveryHash + ) internal virtual {} +} \ No newline at end of file diff --git a/src/WormholeRelayerSDK.sol b/src/WormholeRelayerSDK.sol index 7470973..1d529c7 100644 --- a/src/WormholeRelayerSDK.sol +++ b/src/WormholeRelayerSDK.sol @@ -6,3 +6,4 @@ import "./interfaces/IWormholeRelayer.sol"; import "./Utils.sol"; import {Base} from "./Base.sol"; import {TokenBase, TokenReceiver, TokenSender} from "./TokenBase.sol"; +import {CCTPBase, CCTPReceiver, CCTPSender} from "./CCTPBase.sol"; diff --git a/test/CCTPBase.t.sol b/test/CCTPBase.t.sol new file mode 100644 index 0000000..ce73614 --- /dev/null +++ b/test/CCTPBase.t.sol @@ -0,0 +1,180 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "../src/WormholeRelayerSDK.sol"; +import "../src/interfaces/IWormholeReceiver.sol"; +import "../src/interfaces/IWormholeRelayer.sol"; +import "../src/interfaces/IERC20.sol"; + +import "../src/testing/WormholeRelayerTest.sol"; + +import "../src/WormholeRelayerSDK.sol"; +import "../src/Utils.sol"; + +import "forge-std/console.sol"; + +contract CCTPToy is CCTPSender, CCTPReceiver { + uint256 constant GAS_LIMIT = 250_000; + + function quoteCrossChainDeposit(uint16 targetChain) public view returns (uint256 cost) { + // Cost of delivering token and payload to targetChain + uint256 deliveryCost; + (deliveryCost,) = wormholeRelayer.quoteEVMDeliveryPrice(targetChain, 0, GAS_LIMIT); + } + + function sendCrossChainDeposit(uint16 targetChain, address recipient, uint256 amount) + public + payable + { + uint256 cost = quoteCrossChainDeposit(targetChain); + require(msg.value == cost, "msg.value must be quoteCrossChainDeposit(targetChain)"); + + IERC20(USDC).transferFrom(msg.sender, address(this), amount); + + bytes memory payload = abi.encode(recipient); + sendUSDCWithPayloadToEvm( + targetChain, + fromWormholeFormat(registeredSenders[targetChain]), // address (on targetChain) to send token and payload to + payload, + 0, // receiver value + GAS_LIMIT, + amount + ); + } + + // function sendCrossChainDeposit( + // uint16 targetChain, + // address recipient, + // uint256 amount, + // uint16 refundChain, + // address refundAddress + // ) public payable { + // uint256 cost = quoteCrossChainDeposit(targetChain); + // require(msg.value == cost, "msg.value must be quoteCrossChainDeposit(targetChain)"); + + // IERC20(token).transferFrom(msg.sender, address(this), amount); + + // bytes memory payload = abi.encode(recipient); + // sendUSDCWithPayloadToEvm( + // targetChain, + // fromWormholeFormat(registeredSenders[targetChain]), // address (on targetChain) to send token and payload to + // payload, + // 0, // receiver value + // GAS_LIMIT, + // amount, + // refundChain, + // refundAddress + // ); + // } + + function receivePayloadAndUSDC( + bytes memory payload, + uint256 amount, + bytes32 sourceAddress, + uint16 sourceChain, + bytes32 // deliveryHash + ) internal override onlyWormholeRelayer isRegisteredSender(sourceChain, sourceAddress) { + require(receivedTokens.length == 1, "Expected 1 token transfers"); + address recipient = abi.decode(payload, (address)); + IERC20(receivedTokens[0].tokenAddress).transfer(recipient, receivedTokens[0].amount); + } +} + +contract WormholeSDKTest is WormholeRelayerBasicTest { + Toy toySource; + Toy toyTarget; + CCTPToy CCTPToySource; + CCTPToy CCTPToyTarget; + ERC20Mock public token; + + function setUpSource() public override { + toySource = new Toy(address(relayerSource), address(wormholeSource)); + toySource.setRegisteredSender(targetChain, toWormholeFormat(address(this))); + + CCTPToySource = new CCTPToy(address(relayerSource), address(tokenBridgeSource), address(wormholeSource)); + + token = createAndAttestToken(sourceChain); + } + + function setUpTarget() public override { + toyTarget = new Toy(address(relayerTarget), address(wormholeTarget)); + toyTarget.setRegisteredSender(sourceChain, toWormholeFormat(address(this))); + + CCTPToyTarget = new CCTPToy(address(relayerTarget), address(tokenBridgeTarget), address(wormholeTarget)); + } + + function setUpGeneral() public override { + vm.selectFork(sourceFork); + CCTPToySource.setRegisteredSender(targetChain, toWormholeFormat(address(CCTPToyTarget))); + + vm.selectFork(targetFork); + CCTPToyTarget.setRegisteredSender(sourceChain, toWormholeFormat(address(CCTPToySource))); + } + + function testSendMessage() public { + vm.recordLogs(); + (uint256 cost,) = relayerSource.quoteEVMDeliveryPrice(targetChain, 1e17, 100_000); + relayerSource.sendPayloadToEvm{value: cost}(targetChain, address(toyTarget), abi.encode(55), 1e17, 100_000); + performDelivery(); + + vm.selectFork(targetFork); + require(55 == toyTarget.payloadReceived()); + } + + function testSendMessageSource() public { + vm.selectFork(targetFork); + vm.recordLogs(); + + (uint256 cost,) = relayerTarget.quoteEVMDeliveryPrice(sourceChain, 1e17, 100_000); + relayerTarget.sendPayloadToEvm{value: cost}(sourceChain, address(toySource), abi.encode(56), 1e17, 100_000); + performDelivery(); + + vm.selectFork(sourceFork); + require(56 == toySource.payloadReceived()); + } + + function testSendToken() public { + vm.selectFork(sourceFork); + + uint256 amount = 19e17; + token.approve(address(CCTPToySource), amount); + + vm.selectFork(targetFork); + address recipient = 0x1234567890123456789012345678901234567890; + + vm.selectFork(sourceFork); + uint256 cost = CCTPToySource.quoteCrossChainDeposit(targetChain); + + vm.recordLogs(); + CCTPToySource.sendCrossChainDeposit{value: cost}(targetChain, recipient, amount, address(token)); + performDelivery(); + + vm.selectFork(targetFork); + address wormholeWrappedToken = tokenBridgeTarget.wrappedAsset(sourceChain, toWormholeFormat(address(token))); + assertEq(IERC20(wormholeWrappedToken).balanceOf(recipient), amount); + } + + function testSendTokenWithRefund() public { + vm.selectFork(sourceFork); + + uint256 amount = 19e17; + token.approve(address(CCTPToySource), amount); + + vm.selectFork(targetFork); + address recipient = 0x1234567890123456789012345678901234567890; + address refundAddress = 0x2234567890123456789012345678901234567890; + vm.selectFork(sourceFork); + uint256 cost = CCTPToySource.quoteCrossChainDeposit(targetChain); + + vm.recordLogs(); + CCTPToySource.sendCrossChainDeposit{value: cost}( + targetChain, recipient, amount, address(token), targetChain, refundAddress + ); + performDelivery(); + + vm.selectFork(targetFork); + address wormholeWrappedToken = tokenBridgeTarget.wrappedAsset(sourceChain, toWormholeFormat(address(token))); + assertEq(IERC20(wormholeWrappedToken).balanceOf(recipient), amount); + assertTrue(refundAddress.balance > 0); + } +} diff --git a/test/Fork.t.sol b/test/Fork.t.sol index 698ea83..52e3028 100644 --- a/test/Fork.t.sol +++ b/test/Fork.t.sol @@ -138,11 +138,9 @@ contract WormholeSDKTest is WormholeRelayerBasicTest { function setUpGeneral() public override { vm.selectFork(sourceFork); tokenToySource.setRegisteredSender(targetChain, toWormholeFormat(address(tokenToyTarget))); - vm.selectFork(targetFork); tokenToyTarget.setRegisteredSender(sourceChain, toWormholeFormat(address(tokenToySource))); - } function testSendMessage() public { From 0f6d8e45a3d132529ddfc2c79703c1656389b906 Mon Sep 17 00:00:00 2001 From: Joe Howarth Date: Tue, 5 Sep 2023 11:27:31 -0700 Subject: [PATCH 03/23] save --- src/CCTPBase.sol | 13 ++- src/testing/CCTPMocks.sol | 74 ++++++++++++++++++ src/testing/ERC20Mock.sol | 4 + test/CCTPBase.t.sol | 161 +++++++++++++++----------------------- 4 files changed, 154 insertions(+), 98 deletions(-) create mode 100644 src/testing/CCTPMocks.sol diff --git a/src/CCTPBase.sol b/src/CCTPBase.sol index 6155c44..11aecb8 100644 --- a/src/CCTPBase.sol +++ b/src/CCTPBase.sol @@ -85,7 +85,11 @@ abstract contract CCTPSender is CCTPBase { returns (MessageKey memory) { uint64 nonce = circleTokenMessenger.depositForBurnWithCaller( - amount, getCCTPDomain(targetChain), toWormholeFormat(targetAddress), USDC, toWormholeFormat(targetAddress) + amount, + getCCTPDomain(targetChain), + addressToBytes32CCTP(targetAddress), + USDC, + addressToBytes32CCTP(targetAddress) ); return MessageKey( CCTPMessageLib.CCTP_KEY_TYPE, abi.encode(CCTPMessageLib.CCTPKey(getCCTPDomain(targetChain), nonce)) @@ -115,9 +119,14 @@ abstract contract CCTPSender is CCTPBase { address(0x0), wormholeRelayer.getDefaultDeliveryProvider(), messageKeys, + // new MessageKey[](0), CONSISTENCY_LEVEL_FINALIZED ); } + + function addressToBytes32CCTP(address addr) private pure returns (bytes32) { + return bytes32(uint256(uint160(addr))); + } } abstract contract CCTPReceiver is CCTPBase { @@ -145,4 +154,4 @@ abstract contract CCTPReceiver is CCTPBase { uint16 sourceChain, bytes32 deliveryHash ) internal virtual {} -} \ No newline at end of file +} diff --git a/src/testing/CCTPMocks.sol b/src/testing/CCTPMocks.sol new file mode 100644 index 0000000..0acfc3e --- /dev/null +++ b/src/testing/CCTPMocks.sol @@ -0,0 +1,74 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; + +import "../interfaces/CCTPInterfaces/ITokenMessenger.sol"; +import "../interfaces/CCTPInterfaces/IMessageTransmitter.sol"; +import "./ERC20Mock.sol"; + +struct MockMessage { + uint256 amount; + uint32 destinationDomain; + bytes32 mintRecipient; + address burnToken; + bytes32 destinationCaller; +} + +contract MockMessageTransmitter is IMessageTransmitter { + ERC20Mock public immutable USDC; + + constructor(ERC20Mock _USDC) { + USDC = _USDC; + } + + function receiveMessage(bytes calldata _message, bytes calldata) public returns (bool success) { + MockMessage memory message = abi.decode(_message, (MockMessage)); + require (msg.sender == bytes32ToAddress(message.destinationCaller), "Wrong caller"); + + ERC20Mock(USDC).mint(bytes32ToAddress(message.mintRecipient), message.amount); + return true; + } + + function bytes32ToAddress(bytes32 _buf) public pure returns (address) { + return address(uint160(uint256(_buf))); + } + + /* Unimplemented functions */ + + function sendMessage(uint32, bytes32, bytes calldata) external pure returns (uint64) { + revert("Unimplemented"); + } + + function sendMessageWithCaller(uint32, bytes32, bytes32, bytes calldata) external pure returns (uint64) { + revert("Unimplemented"); + } + + function replaceMessage(bytes calldata, bytes calldata, bytes calldata, bytes32) external pure { + revert("Unimplemented"); + } +} + +contract MockTokenMessenger is ITokenMessenger { + ERC20Mock public immutable USDC; + uint64 public nonce; + + MockMessage message; + bytes packedMessage; + + constructor(ERC20Mock _USDC) { + USDC = _USDC; + } + + function depositForBurnWithCaller( + uint256 amount, + uint32 destinationDomain, + bytes32 mintRecipient, + address burnToken, + bytes32 destinationCaller + ) public returns (uint64) { + ERC20Mock(USDC).burn(msg.sender, amount); + nonce += 1; + message = MockMessage(amount, destinationDomain, mintRecipient, burnToken, destinationCaller); + packedMessage = abi.encode(message); + return nonce; + } +} diff --git a/src/testing/ERC20Mock.sol b/src/testing/ERC20Mock.sol index 3fdee7a..33caf2f 100644 --- a/src/testing/ERC20Mock.sol +++ b/src/testing/ERC20Mock.sol @@ -199,4 +199,8 @@ contract ERC20Mock is ERC20 { function mint(address to, uint256 amount) public { _mint(to, amount); } + + function burn(address from, uint256 amount) public { + _burn(from, amount); + } } diff --git a/test/CCTPBase.t.sol b/test/CCTPBase.t.sol index ce73614..b6ae67d 100644 --- a/test/CCTPBase.t.sol +++ b/test/CCTPBase.t.sol @@ -7,6 +7,7 @@ import "../src/interfaces/IWormholeRelayer.sol"; import "../src/interfaces/IERC20.sol"; import "../src/testing/WormholeRelayerTest.sol"; +import "../src/testing/CCTPMocks.sol"; import "../src/WormholeRelayerSDK.sol"; import "../src/Utils.sol"; @@ -16,16 +17,22 @@ import "forge-std/console.sol"; contract CCTPToy is CCTPSender, CCTPReceiver { uint256 constant GAS_LIMIT = 250_000; + constructor( + address _wormholeRelayer, + address _tokenBridge, + address _wormhole, + address _circleMessageTransmitter, + address _circleTokenMessenger, + address _USDC + ) CCTPBase(_wormholeRelayer, _tokenBridge, _wormhole, _circleMessageTransmitter, _circleTokenMessenger, _USDC) {} + function quoteCrossChainDeposit(uint16 targetChain) public view returns (uint256 cost) { // Cost of delivering token and payload to targetChain uint256 deliveryCost; (deliveryCost,) = wormholeRelayer.quoteEVMDeliveryPrice(targetChain, 0, GAS_LIMIT); } - function sendCrossChainDeposit(uint16 targetChain, address recipient, uint256 amount) - public - payable - { + function sendCrossChainDeposit(uint16 targetChain, address recipient, uint256 amount) public payable { uint256 cost = quoteCrossChainDeposit(targetChain); require(msg.value == cost, "msg.value must be quoteCrossChainDeposit(targetChain)"); @@ -42,31 +49,6 @@ contract CCTPToy is CCTPSender, CCTPReceiver { ); } - // function sendCrossChainDeposit( - // uint16 targetChain, - // address recipient, - // uint256 amount, - // uint16 refundChain, - // address refundAddress - // ) public payable { - // uint256 cost = quoteCrossChainDeposit(targetChain); - // require(msg.value == cost, "msg.value must be quoteCrossChainDeposit(targetChain)"); - - // IERC20(token).transferFrom(msg.sender, address(this), amount); - - // bytes memory payload = abi.encode(recipient); - // sendUSDCWithPayloadToEvm( - // targetChain, - // fromWormholeFormat(registeredSenders[targetChain]), // address (on targetChain) to send token and payload to - // payload, - // 0, // receiver value - // GAS_LIMIT, - // amount, - // refundChain, - // refundAddress - // ); - // } - function receivePayloadAndUSDC( bytes memory payload, uint256 amount, @@ -74,70 +56,57 @@ contract CCTPToy is CCTPSender, CCTPReceiver { uint16 sourceChain, bytes32 // deliveryHash ) internal override onlyWormholeRelayer isRegisteredSender(sourceChain, sourceAddress) { - require(receivedTokens.length == 1, "Expected 1 token transfers"); - address recipient = abi.decode(payload, (address)); - IERC20(receivedTokens[0].tokenAddress).transfer(recipient, receivedTokens[0].amount); + (address recipient, uint256 expectedAmount) = abi.decode(payload, (address, uint256)); + require(amount == expectedAmount, "amount != payload.expectedAmount"); + IERC20(USDC).transfer(recipient, amount); } } contract WormholeSDKTest is WormholeRelayerBasicTest { - Toy toySource; - Toy toyTarget; CCTPToy CCTPToySource; CCTPToy CCTPToyTarget; - ERC20Mock public token; + ERC20Mock USDCSource; + ERC20Mock USDCTarget; + MockMessageTransmitter circleMessageTransmitter; + MockTokenMessenger circleTokenMessenger; function setUpSource() public override { - toySource = new Toy(address(relayerSource), address(wormholeSource)); - toySource.setRegisteredSender(targetChain, toWormholeFormat(address(this))); - - CCTPToySource = new CCTPToy(address(relayerSource), address(tokenBridgeSource), address(wormholeSource)); - - token = createAndAttestToken(sourceChain); + CCTPToySource = new CCTPToy( + address(relayerSource), + address(tokenBridgeSource), + address(wormholeSource), + address(circleMessageTransmitter), + address(circleTokenMessenger), + address(USDCSource) + ); + USDCSource = createAndAttestToken(sourceChain); + circleMessageTransmitter = new MockMessageTransmitter(USDCSource); } function setUpTarget() public override { - toyTarget = new Toy(address(relayerTarget), address(wormholeTarget)); - toyTarget.setRegisteredSender(sourceChain, toWormholeFormat(address(this))); - - CCTPToyTarget = new CCTPToy(address(relayerTarget), address(tokenBridgeTarget), address(wormholeTarget)); - } - - function setUpGeneral() public override { - vm.selectFork(sourceFork); - CCTPToySource.setRegisteredSender(targetChain, toWormholeFormat(address(CCTPToyTarget))); - - vm.selectFork(targetFork); - CCTPToyTarget.setRegisteredSender(sourceChain, toWormholeFormat(address(CCTPToySource))); - } - - function testSendMessage() public { - vm.recordLogs(); - (uint256 cost,) = relayerSource.quoteEVMDeliveryPrice(targetChain, 1e17, 100_000); - relayerSource.sendPayloadToEvm{value: cost}(targetChain, address(toyTarget), abi.encode(55), 1e17, 100_000); - performDelivery(); - - vm.selectFork(targetFork); - require(55 == toyTarget.payloadReceived()); + CCTPToyTarget = new CCTPToy( + address(relayerTarget), + address(tokenBridgeTarget), + address(wormholeTarget), + address(circleMessageTransmitter), + address(circleTokenMessenger), + address(USDCTarget) + ); + USDCTarget = createAndAttestToken(targetChain); + circleTokenMessenger = new MockTokenMessenger(USDCTarget); } - function testSendMessageSource() public { - vm.selectFork(targetFork); - vm.recordLogs(); + // function setUpGeneral() public override { + // vm.selectFork(sourceFork); - (uint256 cost,) = relayerTarget.quoteEVMDeliveryPrice(sourceChain, 1e17, 100_000); - relayerTarget.sendPayloadToEvm{value: cost}(sourceChain, address(toySource), abi.encode(56), 1e17, 100_000); - performDelivery(); - - vm.selectFork(sourceFork); - require(56 == toySource.payloadReceived()); - } + // vm.selectFork(targetFork); + // } function testSendToken() public { vm.selectFork(sourceFork); uint256 amount = 19e17; - token.approve(address(CCTPToySource), amount); + USDCSource.approve(address(CCTPToySource), amount); vm.selectFork(targetFork); address recipient = 0x1234567890123456789012345678901234567890; @@ -146,35 +115,35 @@ contract WormholeSDKTest is WormholeRelayerBasicTest { uint256 cost = CCTPToySource.quoteCrossChainDeposit(targetChain); vm.recordLogs(); - CCTPToySource.sendCrossChainDeposit{value: cost}(targetChain, recipient, amount, address(token)); + CCTPToySource.sendCrossChainDeposit{value: cost}(targetChain, recipient, amount); performDelivery(); vm.selectFork(targetFork); - address wormholeWrappedToken = tokenBridgeTarget.wrappedAsset(sourceChain, toWormholeFormat(address(token))); - assertEq(IERC20(wormholeWrappedToken).balanceOf(recipient), amount); + // address wormholeWrappedToken = tokenBridgeTarget.wrappedAsset(sourceChain, toWormholeFormat(address(token))); + // assertEq(IERC20(wormholeWrappedToken).balanceOf(recipient), amount); } - function testSendTokenWithRefund() public { - vm.selectFork(sourceFork); + // function testSendTokenWithRefund() public { + // vm.selectFork(sourceFork); - uint256 amount = 19e17; - token.approve(address(CCTPToySource), amount); + // uint256 amount = 19e17; + // token.approve(address(CCTPToySource), amount); - vm.selectFork(targetFork); - address recipient = 0x1234567890123456789012345678901234567890; - address refundAddress = 0x2234567890123456789012345678901234567890; - vm.selectFork(sourceFork); - uint256 cost = CCTPToySource.quoteCrossChainDeposit(targetChain); + // vm.selectFork(targetFork); + // address recipient = 0x1234567890123456789012345678901234567890; + // address refundAddress = 0x2234567890123456789012345678901234567890; + // vm.selectFork(sourceFork); + // uint256 cost = CCTPToySource.quoteCrossChainDeposit(targetChain); - vm.recordLogs(); - CCTPToySource.sendCrossChainDeposit{value: cost}( - targetChain, recipient, amount, address(token), targetChain, refundAddress - ); - performDelivery(); + // vm.recordLogs(); + // CCTPToySource.sendCrossChainDeposit{value: cost}( + // targetChain, recipient, amount, address(token), targetChain, refundAddress + // ); + // performDelivery(); - vm.selectFork(targetFork); - address wormholeWrappedToken = tokenBridgeTarget.wrappedAsset(sourceChain, toWormholeFormat(address(token))); - assertEq(IERC20(wormholeWrappedToken).balanceOf(recipient), amount); - assertTrue(refundAddress.balance > 0); - } + // vm.selectFork(targetFork); + // address wormholeWrappedToken = tokenBridgeTarget.wrappedAsset(sourceChain, toWormholeFormat(address(token))); + // assertEq(IERC20(wormholeWrappedToken).balanceOf(recipient), amount); + // assertTrue(refundAddress.balance > 0); + // } } From 142dad0f9e13f6fea9d46249cec29d95b508ed79 Mon Sep 17 00:00:00 2001 From: derpy-duck <115193320+derpy-duck@users.noreply.github.com> Date: Tue, 5 Sep 2023 18:47:02 -0400 Subject: [PATCH 04/23] Progress + added some testnet chains --- src/CCTPBase.sol | 8 +- src/testing/WormholeRelayerTest.sol | 24 +++ .../helpers/DeliveryInstructionDecoder.sol | 158 ++++++++++++++---- src/testing/helpers/MockOffchainRelayer.sol | 155 ++++++++++++----- test/CCTPBase.t.sol | 87 +++++++--- 5 files changed, 333 insertions(+), 99 deletions(-) diff --git a/src/CCTPBase.sol b/src/CCTPBase.sol index 11aecb8..79a89b4 100644 --- a/src/CCTPBase.sol +++ b/src/CCTPBase.sol @@ -50,8 +50,11 @@ abstract contract CCTPBase is TokenBase { return 0; } else if (targetChain == 6) { return 1; + } else if (targetChain == 23) { + return 3; + } else if (targetChain == 24) { + return 2; } else { - // TODO: Add arbitrum and optimism { revert("Wrong CCTP Domain"); } } @@ -119,7 +122,6 @@ abstract contract CCTPSender is CCTPBase { address(0x0), wormholeRelayer.getDefaultDeliveryProvider(), messageKeys, - // new MessageKey[](0), CONSISTENCY_LEVEL_FINALIZED ); } @@ -137,7 +139,7 @@ abstract contract CCTPReceiver is CCTPBase { uint16 sourceChain, bytes32 deliveryHash ) external payable { - require(additionalMessages.length > 1, "CCTP: At most one Message is supported"); + require(additionalMessages.length <= 1, "CCTP: At most one Message is supported"); uint256 amountUSDCReceived; if (additionalMessages.length == 1) { diff --git a/src/testing/WormholeRelayerTest.sol b/src/testing/WormholeRelayerTest.sol index a116f00..a16b243 100644 --- a/src/testing/WormholeRelayerTest.sol +++ b/src/testing/WormholeRelayerTest.sol @@ -185,6 +185,14 @@ abstract contract WormholeRelayerTest is Test { } function initChainInfo() private { + chainInfosTestnet[2] = ChainInfo({ + chainId: 2, + name: "goerli - ethereum", + url: "https://ethereum-goerli.publicnode.com", + relayer: IWormholeRelayer(0x28D8F1Be96f97C1387e94A53e00eCcFb4E75175a), + tokenBridge: ITokenBridge(0xF890982f9310df57d00f659cf4fd87e65adEd8d7), + wormhole: IWormhole(0x706abc4E45D419950511e474C7B9Ed348A4a716c) + }); chainInfosTestnet[6] = ChainInfo({ chainId: 6, name: "fuji - avalanche", @@ -225,6 +233,22 @@ abstract contract WormholeRelayerTest is Test { tokenBridge: ITokenBridge(0xbc976D4b9D57E57c3cA52e1Fd136C45FF7955A96), wormhole: IWormhole(0xa5B7D85a8f27dd7907dc8FdC21FA5657D5E2F901) }); + chainInfosTestnet[23] = ChainInfo({ + chainId: 23, + name: "goerli - arbitrum", + url: "https://arbitrum-goerli.publicnode.com", + relayer: IWormholeRelayer(0xAd753479354283eEE1b86c9470c84D42f229FF43), + tokenBridge: ITokenBridge(0x23908A62110e21C04F3A4e011d24F901F911744A), + wormhole: IWormhole(0xC7A204bDBFe983FCD8d8E61D02b475D4073fF97e) + }); + chainInfosTestnet[24] = ChainInfo({ + chainId: 24, + name: "goerli - optimism", + url: "https://optimism-goerli.publicnode.com", + relayer: IWormholeRelayer(0x01A957A525a5b7A72808bA9D10c389674E459891), + tokenBridge: ITokenBridge(0xC7A204bDBFe983FCD8d8E61D02b475D4073fF97e), + wormhole: IWormhole(0x6b9C8671cdDC8dEab9c719bB87cBd3e782bA6a35) + }); chainInfosMainnet[2] = ChainInfo({ chainId: 2, diff --git a/src/testing/helpers/DeliveryInstructionDecoder.sol b/src/testing/helpers/DeliveryInstructionDecoder.sol index 7ca1492..f5ee0a7 100644 --- a/src/testing/helpers/DeliveryInstructionDecoder.sol +++ b/src/testing/helpers/DeliveryInstructionDecoder.sol @@ -4,8 +4,6 @@ pragma solidity ^0.8.13; import "../../../src/interfaces/IWormholeRelayer.sol"; import "./BytesParsing.sol"; -import "forge-std/console.sol"; - uint8 constant VERSION_VAAKEY = 1; uint8 constant VERSION_DELIVERY_OVERRIDE = 1; uint8 constant PAYLOAD_ID_DELIVERY_INSTRUCTION = 1; @@ -25,7 +23,7 @@ struct DeliveryInstruction { bytes32 refundDeliveryProvider; bytes32 sourceDeliveryProvider; bytes32 senderAddress; - VaaKey[] vaaKeys; + MessageKey[] messageKeys; } struct RedeliveryInstruction { @@ -43,7 +41,9 @@ struct DeliveryOverride { bytes32 redeliveryHash; } -function decodeDeliveryInstruction(bytes memory encoded) pure returns (DeliveryInstruction memory strct) { +function decodeDeliveryInstruction( + bytes memory encoded +) pure returns (DeliveryInstruction memory strct) { uint256 offset = checkUint8(encoded, 0, PAYLOAD_ID_DELIVERY_INSTRUCTION); uint256 requestedReceiverValue; @@ -60,7 +60,7 @@ function decodeDeliveryInstruction(bytes memory encoded) pure returns (DeliveryI (strct.refundDeliveryProvider, offset) = encoded.asBytes32Unchecked(offset); (strct.sourceDeliveryProvider, offset) = encoded.asBytes32Unchecked(offset); (strct.senderAddress, offset) = encoded.asBytes32Unchecked(offset); - (strct.vaaKeys, offset) = decodeVaaKeyArray(encoded, offset); + (strct.messageKeys, offset) = decodeMessageKeyArray(encoded, offset); strct.requestedReceiverValue = requestedReceiverValue; strct.extraReceiverValue = extraReceiverValue; @@ -68,16 +68,20 @@ function decodeDeliveryInstruction(bytes memory encoded) pure returns (DeliveryI checkLength(encoded, offset); } -function decodeRedeliveryInstruction(bytes memory encoded) pure returns (RedeliveryInstruction memory strct) { +function decodeRedeliveryInstruction( + bytes memory encoded +) pure returns (RedeliveryInstruction memory strct) { uint256 offset = checkUint8(encoded, 0, PAYLOAD_ID_REDELIVERY_INSTRUCTION); uint256 newRequestedReceiverValue; - + offset = checkUint8(encoded, offset, VAA_KEY_TYPE); (strct.deliveryVaaKey, offset) = decodeVaaKey(encoded, offset); (strct.targetChain, offset) = encoded.asUint16Unchecked(offset); (newRequestedReceiverValue, offset) = encoded.asUint256Unchecked(offset); (strct.newEncodedExecutionInfo, offset) = decodeBytes(encoded, offset); - (strct.newSourceDeliveryProvider, offset) = encoded.asBytes32Unchecked(offset); + (strct.newSourceDeliveryProvider, offset) = encoded.asBytes32Unchecked( + offset + ); (strct.newSenderAddress, offset) = encoded.asBytes32Unchecked(offset); strct.newRequestedReceiverValue = newRequestedReceiverValue; @@ -85,56 +89,123 @@ function decodeRedeliveryInstruction(bytes memory encoded) pure returns (Redeliv checkLength(encoded, offset); } -function encodeVaaKeyArray(VaaKey[] memory vaaKeys) pure returns (bytes memory encoded) { - assert(vaaKeys.length < type(uint8).max); - encoded = abi.encodePacked(uint8(vaaKeys.length)); - for (uint256 i = 0; i < vaaKeys.length;) { - encoded = abi.encodePacked(encoded, encodeVaaKey(vaaKeys[i])); +function vaaKeyArrayToMessageKeyArray( + VaaKey[] memory vaaKeys +) pure returns (MessageKey[] memory msgKeys) { + msgKeys = new MessageKey[](vaaKeys.length); + uint256 len = vaaKeys.length; + for (uint256 i = 0; i < len; ) { + msgKeys[i] = MessageKey(VAA_KEY_TYPE, encodeVaaKey(vaaKeys[i])); unchecked { ++i; } } } -function decodeVaaKeyArray(bytes memory encoded, uint256 startOffset) - pure - returns (VaaKey[] memory vaaKeys, uint256 offset) -{ - uint8 vaaKeysLength; - (vaaKeysLength, offset) = encoded.asUint8Unchecked(startOffset); - vaaKeys = new VaaKey[](vaaKeysLength); - for (uint256 i = 0; i < vaaKeys.length;) { - (vaaKeys[i], offset) = decodeVaaKey(encoded, offset); - unchecked { - ++i; - } +function encodeMessageKey( + MessageKey memory msgKey +) pure returns (bytes memory encoded) { + if (msgKey.keyType == VAA_KEY_TYPE) { + // known length + encoded = abi.encodePacked(msgKey.keyType, msgKey.encodedKey); + } else { + encoded = abi.encodePacked( + msgKey.keyType, + encodeBytes(msgKey.encodedKey) + ); + } +} + +uint256 constant VAA_KEY_TYPE_LENGTH = 2 + 32 + 8; + +function decodeMessageKey( + bytes memory encoded, + uint256 startOffset +) pure returns (MessageKey memory msgKey, uint256 offset) { + (msgKey.keyType, offset) = encoded.asUint8Unchecked(startOffset); + if (msgKey.keyType == VAA_KEY_TYPE) { + (msgKey.encodedKey, offset) = encoded.sliceUnchecked( + offset, + VAA_KEY_TYPE_LENGTH + ); + } else { + (msgKey.encodedKey, offset) = decodeBytes(encoded, offset); } } -function encodeVaaKey(VaaKey memory vaaKey) pure returns (bytes memory encoded) { - encoded = abi.encodePacked(encoded, VERSION_VAAKEY, vaaKey.chainId, vaaKey.emitterAddress, vaaKey.sequence); +function encodeVaaKey( + VaaKey memory vaaKey +) pure returns (bytes memory encoded) { + encoded = abi.encodePacked( + vaaKey.chainId, + vaaKey.emitterAddress, + vaaKey.sequence + ); } -function decodeVaaKey(bytes memory encoded, uint256 startOffset) pure returns (VaaKey memory vaaKey, uint256 offset) { - offset = checkUint8(encoded, startOffset, VERSION_VAAKEY); +function decodeVaaKey( + bytes memory encoded, + uint256 startOffset +) pure returns (VaaKey memory vaaKey, uint256 offset) { + offset = startOffset; (vaaKey.chainId, offset) = encoded.asUint16Unchecked(offset); (vaaKey.emitterAddress, offset) = encoded.asBytes32Unchecked(offset); (vaaKey.sequence, offset) = encoded.asUint64Unchecked(offset); } +function encodeMessageKeyArray( + MessageKey[] memory msgKeys +) pure returns (bytes memory encoded) { + uint256 len = msgKeys.length; + if (len > type(uint8).max) { + revert TooManyMessageKeys(len); + } + encoded = abi.encodePacked(uint8(msgKeys.length)); + for (uint256 i = 0; i < len; ) { + encoded = abi.encodePacked(encoded, encodeMessageKey(msgKeys[i])); + unchecked { + ++i; + } + } +} + +function decodeMessageKeyArray( + bytes memory encoded, + uint256 startOffset +) pure returns (MessageKey[] memory msgKeys, uint256 offset) { + uint8 msgKeysLength; + (msgKeysLength, offset) = encoded.asUint8Unchecked(startOffset); + msgKeys = new MessageKey[](msgKeysLength); + for (uint256 i = 0; i < msgKeysLength; ) { + (msgKeys[i], offset) = decodeMessageKey(encoded, offset); + unchecked { + ++i; + } + } +} + +// ------------------------------------------ -------------------------------------------- + function encodeBytes(bytes memory payload) pure returns (bytes memory encoded) { //casting payload.length to uint32 is safe because you'll be hard-pressed to allocate 4 GB of // EVM memory in a single transaction encoded = abi.encodePacked(uint32(payload.length), payload); } -function decodeBytes(bytes memory encoded, uint256 startOffset) pure returns (bytes memory payload, uint256 offset) { +function decodeBytes( + bytes memory encoded, + uint256 startOffset +) pure returns (bytes memory payload, uint256 offset) { uint32 payloadLength; (payloadLength, offset) = encoded.asUint32Unchecked(startOffset); (payload, offset) = encoded.sliceUnchecked(offset, payloadLength); } -function checkUint8(bytes memory encoded, uint256 startOffset, uint8 expectedPayloadId) pure returns (uint256 offset) { +function checkUint8( + bytes memory encoded, + uint256 startOffset, + uint8 expectedPayloadId +) pure returns (uint256 offset) { uint8 parsedPayloadId; (parsedPayloadId, offset) = encoded.asUint8Unchecked(startOffset); if (parsedPayloadId != expectedPayloadId) { @@ -148,8 +219,29 @@ function checkLength(bytes memory encoded, uint256 expected) pure { } } -function encode(DeliveryOverride memory strct) pure returns (bytes memory encoded) { +function encode( + DeliveryOverride memory strct +) pure returns (bytes memory encoded) { encoded = abi.encodePacked( - VERSION_DELIVERY_OVERRIDE, strct.newReceiverValue, encodeBytes(strct.newExecutionInfo), strct.redeliveryHash + VERSION_DELIVERY_OVERRIDE, + strct.newReceiverValue, + encodeBytes(strct.newExecutionInfo), + strct.redeliveryHash ); } + +function decodeDeliveryOverride( + bytes memory encoded +) pure returns (DeliveryOverride memory strct) { + uint256 offset = checkUint8(encoded, 0, VERSION_DELIVERY_OVERRIDE); + + uint256 receiverValue; + + (receiverValue, offset) = encoded.asUint256Unchecked(offset); + (strct.newExecutionInfo, offset) = decodeBytes(encoded, offset); + (strct.redeliveryHash, offset) = encoded.asBytes32Unchecked(offset); + + strct.newReceiverValue = receiverValue; + + checkLength(encoded, offset); +} diff --git a/src/testing/helpers/MockOffchainRelayer.sol b/src/testing/helpers/MockOffchainRelayer.sol index 839bfc8..5e879e6 100644 --- a/src/testing/helpers/MockOffchainRelayer.sol +++ b/src/testing/helpers/MockOffchainRelayer.sol @@ -10,6 +10,7 @@ import "forge-std/Vm.sol"; import "./DeliveryInstructionDecoder.sol"; import "./ExecutionParameters.sol"; import "./BytesParsing.sol"; +import "forge-std/console.sol"; using BytesParsing for bytes; @@ -37,15 +38,31 @@ contract MockOffchainRelayer { chainIdOfWormholeAndGuardianUtilities = relayerWormhole.chainId(); } - function getPastEncodedVMs(uint16 chainId, uint64 deliveryVAASequence) public view returns (bytes[] memory) { - return pastEncodedVMs[keccak256(abi.encodePacked(chainId, deliveryVAASequence))]; + function getPastEncodedVMs( + uint16 chainId, + uint64 deliveryVAASequence + ) public view returns (bytes[] memory) { + return + pastEncodedVMs[ + keccak256(abi.encodePacked(chainId, deliveryVAASequence)) + ]; } - function getPastDeliveryVAA(uint16 chainId, uint64 deliveryVAASequence) public view returns (bytes memory) { - return pastEncodedDeliveryVAA[keccak256(abi.encodePacked(chainId, deliveryVAASequence))]; + function getPastDeliveryVAA( + uint16 chainId, + uint64 deliveryVAASequence + ) public view returns (bytes memory) { + return + pastEncodedDeliveryVAA[ + keccak256(abi.encodePacked(chainId, deliveryVAASequence)) + ]; } - function registerChain(uint16 chainId, address wormholeRelayerContractAddress, uint256 fork) public { + function registerChain( + uint16 chainId, + address wormholeRelayerContractAddress, + uint256 fork + ) public { wormholeRelayerContracts[chainId] = wormholeRelayerContractAddress; forks[chainId] = fork; chainIdFromFork[fork] = chainId; @@ -63,19 +80,32 @@ contract MockOffchainRelayer { relay(logs, bytes(""), false); } - function vaaKeyMatchesVAA(VaaKey memory vaaKey, bytes memory signedVaa) internal view returns (bool) { + function vaaKeyMatchesVAA( + VaaKey memory vaaKey, + bytes memory signedVaa + ) internal view returns (bool) { IWormhole.VM memory parsedVaa = relayerWormhole.parseVM(signedVaa); - return (vaaKey.chainId == parsedVaa.emitterChainId) && (vaaKey.emitterAddress == parsedVaa.emitterAddress) - && (vaaKey.sequence == parsedVaa.sequence); + return + (vaaKey.chainId == parsedVaa.emitterChainId) && + (vaaKey.emitterAddress == parsedVaa.emitterAddress) && + (vaaKey.sequence == parsedVaa.sequence); } - function relay(Vm.Log[] memory logs, bytes memory deliveryOverrides, bool debugLogging) public { + function relay( + Vm.Log[] memory logs, + bytes memory deliveryOverrides, + bool debugLogging + ) public { uint16 chainId = chainIdFromFork[vm.activeFork()]; - require(wormholeRelayerContracts[chainId] != address(0), "Chain not registered with MockOffchainRelayer"); + require( + wormholeRelayerContracts[chainId] != address(0), + "Chain not registered with MockOffchainRelayer" + ); vm.selectFork(forks[chainIdOfWormholeAndGuardianUtilities]); - Vm.Log[] memory entries = relayerWormholeSimulator.fetchWormholeMessageFromLog(logs); + Vm.Log[] memory entries = relayerWormholeSimulator + .fetchWormholeMessageFromLog(logs); if (debugLogging) { console.log("Found %s wormhole messages in logs", entries.length); @@ -83,7 +113,10 @@ contract MockOffchainRelayer { bytes[] memory encodedVMs = new bytes[](entries.length); for (uint256 i = 0; i < encodedVMs.length; i++) { - encodedVMs[i] = relayerWormholeSimulator.fetchSignedMessageFromLogs(entries[i], chainId); + encodedVMs[i] = relayerWormholeSimulator.fetchSignedMessageFromLogs( + entries[i], + chainId + ); } IWormhole.VM[] memory parsed = new IWormhole.VM[](encodedVMs.length); for (uint16 i = 0; i < encodedVMs.length; i++) { @@ -99,14 +132,20 @@ contract MockOffchainRelayer { } if ( - parsed[i].emitterAddress == toWormholeFormat(wormholeRelayerContracts[chainId]) - && (parsed[i].emitterChainId == chainId) + parsed[i].emitterAddress == + toWormholeFormat(wormholeRelayerContracts[chainId]) && + (parsed[i].emitterChainId == chainId) ) { if (debugLogging) { console.log("Relaying VAA to chain %s", chainId); } vm.selectFork(forks[chainIdOfWormholeAndGuardianUtilities]); - genericRelay(encodedVMs[i], encodedVMs, parsed[i], deliveryOverrides); + genericRelay( + encodedVMs[i], + encodedVMs, + parsed[i], + deliveryOverrides + ); } } @@ -134,25 +173,42 @@ contract MockOffchainRelayer { IWormhole.VM memory parsedDeliveryVAA, bytes memory deliveryOverrides ) internal { - (uint8 payloadId,) = parsedDeliveryVAA.payload.asUint8Unchecked(0); + (uint8 payloadId, ) = parsedDeliveryVAA.payload.asUint8Unchecked(0); if (payloadId == 1) { - DeliveryInstruction memory instruction = decodeDeliveryInstruction(parsedDeliveryVAA.payload); + DeliveryInstruction memory instruction = decodeDeliveryInstruction( + parsedDeliveryVAA.payload + ); - bytes[] memory encodedVMsToBeDelivered = new bytes[](instruction.vaaKeys.length); + bytes[] memory encodedVMsToBeDelivered = new bytes[]( + instruction.messageKeys.length + ); - for (uint8 i = 0; i < instruction.vaaKeys.length; i++) { - for (uint8 j = 0; j < encodedVMs.length; j++) { - if (vaaKeyMatchesVAA(instruction.vaaKeys[i], encodedVMs[j])) { - encodedVMsToBeDelivered[i] = encodedVMs[j]; - break; + for (uint8 i = 0; i < instruction.messageKeys.length; i++) { + if (instruction.messageKeys[i].keyType == 1) { + // VaaKey + (VaaKey memory vaaKey, ) = decodeVaaKey( + instruction.messageKeys[i].encodedKey, + 0 + ); + for (uint8 j = 0; j < encodedVMs.length; j++) { + if (vaaKeyMatchesVAA(vaaKey, encodedVMs[j])) { + encodedVMsToBeDelivered[i] = encodedVMs[j]; + break; + } } + } else if (instruction.messageKeys[i].keyType == 2) { + // CCTP Key } } - EvmExecutionInfoV1 memory executionInfo = decodeEvmExecutionInfoV1(instruction.encodedExecutionInfo); + EvmExecutionInfoV1 memory executionInfo = decodeEvmExecutionInfoV1( + instruction.encodedExecutionInfo + ); - uint256 budget = executionInfo.gasLimit * executionInfo.targetChainRefundPerGasUnused - + instruction.requestedReceiverValue + instruction.extraReceiverValue; + uint256 budget = executionInfo.gasLimit * + executionInfo.targetChainRefundPerGasUnused + + instruction.requestedReceiverValue + + instruction.extraReceiverValue; uint16 targetChain = instruction.targetChain; @@ -161,8 +217,12 @@ contract MockOffchainRelayer { vm.deal(address(this), budget); vm.recordLogs(); - IWormholeRelayerDelivery(wormholeRelayerContracts[targetChain]).deliver{value: budget}( - encodedVMsToBeDelivered, encodedDeliveryVAA, payable(address(this)), deliveryOverrides + IWormholeRelayerDelivery(wormholeRelayerContracts[targetChain]) + .deliver{value: budget}( + encodedVMsToBeDelivered, + encodedDeliveryVAA, + payable(address(this)), + deliveryOverrides ); setInfo( @@ -172,7 +232,10 @@ contract MockOffchainRelayer { encodedDeliveryVAA ); } else if (payloadId == 2) { - RedeliveryInstruction memory instruction = decodeRedeliveryInstruction(parsedDeliveryVAA.payload); + RedeliveryInstruction + memory instruction = decodeRedeliveryInstruction( + parsedDeliveryVAA.payload + ); DeliveryOverride memory deliveryOverride = DeliveryOverride({ newExecutionInfo: instruction.newEncodedExecutionInfo, @@ -180,21 +243,33 @@ contract MockOffchainRelayer { redeliveryHash: parsedDeliveryVAA.hash }); - EvmExecutionInfoV1 memory executionInfo = decodeEvmExecutionInfoV1(instruction.newEncodedExecutionInfo); - uint256 budget = executionInfo.gasLimit * executionInfo.targetChainRefundPerGasUnused - + instruction.newRequestedReceiverValue; + EvmExecutionInfoV1 memory executionInfo = decodeEvmExecutionInfoV1( + instruction.newEncodedExecutionInfo + ); + uint256 budget = executionInfo.gasLimit * + executionInfo.targetChainRefundPerGasUnused + + instruction.newRequestedReceiverValue; - bytes memory oldEncodedDeliveryVAA = - getPastDeliveryVAA(instruction.deliveryVaaKey.chainId, instruction.deliveryVaaKey.sequence); - bytes[] memory oldEncodedVMs = - getPastEncodedVMs(instruction.deliveryVaaKey.chainId, instruction.deliveryVaaKey.sequence); + bytes memory oldEncodedDeliveryVAA = getPastDeliveryVAA( + instruction.deliveryVaaKey.chainId, + instruction.deliveryVaaKey.sequence + ); + bytes[] memory oldEncodedVMs = getPastEncodedVMs( + instruction.deliveryVaaKey.chainId, + instruction.deliveryVaaKey.sequence + ); - uint16 targetChain = - decodeDeliveryInstruction(relayerWormhole.parseVM(oldEncodedDeliveryVAA).payload).targetChain; + uint16 targetChain = decodeDeliveryInstruction( + relayerWormhole.parseVM(oldEncodedDeliveryVAA).payload + ).targetChain; vm.selectFork(forks[targetChain]); - IWormholeRelayerDelivery(wormholeRelayerContracts[targetChain]).deliver{value: budget}( - oldEncodedVMs, oldEncodedDeliveryVAA, payable(address(this)), encode(deliveryOverride) + IWormholeRelayerDelivery(wormholeRelayerContracts[targetChain]) + .deliver{value: budget}( + oldEncodedVMs, + oldEncodedDeliveryVAA, + payable(address(this)), + encode(deliveryOverride) ); } } diff --git a/test/CCTPBase.t.sol b/test/CCTPBase.t.sol index b6ae67d..f8aa8d7 100644 --- a/test/CCTPBase.t.sol +++ b/test/CCTPBase.t.sol @@ -24,17 +24,38 @@ contract CCTPToy is CCTPSender, CCTPReceiver { address _circleMessageTransmitter, address _circleTokenMessenger, address _USDC - ) CCTPBase(_wormholeRelayer, _tokenBridge, _wormhole, _circleMessageTransmitter, _circleTokenMessenger, _USDC) {} - - function quoteCrossChainDeposit(uint16 targetChain) public view returns (uint256 cost) { + ) + CCTPBase( + _wormholeRelayer, + _tokenBridge, + _wormhole, + _circleMessageTransmitter, + _circleTokenMessenger, + _USDC + ) + {} + + function quoteCrossChainDeposit( + uint16 targetChain + ) public view returns (uint256 cost) { // Cost of delivering token and payload to targetChain - uint256 deliveryCost; - (deliveryCost,) = wormholeRelayer.quoteEVMDeliveryPrice(targetChain, 0, GAS_LIMIT); + (cost, ) = wormholeRelayer.quoteEVMDeliveryPrice( + targetChain, + 0, + GAS_LIMIT + ); } - function sendCrossChainDeposit(uint16 targetChain, address recipient, uint256 amount) public payable { + function sendCrossChainDeposit( + uint16 targetChain, + address recipient, + uint256 amount + ) public payable { uint256 cost = quoteCrossChainDeposit(targetChain); - require(msg.value == cost, "msg.value must be quoteCrossChainDeposit(targetChain)"); + require( + msg.value == cost, + "msg.value must be quoteCrossChainDeposit(targetChain)" + ); IERC20(USDC).transferFrom(msg.sender, address(this), amount); @@ -55,8 +76,16 @@ contract CCTPToy is CCTPSender, CCTPReceiver { bytes32 sourceAddress, uint16 sourceChain, bytes32 // deliveryHash - ) internal override onlyWormholeRelayer isRegisteredSender(sourceChain, sourceAddress) { - (address recipient, uint256 expectedAmount) = abi.decode(payload, (address, uint256)); + ) + internal + override + onlyWormholeRelayer + isRegisteredSender(sourceChain, sourceAddress) + { + (address recipient, uint256 expectedAmount) = abi.decode( + payload, + (address, uint256) + ); require(amount == expectedAmount, "amount != payload.expectedAmount"); IERC20(USDC).transfer(recipient, amount); } @@ -67,33 +96,41 @@ contract WormholeSDKTest is WormholeRelayerBasicTest { CCTPToy CCTPToyTarget; ERC20Mock USDCSource; ERC20Mock USDCTarget; - MockMessageTransmitter circleMessageTransmitter; - MockTokenMessenger circleTokenMessenger; + MockMessageTransmitter circleMessageTransmitterSource; + MockTokenMessenger circleTokenMessengerSource; + MockMessageTransmitter circleMessageTransmitterTarget; + MockTokenMessenger circleTokenMessengerTarget; + + constructor() { + setTestnetForkChains(2, 6); + } function setUpSource() public override { + USDCSource = createAndAttestToken(sourceChain); + circleMessageTransmitterSource = new MockMessageTransmitter(USDCSource); + circleTokenMessengerSource = new MockTokenMessenger(USDCSource); CCTPToySource = new CCTPToy( address(relayerSource), address(tokenBridgeSource), address(wormholeSource), - address(circleMessageTransmitter), - address(circleTokenMessenger), + address(circleMessageTransmitterSource), + address(circleTokenMessengerSource), address(USDCSource) ); - USDCSource = createAndAttestToken(sourceChain); - circleMessageTransmitter = new MockMessageTransmitter(USDCSource); } function setUpTarget() public override { + USDCTarget = createAndAttestToken(targetChain); + circleTokenMessengerTarget = new MockTokenMessenger(USDCTarget); + circleMessageTransmitterTarget = new MockMessageTransmitter(USDCTarget); CCTPToyTarget = new CCTPToy( - address(relayerTarget), - address(tokenBridgeTarget), - address(wormholeTarget), - address(circleMessageTransmitter), - address(circleTokenMessenger), + address(relayerTarget), + address(tokenBridgeTarget), + address(wormholeTarget), + address(circleMessageTransmitterTarget), + address(circleTokenMessengerTarget), address(USDCTarget) ); - USDCTarget = createAndAttestToken(targetChain); - circleTokenMessenger = new MockTokenMessenger(USDCTarget); } // function setUpGeneral() public override { @@ -115,7 +152,11 @@ contract WormholeSDKTest is WormholeRelayerBasicTest { uint256 cost = CCTPToySource.quoteCrossChainDeposit(targetChain); vm.recordLogs(); - CCTPToySource.sendCrossChainDeposit{value: cost}(targetChain, recipient, amount); + CCTPToySource.sendCrossChainDeposit{value: cost}( + targetChain, + recipient, + amount + ); performDelivery(); vm.selectFork(targetFork); From cfc0dbb7a81e673c993a4d8d2584ab01a3914d60 Mon Sep 17 00:00:00 2001 From: derpy-duck <115193320+derpy-duck@users.noreply.github.com> Date: Fri, 8 Sep 2023 18:16:59 -0400 Subject: [PATCH 05/23] CircleCCTPSimulator --- src/testing/helpers/CircleCCTPSimulator.sol | 168 ++++++++++++++++++++ src/testing/helpers/WormholeSimulator.sol | 154 ++++++++---------- 2 files changed, 231 insertions(+), 91 deletions(-) create mode 100644 src/testing/helpers/CircleCCTPSimulator.sol diff --git a/src/testing/helpers/CircleCCTPSimulator.sol b/src/testing/helpers/CircleCCTPSimulator.sol new file mode 100644 index 0000000..dd6912e --- /dev/null +++ b/src/testing/helpers/CircleCCTPSimulator.sol @@ -0,0 +1,168 @@ +// SPDX-License-Identifier: Apache 2 +pragma solidity ^0.8.13; + +import {IMessageTransmitter} from "../../../src/interfaces/CCTPInterfaces/IMessageTransmitter.sol"; +import "./BytesLib.sol"; + +import "forge-std/Vm.sol"; +import "forge-std/console.sol"; + +interface MessageTransmitterViewAttesterManager { + function attesterManager() external view returns (address); + + function enableAttester(address newAttester) external; +} + +/** + * @title A Circle MessageTransmitter Simulator + * @notice This contract simulates attesting Circle messages emitted in a forge test. + * It overrides the Circle 'attester' set to allow for signing messages with a single + * private key on any EVM where the MessageTransmitter contract is deployed. + * @dev This contract is meant to be used when testing against a testnet or mainnet fork. + */ +contract CircleMessageTransmitterSimulator { + using BytesLib for bytes; + + // Taken from forge-std/Script.sol + address private constant VM_ADDRESS = + address(bytes20(uint160(uint256(keccak256("hevm cheat code"))))); + Vm public constant vm = Vm(VM_ADDRESS); + + // Allow access to MessageTransmitter + IMessageTransmitter public messageTransmitter; + + // Save the private key to sign messages with + uint256 private attesterPrivateKey; + + /** + * @param messageTransmitter_ address of the MessageTransmitter for the chain being forked + * @param attesterPrivateKey_ private key of the (single) attester - to override the MessageTransmitter contract with + */ + constructor(address messageTransmitter_, uint256 attesterPrivateKey_) { + messageTransmitter = IMessageTransmitter(messageTransmitter_); + attesterPrivateKey = attesterPrivateKey_; + overrideAttester(vm.addr(attesterPrivateKey)); + } + + function overrideAttester(address attesterPublicKey) internal { + { + MessageTransmitterViewAttesterManager attesterManagerInterface = MessageTransmitterViewAttesterManager( + address(messageTransmitter) + ); + address attesterManager = attesterManagerInterface + .attesterManager(); + vm.prank(attesterManager); + attesterManagerInterface.enableAttester(attesterPublicKey); + } + } + + /* + function parseMessageFromMessageTransmitterLog( + Vm.Log memory log + ) internal pure returns (bytes message) { + uint256 index = 32; + + // length of payload + uint256 payloadLen = log.data.toUint256(index); + index += 32; + + vm_.payload = log.data.slice(index, payloadLen); + index += payloadLen; + + // trailing bytes (due to 32 byte slot overlap) + index += log.data.length - index; + + require( + index == log.data.length, + "failed to parse MessageTransmitter message" + ); + }*/ + + /** + * @notice Finds published messageTransmitter events in forge logs + * @param logs The forge Vm.log captured when recording events during test execution + */ + function fetchMessageTransmitterLogsFromLogs( + Vm.Log[] memory logs + ) public pure returns (Vm.Log[] memory) { + uint256 count = 0; + for (uint256 i = 0; i < logs.length; i++) { + if ( + logs[i].topics[0] == + keccak256("event MessageSent(bytes message);") + ) { + count += 1; + } + } + + // create log array to save published messages + Vm.Log[] memory published = new Vm.Log[](count); + + uint256 publishedIndex = 0; + for (uint256 i = 0; i < logs.length; i++) { + if ( + logs[i].topics[0] == + keccak256("event MessageSent(bytes message);") + ) { + published[publishedIndex] = logs[i]; + publishedIndex += 1; + } + } + + return published; + } + + /** + * @notice attests a simulated MessageTransmitter message using the emitted log from MessageTransmitter + * @param log The forge Vm.log captured when recording events during test execution + * @return attestation attested message + */ + /* + function fetchSignedMessageFromLog( + Vm.Log memory log, + uint16 emitterChainId + ) public view returns (bytes memory attestation) { + // Parse messageTransmitter message from ethereum logs + bytes memory message = parseMessageFromMessageTransmitterLog(log); + + // Set empty body values before computing the hash + vm_.version = uint8(1); + vm_.timestamp = uint32(block.timestamp); + vm_.emitterChainId = emitterChainId; + + return encodeAndSignMessage(vm_); + } + */ + /** + * @notice Signs and preformatted simulated messageTransmitter message + * @param vm_ The preformatted messageTransmitter message + * @return signedMessage Formatted and signed messageTransmitter message + */ + /* + function encodeAndSignMessage( + IMessageTransmitter.VM memory vm_ + ) public view returns (bytes memory signedMessage) { + + // Compute the hash of the body + bytes memory body = encodeObservation(vm_); + vm_.hash = doubleKeccak256(body); + + // Sign the hash with the devnet guardian private key + IMessageTransmitter.Signature[] + memory sigs = new IMessageTransmitter.Signature[](1); + (sigs[0].v, sigs[0].r, sigs[0].s) = vm.sign(devnetGuardianPK, vm_.hash); + sigs[0].guardianIndex = 0; + + signedMessage = abi.encodePacked( + vm_.version, + messageTransmitter.getCurrentGuardianSetIndex(), + uint8(sigs.length), + sigs[0].guardianIndex, + sigs[0].r, + sigs[0].s, + sigs[0].v - 27, + body + ); + } +*/ +} diff --git a/src/testing/helpers/WormholeSimulator.sol b/src/testing/helpers/WormholeSimulator.sol index f2bc000..a73f6ca 100644 --- a/src/testing/helpers/WormholeSimulator.sol +++ b/src/testing/helpers/WormholeSimulator.sol @@ -12,13 +12,14 @@ import "forge-std/console.sol"; * @notice This contract simulates signing Wormhole messages emitted in a forge test. * It overrides the Wormhole guardian set to allow for signing messages with a single * private key on any EVM where Wormhole core contracts are deployed. - * @dev This contract is meant to be used when testing against a mainnet fork. + * @dev This contract is meant to be used when testing against a testnet or mainnet fork. */ contract WormholeSimulator { using BytesLib for bytes; // Taken from forge-std/Script.sol - address private constant VM_ADDRESS = address(bytes20(uint160(uint256(keccak256("hevm cheat code"))))); + address private constant VM_ADDRESS = + address(bytes20(uint160(uint256(keccak256("hevm cheat code"))))); Vm public constant vm = Vm(VM_ADDRESS); // Allow access to Wormhole @@ -44,15 +45,24 @@ contract WormholeSimulator { // Get slot for Guardian Set at the current index uint32 guardianSetIndex = wormhole.getCurrentGuardianSetIndex(); - bytes32 guardianSetSlot = keccak256(abi.encode(guardianSetIndex, 2)); + bytes32 guardianSetSlot = keccak256( + abi.encode(guardianSetIndex, 2) + ); // Overwrite all but first guardian set to zero address. This isn't // necessary, but just in case we inadvertently access these slots // for any reason. - uint256 numGuardians = uint256(vm.load(address(wormhole), guardianSetSlot)); - for (uint256 i = 1; i < numGuardians;) { + uint256 numGuardians = uint256( + vm.load(address(wormhole), guardianSetSlot) + ); + for (uint256 i = 1; i < numGuardians; ) { vm.store( - address(wormhole), bytes32(uint256(keccak256(abi.encodePacked(guardianSetSlot))) + i), bytes32(0) + address(wormhole), + bytes32( + uint256(keccak256(abi.encodePacked(guardianSetSlot))) + + i + ), + bytes32(0) ); unchecked { i += 1; @@ -63,7 +73,9 @@ contract WormholeSimulator { // in the function argument. vm.store( address(wormhole), - bytes32(uint256(keccak256(abi.encodePacked(guardianSetSlot))) + 0), // just explicit w/ index 0 + bytes32( + uint256(keccak256(abi.encodePacked(guardianSetSlot))) + 0 + ), // just explicit w/ index 0 bytes32(uint256(uint160(devnetGuardian))) ); @@ -75,17 +87,26 @@ contract WormholeSimulator { ); // Confirm guardian set override - address[] memory guardians = wormhole.getGuardianSet(guardianSetIndex).keys; + address[] memory guardians = wormhole + .getGuardianSet(guardianSetIndex) + .keys; require(guardians.length == 1, "guardians.length != 1"); - require(guardians[0] == devnetGuardian, "incorrect guardian set override"); + require( + guardians[0] == devnetGuardian, + "incorrect guardian set override" + ); } } - function doubleKeccak256(bytes memory body) internal pure returns (bytes32) { + function doubleKeccak256( + bytes memory body + ) internal pure returns (bytes32) { return keccak256(abi.encodePacked(keccak256(body))); } - function parseVMFromLogs(Vm.Log memory log) internal pure returns (IWormhole.VM memory vm_) { + function parseVMFromLogs( + Vm.Log memory log + ) internal pure returns (IWormhole.VM memory vm_) { uint256 index = 0; // emitterAddress @@ -123,10 +144,17 @@ contract WormholeSimulator { * @notice Finds published Wormhole events in forge logs * @param logs The forge Vm.log captured when recording events during test execution */ - function fetchWormholeMessageFromLog(Vm.Log[] memory logs) public pure returns (Vm.Log[] memory) { + function fetchWormholeMessageFromLog( + Vm.Log[] memory logs + ) public pure returns (Vm.Log[] memory) { uint256 count = 0; for (uint256 i = 0; i < logs.length; i++) { - if (logs[i].topics[0] == keccak256("LogMessagePublished(address,uint64,uint32,bytes,uint8)")) { + if ( + logs[i].topics[0] == + keccak256( + "LogMessagePublished(address,uint64,uint32,bytes,uint8)" + ) + ) { count += 1; } } @@ -136,7 +164,12 @@ contract WormholeSimulator { uint256 publishedIndex = 0; for (uint256 i = 0; i < logs.length; i++) { - if (logs[i].topics[0] == keccak256("LogMessagePublished(address,uint64,uint32,bytes,uint8)")) { + if ( + logs[i].topics[0] == + keccak256( + "LogMessagePublished(address,uint64,uint32,bytes,uint8)" + ) + ) { published[publishedIndex] = logs[i]; publishedIndex += 1; } @@ -150,7 +183,9 @@ contract WormholeSimulator { * @param vm_ Wormhole VM struct * @return encodedObservation Wormhole message body encoded into bytes */ - function encodeObservation(IWormhole.VM memory vm_) public pure returns (bytes memory encodedObservation) { + function encodeObservation( + IWormhole.VM memory vm_ + ) public pure returns (bytes memory encodedObservation) { encodedObservation = abi.encodePacked( vm_.timestamp, vm_.nonce, @@ -167,11 +202,10 @@ contract WormholeSimulator { * @param log The forge Vm.log captured when recording events during test execution * @return signedMessage Formatted and signed Wormhole message */ - function fetchSignedMessageFromLogs(Vm.Log memory log, uint16 emitterChainId) - public - view - returns (bytes memory signedMessage) - { + function fetchSignedMessageFromLogs( + Vm.Log memory log, + uint16 emitterChainId + ) public view returns (bytes memory signedMessage) { // Create message instance IWormhole.VM memory vm_; @@ -186,81 +220,14 @@ contract WormholeSimulator { return encodeAndSignMessage(vm_); } - /** - * @notice Formats and signs a simulated Wormhole batch VAA given an array of Wormhole log entries - * @param logs The forge Vm.log entries captured when recording events during test execution - * @param nonce The nonce of the messages to be accumulated into the batch VAA - * @return signedMessage Formatted and signed Wormhole message - */ - function fetchSignedBatchVAAFromLogs( - Vm.Log[] memory logs, - uint32 nonce, - uint16 emitterChainId, - address emitterAddress - ) public view returns (bytes memory signedMessage) { - uint8 numObservations = 0; - IWormhole.VM[] memory vm_ = new IWormhole.VM[](logs.length); - - for (uint256 i = 0; i < logs.length; i++) { - vm_[i] = parseVMFromLogs(logs[i]); - vm_[i].timestamp = uint32(block.timestamp); - vm_[i].emitterChainId = emitterChainId; - vm_[i].emitterAddress = bytes32(uint256(uint160(emitterAddress))); - if (vm_[i].nonce == nonce) { - numObservations += 1; - } - } - - bytes memory packedObservations; - bytes32[] memory hashes = new bytes32[](numObservations); - - uint8 counter = 0; - for (uint256 i = 0; i < logs.length; i++) { - if (vm_[i].nonce == nonce) { - bytes memory observation = abi.encodePacked( - vm_[i].timestamp, - vm_[i].nonce, - vm_[i].emitterChainId, - vm_[i].emitterAddress, - vm_[i].sequence, - vm_[i].consistencyLevel, - vm_[i].payload - ); - hashes[counter] = doubleKeccak256(observation); - packedObservations = - abi.encodePacked(packedObservations, uint8(counter), uint32(observation.length), observation); - counter++; - } - } - - bytes32 batchHash = doubleKeccak256(abi.encodePacked(uint8(2), keccak256(abi.encodePacked(hashes)))); - - IWormhole.Signature[] memory sigs = new IWormhole.Signature[](1); - (sigs[0].v, sigs[0].r, sigs[0].s) = vm.sign(devnetGuardianPK, batchHash); - - sigs[0].guardianIndex = 0; - - signedMessage = abi.encodePacked( - uint8(2), - wormhole.getCurrentGuardianSetIndex(), - uint8(1), - sigs[0].guardianIndex, - sigs[0].r, - sigs[0].s, - uint8(sigs[0].v - 27), - numObservations, - hashes, - numObservations, - packedObservations - ); - } - /** * @notice Signs and preformatted simulated Wormhole message * @param vm_ The preformatted Wormhole message * @return signedMessage Formatted and signed Wormhole message */ - function encodeAndSignMessage(IWormhole.VM memory vm_) public view returns (bytes memory signedMessage) { + function encodeAndSignMessage( + IWormhole.VM memory vm_ + ) public view returns (bytes memory signedMessage) { // Compute the hash of the body bytes memory body = encodeObservation(vm_); vm_.hash = doubleKeccak256(body); @@ -288,7 +255,12 @@ contract WormholeSimulator { */ function setMessageFee(uint256 newFee) public { bytes32 coreModule = 0x00000000000000000000000000000000000000000000000000000000436f7265; - bytes memory message = abi.encodePacked(coreModule, uint8(3), uint16(wormhole.chainId()), newFee); + bytes memory message = abi.encodePacked( + coreModule, + uint8(3), + uint16(wormhole.chainId()), + newFee + ); IWormhole.VM memory preSignedMessage = IWormhole.VM({ version: 1, timestamp: uint32(block.timestamp), From ab4ce3ebc8c9b68a90d04fef247840c305d2c382 Mon Sep 17 00:00:00 2001 From: derpy-duck <115193320+derpy-duck@users.noreply.github.com> Date: Mon, 11 Sep 2023 18:18:07 -0400 Subject: [PATCH 06/23] Finish CCTP Simulator' --- src/testing/helpers/CircleCCTPSimulator.sol | 62 +++++++-------------- src/testing/helpers/MockOffchainRelayer.sol | 50 +++++++++-------- 2 files changed, 48 insertions(+), 64 deletions(-) diff --git a/src/testing/helpers/CircleCCTPSimulator.sol b/src/testing/helpers/CircleCCTPSimulator.sol index dd6912e..910eea2 100644 --- a/src/testing/helpers/CircleCCTPSimulator.sol +++ b/src/testing/helpers/CircleCCTPSimulator.sol @@ -56,27 +56,28 @@ contract CircleMessageTransmitterSimulator { } } - /* + function parseMessageFromMessageTransmitterLog( Vm.Log memory log - ) internal pure returns (bytes message) { + ) internal pure returns (bytes memory message) { uint256 index = 32; // length of payload uint256 payloadLen = log.data.toUint256(index); index += 32; - vm_.payload = log.data.slice(index, payloadLen); + message = log.data.slice(index, payloadLen); index += payloadLen; // trailing bytes (due to 32 byte slot overlap) + require(log.data.length - index < 32, "Too many extra bytes"); index += log.data.length - index; require( index == log.data.length, "failed to parse MessageTransmitter message" ); - }*/ + } /** * @notice Finds published messageTransmitter events in forge logs @@ -117,52 +118,31 @@ contract CircleMessageTransmitterSimulator { * @param log The forge Vm.log captured when recording events during test execution * @return attestation attested message */ - /* + function fetchSignedMessageFromLog( - Vm.Log memory log, - uint16 emitterChainId + Vm.Log memory log ) public view returns (bytes memory attestation) { // Parse messageTransmitter message from ethereum logs bytes memory message = parseMessageFromMessageTransmitterLog(log); - // Set empty body values before computing the hash - vm_.version = uint8(1); - vm_.timestamp = uint32(block.timestamp); - vm_.emitterChainId = emitterChainId; - - return encodeAndSignMessage(vm_); + return signMessage(message); } - */ + /** - * @notice Signs and preformatted simulated messageTransmitter message - * @param vm_ The preformatted messageTransmitter message - * @return signedMessage Formatted and signed messageTransmitter message + * @notice Signs a simulated messageTransmitter message + * @param message The messageTransmitter message + * @return signedMessage signed messageTransmitter message */ - /* - function encodeAndSignMessage( - IMessageTransmitter.VM memory vm_ + + function signMessage( + bytes memory message ) public view returns (bytes memory signedMessage) { - // Compute the hash of the body - bytes memory body = encodeObservation(vm_); - vm_.hash = doubleKeccak256(body); - - // Sign the hash with the devnet guardian private key - IMessageTransmitter.Signature[] - memory sigs = new IMessageTransmitter.Signature[](1); - (sigs[0].v, sigs[0].r, sigs[0].s) = vm.sign(devnetGuardianPK, vm_.hash); - sigs[0].guardianIndex = 0; - - signedMessage = abi.encodePacked( - vm_.version, - messageTransmitter.getCurrentGuardianSetIndex(), - uint8(sigs.length), - sigs[0].guardianIndex, - sigs[0].r, - sigs[0].s, - sigs[0].v - 27, - body - ); + bytes32 messageHash = keccak256(message); + + // Sign the hash with the attester private key + (uint8 v, bytes32 r, bytes32 s) = vm.sign(attesterPrivateKey, messageHash); + return abi.encodePacked(r, s, v); } -*/ + } diff --git a/src/testing/helpers/MockOffchainRelayer.sol b/src/testing/helpers/MockOffchainRelayer.sol index 5e879e6..05b105b 100644 --- a/src/testing/helpers/MockOffchainRelayer.sol +++ b/src/testing/helpers/MockOffchainRelayer.sol @@ -8,6 +8,7 @@ import "../../interfaces/IWormholeRelayer.sol"; import "../../interfaces/IWormhole.sol"; import "forge-std/Vm.sol"; import "./DeliveryInstructionDecoder.sol"; +import {CCTPMessageLib} from "../../CCTPBase.sol"; import "./ExecutionParameters.sol"; import "./BytesParsing.sol"; import "forge-std/console.sol"; @@ -27,7 +28,7 @@ contract MockOffchainRelayer { mapping(uint256 => uint16) chainIdFromFork; - mapping(bytes32 => bytes[]) pastEncodedVMs; + mapping(bytes32 => bytes[]) pastEncodedSignedVaas; mapping(bytes32 => bytes) pastEncodedDeliveryVAA; @@ -38,12 +39,12 @@ contract MockOffchainRelayer { chainIdOfWormholeAndGuardianUtilities = relayerWormhole.chainId(); } - function getPastEncodedVMs( + function getPastEncodedSignedVaas( uint16 chainId, uint64 deliveryVAASequence ) public view returns (bytes[] memory) { return - pastEncodedVMs[ + pastEncodedSignedVaas[ keccak256(abi.encodePacked(chainId, deliveryVAASequence)) ]; } @@ -111,18 +112,18 @@ contract MockOffchainRelayer { console.log("Found %s wormhole messages in logs", entries.length); } - bytes[] memory encodedVMs = new bytes[](entries.length); - for (uint256 i = 0; i < encodedVMs.length; i++) { - encodedVMs[i] = relayerWormholeSimulator.fetchSignedMessageFromLogs( + bytes[] memory encodedSignedVaas = new bytes[](entries.length); + for (uint256 i = 0; i < encodedSignedVaas.length; i++) { + encodedSignedVaas[i] = relayerWormholeSimulator.fetchSignedMessageFromLogs( entries[i], chainId ); } - IWormhole.VM[] memory parsed = new IWormhole.VM[](encodedVMs.length); - for (uint16 i = 0; i < encodedVMs.length; i++) { - parsed[i] = relayerWormhole.parseVM(encodedVMs[i]); + IWormhole.VM[] memory parsed = new IWormhole.VM[](encodedSignedVaas.length); + for (uint16 i = 0; i < encodedSignedVaas.length; i++) { + parsed[i] = relayerWormhole.parseVM(encodedSignedVaas[i]); } - for (uint16 i = 0; i < encodedVMs.length; i++) { + for (uint16 i = 0; i < encodedSignedVaas.length; i++) { if (debugLogging) { console.log( "Found VAA from chain %s emitted from %s", @@ -141,8 +142,8 @@ contract MockOffchainRelayer { } vm.selectFork(forks[chainIdOfWormholeAndGuardianUtilities]); genericRelay( - encodedVMs[i], - encodedVMs, + encodedSignedVaas[i], + encodedSignedVaas, parsed[i], deliveryOverrides ); @@ -159,17 +160,17 @@ contract MockOffchainRelayer { function setInfo( uint16 chainId, uint64 deliveryVAASequence, - bytes[] memory encodedVMs, + bytes[] memory encodedSignedVaas, bytes memory encodedDeliveryVAA ) internal { bytes32 key = keccak256(abi.encodePacked(chainId, deliveryVAASequence)); - pastEncodedVMs[key] = encodedVMs; + pastEncodedSignedVaas[key] = encodedSignedVaas; pastEncodedDeliveryVAA[key] = encodedDeliveryVAA; } function genericRelay( bytes memory encodedDeliveryVAA, - bytes[] memory encodedVMs, + bytes[] memory encodedSignedVaas, IWormhole.VM memory parsedDeliveryVAA, bytes memory deliveryOverrides ) internal { @@ -179,7 +180,7 @@ contract MockOffchainRelayer { parsedDeliveryVAA.payload ); - bytes[] memory encodedVMsToBeDelivered = new bytes[]( + bytes[] memory encodedSignedVaasToBeDelivered = new bytes[]( instruction.messageKeys.length ); @@ -190,14 +191,17 @@ contract MockOffchainRelayer { instruction.messageKeys[i].encodedKey, 0 ); - for (uint8 j = 0; j < encodedVMs.length; j++) { - if (vaaKeyMatchesVAA(vaaKey, encodedVMs[j])) { - encodedVMsToBeDelivered[i] = encodedVMs[j]; + for (uint8 j = 0; j < encodedSignedVaas.length; j++) { + if (vaaKeyMatchesVAA(vaaKey, encodedSignedVaas[j])) { + encodedSignedVaasToBeDelivered[i] = encodedSignedVaas[j]; break; } } } else if (instruction.messageKeys[i].keyType == 2) { // CCTP Key + // clean this up + CCTPMessageLib.CCTPMessage memory message = abi.decode(instruction.messageKeys[i].encodedKey, (CCTPMessageLib.CCTPMessage)); + } } @@ -219,7 +223,7 @@ contract MockOffchainRelayer { vm.recordLogs(); IWormholeRelayerDelivery(wormholeRelayerContracts[targetChain]) .deliver{value: budget}( - encodedVMsToBeDelivered, + encodedSignedVaasToBeDelivered, encodedDeliveryVAA, payable(address(this)), deliveryOverrides @@ -228,7 +232,7 @@ contract MockOffchainRelayer { setInfo( parsedDeliveryVAA.emitterChainId, parsedDeliveryVAA.sequence, - encodedVMsToBeDelivered, + encodedSignedVaasToBeDelivered, encodedDeliveryVAA ); } else if (payloadId == 2) { @@ -254,7 +258,7 @@ contract MockOffchainRelayer { instruction.deliveryVaaKey.chainId, instruction.deliveryVaaKey.sequence ); - bytes[] memory oldEncodedVMs = getPastEncodedVMs( + bytes[] memory oldEncodedSignedVaas = getPastEncodedSignedVaas( instruction.deliveryVaaKey.chainId, instruction.deliveryVaaKey.sequence ); @@ -266,7 +270,7 @@ contract MockOffchainRelayer { vm.selectFork(forks[targetChain]); IWormholeRelayerDelivery(wormholeRelayerContracts[targetChain]) .deliver{value: budget}( - oldEncodedVMs, + oldEncodedSignedVaas, oldEncodedDeliveryVAA, payable(address(this)), encode(deliveryOverride) From 669ccb6bea2db6fe6b41033438f339f7862a66b4 Mon Sep 17 00:00:00 2001 From: derpy-duck <115193320+derpy-duck@users.noreply.github.com> Date: Fri, 15 Sep 2023 17:41:35 -0400 Subject: [PATCH 07/23] Working CCTP Test --- src/CCTPBase.sol | 1 + src/testing/CCTPMocks.sol | 74 ---------- src/testing/WormholeRelayerTest.sol | 149 +++++++++++++++++--- src/testing/helpers/CircleCCTPSimulator.sol | 23 ++- src/testing/helpers/MockOffchainRelayer.sol | 53 ++++++- test/CCTPBase.t.sol | 42 +++--- 6 files changed, 215 insertions(+), 127 deletions(-) delete mode 100644 src/testing/CCTPMocks.sol diff --git a/src/CCTPBase.sol b/src/CCTPBase.sol index 79a89b4..60a4c6c 100644 --- a/src/CCTPBase.sol +++ b/src/CCTPBase.sol @@ -87,6 +87,7 @@ abstract contract CCTPSender is CCTPBase { internal returns (MessageKey memory) { + IERC20(USDC).approve(address(circleTokenMessenger), amount); uint64 nonce = circleTokenMessenger.depositForBurnWithCaller( amount, getCCTPDomain(targetChain), diff --git a/src/testing/CCTPMocks.sol b/src/testing/CCTPMocks.sol deleted file mode 100644 index 0acfc3e..0000000 --- a/src/testing/CCTPMocks.sol +++ /dev/null @@ -1,74 +0,0 @@ -// SPDX-License-Identifier: UNLICENSED -pragma solidity ^0.8.13; - -import "../interfaces/CCTPInterfaces/ITokenMessenger.sol"; -import "../interfaces/CCTPInterfaces/IMessageTransmitter.sol"; -import "./ERC20Mock.sol"; - -struct MockMessage { - uint256 amount; - uint32 destinationDomain; - bytes32 mintRecipient; - address burnToken; - bytes32 destinationCaller; -} - -contract MockMessageTransmitter is IMessageTransmitter { - ERC20Mock public immutable USDC; - - constructor(ERC20Mock _USDC) { - USDC = _USDC; - } - - function receiveMessage(bytes calldata _message, bytes calldata) public returns (bool success) { - MockMessage memory message = abi.decode(_message, (MockMessage)); - require (msg.sender == bytes32ToAddress(message.destinationCaller), "Wrong caller"); - - ERC20Mock(USDC).mint(bytes32ToAddress(message.mintRecipient), message.amount); - return true; - } - - function bytes32ToAddress(bytes32 _buf) public pure returns (address) { - return address(uint160(uint256(_buf))); - } - - /* Unimplemented functions */ - - function sendMessage(uint32, bytes32, bytes calldata) external pure returns (uint64) { - revert("Unimplemented"); - } - - function sendMessageWithCaller(uint32, bytes32, bytes32, bytes calldata) external pure returns (uint64) { - revert("Unimplemented"); - } - - function replaceMessage(bytes calldata, bytes calldata, bytes calldata, bytes32) external pure { - revert("Unimplemented"); - } -} - -contract MockTokenMessenger is ITokenMessenger { - ERC20Mock public immutable USDC; - uint64 public nonce; - - MockMessage message; - bytes packedMessage; - - constructor(ERC20Mock _USDC) { - USDC = _USDC; - } - - function depositForBurnWithCaller( - uint256 amount, - uint32 destinationDomain, - bytes32 mintRecipient, - address burnToken, - bytes32 destinationCaller - ) public returns (uint64) { - ERC20Mock(USDC).burn(msg.sender, amount); - nonce += 1; - message = MockMessage(amount, destinationDomain, mintRecipient, burnToken, destinationCaller); - packedMessage = abi.encode(message); - return nonce; - } -} diff --git a/src/testing/WormholeRelayerTest.sol b/src/testing/WormholeRelayerTest.sol index a16b243..6a22229 100644 --- a/src/testing/WormholeRelayerTest.sol +++ b/src/testing/WormholeRelayerTest.sol @@ -4,9 +4,12 @@ pragma solidity ^0.8.13; import "../../src/interfaces/IWormholeRelayer.sol"; import "../../src/interfaces/IWormhole.sol"; import "../../src/interfaces/ITokenBridge.sol"; +import "../../src/interfaces/CCTPInterfaces/IMessageTransmitter.sol"; +import "../../src/interfaces/CCTPInterfaces/ITokenMessenger.sol"; import "../../src/Utils.sol"; import "./helpers/WormholeSimulator.sol"; +import "./helpers/CircleCCTPSimulator.sol"; import "./ERC20Mock.sol"; import "./helpers/DeliveryInstructionDecoder.sol"; import "./helpers/ExecutionParameters.sol"; @@ -22,6 +25,15 @@ struct ChainInfo { IWormholeRelayer relayer; ITokenBridge tokenBridge; IWormhole wormhole; + IMessageTransmitter circleMessageTransmitter; + ITokenMessenger circleTokenMessenger; + IERC20 USDC; +} + +interface USDCMinter { + function mint(address _to, uint256 _amount) external returns (bool); + function masterMinter() external returns (address); + function configureMinter(address minter, uint256 minterAllowedAmount) external returns (bool); } struct ActiveFork { @@ -33,6 +45,11 @@ struct ActiveFork { ITokenBridge tokenBridge; IWormhole wormhole; WormholeSimulator guardian; + // USDC parameters - only non-empty for Ethereum, Avalanche, Optimism, Arbitrum mainnets/testnets + IERC20 USDC; + ITokenMessenger circleTokenMessenger; + IMessageTransmitter circleMessageTransmitter; + CircleMessageTransmitterSimulator circleAttester; } abstract contract WormholeRelayerTest is Test { @@ -47,6 +64,7 @@ abstract contract WormholeRelayerTest is Test { function setUpGeneral() public virtual {} uint256 constant DEVNET_GUARDIAN_PK = 0xcfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0; + uint256 constant CIRCLE_ATTESTER_PK = 0xcfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0; // conveneince information to set up tests against testnet/mainnet forks mapping(uint16 => ChainInfo) public chainInfosTestnet; @@ -85,7 +103,11 @@ abstract contract WormholeRelayerTest is Test { wormhole: chainInfos[i].wormhole, // patch these in setUp() once we have the fork fork: 0, - guardian: WormholeSimulator(address(0)) + guardian: WormholeSimulator(address(0)), + circleMessageTransmitter: chainInfos[i].circleMessageTransmitter, + circleTokenMessenger: chainInfos[i].circleTokenMessenger, + USDC: chainInfos[i].USDC, + circleAttester: CircleMessageTransmitterSimulator(address(0)) }); } } @@ -108,6 +130,10 @@ abstract contract WormholeRelayerTest is Test { address(fork.wormhole), DEVNET_GUARDIAN_PK ); + fork.circleAttester = new CircleMessageTransmitterSimulator( + address(fork.circleMessageTransmitter), + CIRCLE_ATTESTER_PK + ); } // run setUp virtual functions for each fork @@ -119,7 +145,7 @@ abstract contract WormholeRelayerTest is Test { ActiveFork memory firstFork = activeForks[activeForksList[0]]; vm.selectFork(firstFork.fork); - mockOffchainRelayer = new MockOffchainRelayer(address(firstFork.wormhole), address(firstFork.guardian), vm); + mockOffchainRelayer = new MockOffchainRelayer(address(firstFork.wormhole), address(firstFork.guardian), address(firstFork.circleAttester)); // register all active forks with the 'offchain' relayer for (uint256 i = 0; i < activeForksList.length; ++i) { ActiveFork storage fork = activeForks[activeForksList[i]]; @@ -139,6 +165,10 @@ abstract contract WormholeRelayerTest is Test { performDelivery(vm.getRecordedLogs()); } + function performDelivery(bool debugLogging) public { + performDelivery(vm.getRecordedLogs(), debugLogging); + } + function performDelivery(Vm.Log[] memory logs, bool debugLogging) public { require(logs.length > 0, "no events recorded"); mockOffchainRelayer.relay(logs, debugLogging); @@ -174,6 +204,20 @@ abstract contract WormholeRelayerTest is Test { vm.selectFork(originalFork); } + function mintUSDC(uint16 chain, address addr, uint256 amount) public { + uint256 originalFork = vm.activeFork(); + ActiveFork memory current = activeForks[chain]; + vm.selectFork(current.fork); + + USDCMinter tokenMinter = USDCMinter(address(current.USDC)); + vm.prank(tokenMinter.masterMinter()); + tokenMinter.configureMinter(address(this), amount); + tokenMinter.mint(addr, amount); + + vm.selectFork(originalFork); + } + + function logFork() public view { uint256 fork = vm.activeFork(); for (uint256 i = 0; i < activeForksList.length; ++i) { @@ -191,7 +235,10 @@ abstract contract WormholeRelayerTest is Test { url: "https://ethereum-goerli.publicnode.com", relayer: IWormholeRelayer(0x28D8F1Be96f97C1387e94A53e00eCcFb4E75175a), tokenBridge: ITokenBridge(0xF890982f9310df57d00f659cf4fd87e65adEd8d7), - wormhole: IWormhole(0x706abc4E45D419950511e474C7B9Ed348A4a716c) + wormhole: IWormhole(0x706abc4E45D419950511e474C7B9Ed348A4a716c), + circleMessageTransmitter: IMessageTransmitter(0x26413e8157CD32011E726065a5462e97dD4d03D9), + circleTokenMessenger: ITokenMessenger(0xD0C3da58f55358142b8d3e06C1C30c5C6114EFE8), + USDC: IERC20(0x07865c6E87B9F70255377e024ace6630C1Eaa37F) }); chainInfosTestnet[6] = ChainInfo({ chainId: 6, @@ -199,7 +246,10 @@ abstract contract WormholeRelayerTest is Test { url: vm.envOr("AVALANCHE_FUJI_RPC_URL", string("https://api.avax-test.network/ext/bc/C/rpc")), relayer: IWormholeRelayer(0xA3cF45939bD6260bcFe3D66bc73d60f19e49a8BB), tokenBridge: ITokenBridge(0x61E44E506Ca5659E6c0bba9b678586fA2d729756), - wormhole: IWormhole(0x7bbcE28e64B3F8b84d876Ab298393c38ad7aac4C) + wormhole: IWormhole(0x7bbcE28e64B3F8b84d876Ab298393c38ad7aac4C), + circleMessageTransmitter: IMessageTransmitter(0xa9fB1b3009DCb79E2fe346c16a604B8Fa8aE0a79), + circleTokenMessenger: ITokenMessenger(0xeb08f243E5d3FCFF26A9E38Ae5520A669f4019d0), + USDC: IERC20(0x5425890298aed601595a70AB815c96711a31Bc65) }); chainInfosTestnet[14] = ChainInfo({ chainId: 14, @@ -207,7 +257,10 @@ abstract contract WormholeRelayerTest is Test { url: vm.envOr("CELO_TESTNET_RPC_URL", string("https://alfajores-forno.celo-testnet.org")), relayer: IWormholeRelayer(0x306B68267Deb7c5DfCDa3619E22E9Ca39C374f84), tokenBridge: ITokenBridge(0x05ca6037eC51F8b712eD2E6Fa72219FEaE74E153), - wormhole: IWormhole(0x88505117CA88e7dd2eC6EA1E13f0948db2D50D56) + wormhole: IWormhole(0x88505117CA88e7dd2eC6EA1E13f0948db2D50D56), + circleMessageTransmitter: IMessageTransmitter(address(0)), + circleTokenMessenger: ITokenMessenger(address(0)), + USDC: IERC20(address(0)) }); chainInfosTestnet[4] = ChainInfo({ chainId: 4, @@ -215,7 +268,10 @@ abstract contract WormholeRelayerTest is Test { url: vm.envOr("BSC_TESTNET_RPC_URL", string("https://bsc-testnet.public.blastapi.io")), relayer: IWormholeRelayer(0x80aC94316391752A193C1c47E27D382b507c93F3), tokenBridge: ITokenBridge(0x9dcF9D205C9De35334D646BeE44b2D2859712A09), - wormhole: IWormhole(0x68605AD7b15c732a30b1BbC62BE8F2A509D74b4D) + wormhole: IWormhole(0x68605AD7b15c732a30b1BbC62BE8F2A509D74b4D), + circleMessageTransmitter: IMessageTransmitter(address(0)), + circleTokenMessenger: ITokenMessenger(address(0)), + USDC: IERC20(address(0)) }); chainInfosTestnet[5] = ChainInfo({ chainId: 5, @@ -223,7 +279,10 @@ abstract contract WormholeRelayerTest is Test { url: vm.envOr("POLYGON_MUMBAI_RPC_URL", string("https://rpc.ankr.com/polygon_mumbai")), relayer: IWormholeRelayer(0x0591C25ebd0580E0d4F27A82Fc2e24E7489CB5e0), tokenBridge: ITokenBridge(0x377D55a7928c046E18eEbb61977e714d2a76472a), - wormhole: IWormhole(0x0CBE91CF822c73C2315FB05100C2F714765d5c20) + wormhole: IWormhole(0x0CBE91CF822c73C2315FB05100C2F714765d5c20), + circleMessageTransmitter: IMessageTransmitter(address(0)), + circleTokenMessenger: ITokenMessenger(address(0)), + USDC: IERC20(address(0)) }); chainInfosTestnet[16] = ChainInfo({ chainId: 16, @@ -231,7 +290,10 @@ abstract contract WormholeRelayerTest is Test { url: vm.envOr("MOONBASE_ALPHA_RPC_URL", string("https://rpc.testnet.moonbeam.network")), relayer: IWormholeRelayer(0x0591C25ebd0580E0d4F27A82Fc2e24E7489CB5e0), tokenBridge: ITokenBridge(0xbc976D4b9D57E57c3cA52e1Fd136C45FF7955A96), - wormhole: IWormhole(0xa5B7D85a8f27dd7907dc8FdC21FA5657D5E2F901) + wormhole: IWormhole(0xa5B7D85a8f27dd7907dc8FdC21FA5657D5E2F901), + circleMessageTransmitter: IMessageTransmitter(address(0)), + circleTokenMessenger: ITokenMessenger(address(0)), + USDC: IERC20(address(0)) }); chainInfosTestnet[23] = ChainInfo({ chainId: 23, @@ -239,7 +301,10 @@ abstract contract WormholeRelayerTest is Test { url: "https://arbitrum-goerli.publicnode.com", relayer: IWormholeRelayer(0xAd753479354283eEE1b86c9470c84D42f229FF43), tokenBridge: ITokenBridge(0x23908A62110e21C04F3A4e011d24F901F911744A), - wormhole: IWormhole(0xC7A204bDBFe983FCD8d8E61D02b475D4073fF97e) + wormhole: IWormhole(0xC7A204bDBFe983FCD8d8E61D02b475D4073fF97e), + circleMessageTransmitter: IMessageTransmitter(0x109bC137cb64eAb7c0B1ddDd1Edf341467dC2D35), + circleTokenMessenger: ITokenMessenger(0x12Dcfd3Fe2E9EAc2859fd1Ed86d2ab8C5a2f9352), + USDC: IERC20(0xfd064A18f3BF249cf1f87FC203E90D8f650f2d63) }); chainInfosTestnet[24] = ChainInfo({ chainId: 24, @@ -247,7 +312,10 @@ abstract contract WormholeRelayerTest is Test { url: "https://optimism-goerli.publicnode.com", relayer: IWormholeRelayer(0x01A957A525a5b7A72808bA9D10c389674E459891), tokenBridge: ITokenBridge(0xC7A204bDBFe983FCD8d8E61D02b475D4073fF97e), - wormhole: IWormhole(0x6b9C8671cdDC8dEab9c719bB87cBd3e782bA6a35) + wormhole: IWormhole(0x6b9C8671cdDC8dEab9c719bB87cBd3e782bA6a35), + circleMessageTransmitter: IMessageTransmitter(0x9ff9a4da6f2157A9c82CE756f8fD7E0d75be8895), + circleTokenMessenger: ITokenMessenger(0x23A04D5935ed8bC8E3eB78dB3541f0ABfB001c6e), + USDC: IERC20(0xe05606174bac4A6364B31bd0eCA4bf4dD368f8C6) }); chainInfosMainnet[2] = ChainInfo({ @@ -256,7 +324,10 @@ abstract contract WormholeRelayerTest is Test { url: vm.envOr("ETHEREUM_RPC_URL", string("https://rpc.ankr.com/eth")), relayer: IWormholeRelayer(0x27428DD2d3DD32A4D7f7C497eAaa23130d894911), tokenBridge: ITokenBridge(0x3ee18B2214AFF97000D974cf647E7C347E8fa585), - wormhole: IWormhole(0x98f3c9e6E3fAce36bAAd05FE09d375Ef1464288B) + wormhole: IWormhole(0x98f3c9e6E3fAce36bAAd05FE09d375Ef1464288B), + circleMessageTransmitter: IMessageTransmitter(0x0a992d191DEeC32aFe36203Ad87D7d289a738F81), + circleTokenMessenger: ITokenMessenger(0xBd3fa81B58Ba92a82136038B25aDec7066af3155), + USDC: IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48) }); chainInfosMainnet[4] = ChainInfo({ chainId: 4, @@ -264,7 +335,10 @@ abstract contract WormholeRelayerTest is Test { url: vm.envOr("BSC_RPC_URL", string("https://bsc-dataseed2.defibit.io")), relayer: IWormholeRelayer(0x27428DD2d3DD32A4D7f7C497eAaa23130d894911), tokenBridge: ITokenBridge(0xB6F6D86a8f9879A9c87f643768d9efc38c1Da6E7), - wormhole: IWormhole(0x98f3c9e6E3fAce36bAAd05FE09d375Ef1464288B) + wormhole: IWormhole(0x98f3c9e6E3fAce36bAAd05FE09d375Ef1464288B), + circleMessageTransmitter: IMessageTransmitter(address(0)), + circleTokenMessenger: ITokenMessenger(address(0)), + USDC: IERC20(address(0)) }); chainInfosMainnet[6] = ChainInfo({ chainId: 6, @@ -272,7 +346,10 @@ abstract contract WormholeRelayerTest is Test { url: vm.envOr("AVALANCHE_RPC_URL", string("https://rpc.ankr.com/avalanche")), relayer: IWormholeRelayer(0x27428DD2d3DD32A4D7f7C497eAaa23130d894911), tokenBridge: ITokenBridge(0x0e082F06FF657D94310cB8cE8B0D9a04541d8052), - wormhole: IWormhole(0x54a8e5f9c4CbA08F9943965859F6c34eAF03E26c) + wormhole: IWormhole(0x54a8e5f9c4CbA08F9943965859F6c34eAF03E26c), + circleMessageTransmitter: IMessageTransmitter(0x8186359aF5F57FbB40c6b14A588d2A59C0C29880), + circleTokenMessenger: ITokenMessenger(0x6B25532e1060CE10cc3B0A99e5683b91BFDe6982), + USDC: IERC20(0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E) }); chainInfosMainnet[10] = ChainInfo({ chainId: 10, @@ -280,7 +357,10 @@ abstract contract WormholeRelayerTest is Test { url: vm.envOr("FANTOM_RPC_URL", string("https://rpc.ankr.com/fantom")), relayer: IWormholeRelayer(0x27428DD2d3DD32A4D7f7C497eAaa23130d894911), tokenBridge: ITokenBridge(0x7C9Fc5741288cDFdD83CeB07f3ea7e22618D79D2), - wormhole: IWormhole(0x126783A6Cb203a3E35344528B26ca3a0489a1485) + wormhole: IWormhole(0x126783A6Cb203a3E35344528B26ca3a0489a1485), + circleMessageTransmitter: IMessageTransmitter(address(0)), + circleTokenMessenger: ITokenMessenger(address(0)), + USDC: IERC20(address(0)) }); chainInfosMainnet[13] = ChainInfo({ chainId: 13, @@ -288,7 +368,10 @@ abstract contract WormholeRelayerTest is Test { url: vm.envOr("KLAYTN_RPC_URL", string("https://klaytn-mainnet-rpc.allthatnode.com:8551")), relayer: IWormholeRelayer(0x27428DD2d3DD32A4D7f7C497eAaa23130d894911), tokenBridge: ITokenBridge(0x5b08ac39EAED75c0439FC750d9FE7E1F9dD0193F), - wormhole: IWormhole(0x0C21603c4f3a6387e241c0091A7EA39E43E90bb7) + wormhole: IWormhole(0x0C21603c4f3a6387e241c0091A7EA39E43E90bb7), + circleMessageTransmitter: IMessageTransmitter(address(0)), + circleTokenMessenger: ITokenMessenger(address(0)), + USDC: IERC20(address(0)) }); chainInfosMainnet[14] = ChainInfo({ chainId: 14, @@ -296,7 +379,10 @@ abstract contract WormholeRelayerTest is Test { url: vm.envOr("CELO_RPC_URL", string("https://forno.celo.org")), relayer: IWormholeRelayer(0x27428DD2d3DD32A4D7f7C497eAaa23130d894911), tokenBridge: ITokenBridge(0x796Dff6D74F3E27060B71255Fe517BFb23C93eed), - wormhole: IWormhole(0xa321448d90d4e5b0A732867c18eA198e75CAC48E) + wormhole: IWormhole(0xa321448d90d4e5b0A732867c18eA198e75CAC48E), + circleMessageTransmitter: IMessageTransmitter(address(0)), + circleTokenMessenger: ITokenMessenger(address(0)), + USDC: IERC20(address(0)) }); chainInfosMainnet[12] = ChainInfo({ chainId: 12, @@ -304,7 +390,10 @@ abstract contract WormholeRelayerTest is Test { url: vm.envOr("ACALA_RPC_URL", string("https://eth-rpc-acala.aca-api.network")), relayer: IWormholeRelayer(0x27428DD2d3DD32A4D7f7C497eAaa23130d894911), tokenBridge: ITokenBridge(0xae9d7fe007b3327AA64A32824Aaac52C42a6E624), - wormhole: IWormhole(0xa321448d90d4e5b0A732867c18eA198e75CAC48E) + wormhole: IWormhole(0xa321448d90d4e5b0A732867c18eA198e75CAC48E), + circleMessageTransmitter: IMessageTransmitter(address(0)), + circleTokenMessenger: ITokenMessenger(address(0)), + USDC: IERC20(address(0)) }); chainInfosMainnet[11] = ChainInfo({ chainId: 11, @@ -312,7 +401,10 @@ abstract contract WormholeRelayerTest is Test { url: vm.envOr("KARURA_RPC_URL", string("https://eth-rpc-karura.aca-api.network")), relayer: IWormholeRelayer(0x27428DD2d3DD32A4D7f7C497eAaa23130d894911), tokenBridge: ITokenBridge(0xae9d7fe007b3327AA64A32824Aaac52C42a6E624), - wormhole: IWormhole(0xa321448d90d4e5b0A732867c18eA198e75CAC48E) + wormhole: IWormhole(0xa321448d90d4e5b0A732867c18eA198e75CAC48E), + circleMessageTransmitter: IMessageTransmitter(address(0)), + circleTokenMessenger: ITokenMessenger(address(0)), + USDC: IERC20(address(0)) }); chainInfosMainnet[16] = ChainInfo({ chainId: 16, @@ -320,7 +412,10 @@ abstract contract WormholeRelayerTest is Test { url: vm.envOr("MOOMBEAM_RPC_URL", string("https://rpc.ankr.com/moonbeam")), relayer: IWormholeRelayer(0x27428DD2d3DD32A4D7f7C497eAaa23130d894911), tokenBridge: ITokenBridge(0xB1731c586ca89a23809861c6103F0b96B3F57D92), - wormhole: IWormhole(0xC8e2b0cD52Cf01b0Ce87d389Daa3d414d4cE29f3) + wormhole: IWormhole(0xC8e2b0cD52Cf01b0Ce87d389Daa3d414d4cE29f3), + circleMessageTransmitter: IMessageTransmitter(address(0)), + circleTokenMessenger: ITokenMessenger(address(0)), + USDC: IERC20(address(0)) }); chainInfosMainnet[23] = ChainInfo({ chainId: 23, @@ -328,7 +423,10 @@ abstract contract WormholeRelayerTest is Test { url: vm.envOr("ARBITRUM_RPC_URL", string("https://rpc.ankr.com/arbitrum")), relayer: IWormholeRelayer(0x27428DD2d3DD32A4D7f7C497eAaa23130d894911), tokenBridge: ITokenBridge(0x0b2402144Bb366A632D14B83F244D2e0e21bD39c), - wormhole: IWormhole(0xa5f208e072434bC67592E4C49C1B991BA79BCA46) + wormhole: IWormhole(0xa5f208e072434bC67592E4C49C1B991BA79BCA46), + circleMessageTransmitter: IMessageTransmitter(0xC30362313FBBA5cf9163F0bb16a0e01f01A896ca), + circleTokenMessenger: ITokenMessenger(0x19330d10D9Cc8751218eaf51E8885D058642E08A), + USDC: IERC20(0xaf88d065e77c8cC2239327C5EDb3A432268e5831) }); chainInfosMainnet[24] = ChainInfo({ chainId: 24, @@ -336,7 +434,10 @@ abstract contract WormholeRelayerTest is Test { url: vm.envOr("OPTIMISM_RPC_URL", string("https://rpc.ankr.com/optimism")), relayer: IWormholeRelayer(0x27428DD2d3DD32A4D7f7C497eAaa23130d894911), tokenBridge: ITokenBridge(0x1D68124e65faFC907325e3EDbF8c4d84499DAa8b), - wormhole: IWormhole(0xEe91C335eab126dF5fDB3797EA9d6aD93aeC9722) + wormhole: IWormhole(0xEe91C335eab126dF5fDB3797EA9d6aD93aeC9722), + circleMessageTransmitter: IMessageTransmitter(0x4D41f22c5a0e5c74090899E5a8Fb597a8842b3e8), + circleTokenMessenger: ITokenMessenger(0x2B4069517957735bE00ceE0fadAE88a26365528f), + USDC: IERC20(0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85) }); } @@ -384,6 +485,10 @@ abstract contract WormholeRelayerBasicTest is WormholeRelayerTest { WormholeSimulator public guardianSource; WormholeSimulator public guardianTarget; + CircleMessageTransmitterSimulator public circleAttesterSource; + CircleMessageTransmitterSimulator public circleAttesterTarget; + + /* * end activeForks aliases */ @@ -399,6 +504,8 @@ abstract contract WormholeRelayerBasicTest is WormholeRelayerTest { // aliases can't be set until after setUp guardianSource = activeForks[activeForksList[0]].guardian; guardianTarget = activeForks[activeForksList[1]].guardian; + circleAttesterSource = activeForks[activeForksList[0]].circleAttester; + circleAttesterTarget = activeForks[activeForksList[1]].circleAttester; sourceFork = activeForks[activeForksList[0]].fork; targetFork = activeForks[activeForksList[1]].fork; } diff --git a/src/testing/helpers/CircleCCTPSimulator.sol b/src/testing/helpers/CircleCCTPSimulator.sol index 910eea2..c191df6 100644 --- a/src/testing/helpers/CircleCCTPSimulator.sol +++ b/src/testing/helpers/CircleCCTPSimulator.sol @@ -7,10 +7,14 @@ import "./BytesLib.sol"; import "forge-std/Vm.sol"; import "forge-std/console.sol"; +import {CCTPMessageLib} from "../../CCTPBase.sol"; + interface MessageTransmitterViewAttesterManager { function attesterManager() external view returns (address); function enableAttester(address newAttester) external; + + function setSignatureThreshold(uint256 newSignatureThreshold) external; } /** @@ -23,6 +27,8 @@ interface MessageTransmitterViewAttesterManager { contract CircleMessageTransmitterSimulator { using BytesLib for bytes; + bool public valid; + // Taken from forge-std/Script.sol address private constant VM_ADDRESS = address(bytes20(uint160(uint256(keccak256("hevm cheat code"))))); @@ -41,7 +47,8 @@ contract CircleMessageTransmitterSimulator { constructor(address messageTransmitter_, uint256 attesterPrivateKey_) { messageTransmitter = IMessageTransmitter(messageTransmitter_); attesterPrivateKey = attesterPrivateKey_; - overrideAttester(vm.addr(attesterPrivateKey)); + valid = messageTransmitter_ != address(0); + if(valid) overrideAttester(vm.addr(attesterPrivateKey)); } function overrideAttester(address attesterPublicKey) internal { @@ -53,13 +60,16 @@ contract CircleMessageTransmitterSimulator { .attesterManager(); vm.prank(attesterManager); attesterManagerInterface.enableAttester(attesterPublicKey); + + vm.prank(attesterManager); + attesterManagerInterface.setSignatureThreshold(1); } } function parseMessageFromMessageTransmitterLog( Vm.Log memory log - ) internal pure returns (bytes memory message) { + ) internal view returns (bytes memory message) { uint256 index = 32; // length of payload @@ -72,7 +82,6 @@ contract CircleMessageTransmitterSimulator { // trailing bytes (due to 32 byte slot overlap) require(log.data.length - index < 32, "Too many extra bytes"); index += log.data.length - index; - require( index == log.data.length, "failed to parse MessageTransmitter message" @@ -90,7 +99,7 @@ contract CircleMessageTransmitterSimulator { for (uint256 i = 0; i < logs.length; i++) { if ( logs[i].topics[0] == - keccak256("event MessageSent(bytes message);") + keccak256("MessageSent(bytes)") ) { count += 1; } @@ -103,7 +112,7 @@ contract CircleMessageTransmitterSimulator { for (uint256 i = 0; i < logs.length; i++) { if ( logs[i].topics[0] == - keccak256("event MessageSent(bytes message);") + keccak256("MessageSent(bytes)") ) { published[publishedIndex] = logs[i]; publishedIndex += 1; @@ -121,11 +130,11 @@ contract CircleMessageTransmitterSimulator { function fetchSignedMessageFromLog( Vm.Log memory log - ) public view returns (bytes memory attestation) { + ) public view returns (CCTPMessageLib.CCTPMessage memory) { // Parse messageTransmitter message from ethereum logs bytes memory message = parseMessageFromMessageTransmitterLog(log); - return signMessage(message); + return CCTPMessageLib.CCTPMessage(message, signMessage(message)); } /** diff --git a/src/testing/helpers/MockOffchainRelayer.sol b/src/testing/helpers/MockOffchainRelayer.sol index 05b105b..793da7c 100644 --- a/src/testing/helpers/MockOffchainRelayer.sol +++ b/src/testing/helpers/MockOffchainRelayer.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.0; import {WormholeSimulator} from "./WormholeSimulator.sol"; +import {CircleMessageTransmitterSimulator} from "./CircleCCTPSimulator.sol"; import {toWormholeFormat, fromWormholeFormat} from "../../Utils.sol"; import "../../interfaces/IWormholeRelayer.sol"; import "../../interfaces/IWormhole.sol"; @@ -16,11 +17,17 @@ import "forge-std/console.sol"; using BytesParsing for bytes; contract MockOffchainRelayer { + uint16 chainIdOfWormholeAndGuardianUtilities; IWormhole relayerWormhole; WormholeSimulator relayerWormholeSimulator; + CircleMessageTransmitterSimulator relayerCircleSimulator; + - Vm public vm; + // Taken from forge-std/Script.sol + address private constant VM_ADDRESS = + address(bytes20(uint160(uint256(keccak256("hevm cheat code"))))); + Vm public constant vm = Vm(VM_ADDRESS); mapping(uint16 => address) wormholeRelayerContracts; @@ -32,10 +39,10 @@ contract MockOffchainRelayer { mapping(bytes32 => bytes) pastEncodedDeliveryVAA; - constructor(address _wormhole, address _wormholeSimulator, Vm vm_) { + constructor(address _wormhole, address _wormholeSimulator, address _circleSimulator) { relayerWormhole = IWormhole(_wormhole); relayerWormholeSimulator = WormholeSimulator(_wormholeSimulator); - vm = vm_; + relayerCircleSimulator = CircleMessageTransmitterSimulator(_circleSimulator); chainIdOfWormholeAndGuardianUtilities = relayerWormhole.chainId(); } @@ -92,6 +99,16 @@ contract MockOffchainRelayer { (vaaKey.sequence == parsedVaa.sequence); } + function cctpKeyMatchesCCTPMessage( + CCTPMessageLib.CCTPKey memory cctpKey, + CCTPMessageLib.CCTPMessage memory cctpMessage + ) internal view returns (bool) { + (uint64 nonce,) = cctpMessage.message.asUint64(12); + (uint32 domain,) = cctpMessage.message.asUint32(8); + return + nonce == cctpKey.nonce && domain == cctpKey.domain; + } + function relay( Vm.Log[] memory logs, bytes memory deliveryOverrides, @@ -119,6 +136,25 @@ contract MockOffchainRelayer { chainId ); } + + bool checkCCTP = relayerCircleSimulator.valid(); + Vm.Log[] memory cctpEntries = new Vm.Log[](0); + if(checkCCTP) { + cctpEntries = relayerCircleSimulator + .fetchMessageTransmitterLogsFromLogs(logs); + } + + if (debugLogging) { + console.log("Found %s circle messages in logs", cctpEntries.length); + } + + CCTPMessageLib.CCTPMessage[] memory circleSignedMessages = new CCTPMessageLib.CCTPMessage[](cctpEntries.length); + for (uint256 i = 0; i < cctpEntries.length; i++) { + circleSignedMessages[i] = relayerCircleSimulator.fetchSignedMessageFromLog( + cctpEntries[i] + ); + } + IWormhole.VM[] memory parsed = new IWormhole.VM[](encodedSignedVaas.length); for (uint16 i = 0; i < encodedSignedVaas.length; i++) { parsed[i] = relayerWormhole.parseVM(encodedSignedVaas[i]); @@ -144,6 +180,7 @@ contract MockOffchainRelayer { genericRelay( encodedSignedVaas[i], encodedSignedVaas, + circleSignedMessages, parsed[i], deliveryOverrides ); @@ -171,6 +208,7 @@ contract MockOffchainRelayer { function genericRelay( bytes memory encodedDeliveryVAA, bytes[] memory encodedSignedVaas, + CCTPMessageLib.CCTPMessage[] memory cctpMessages, IWormhole.VM memory parsedDeliveryVAA, bytes memory deliveryOverrides ) internal { @@ -200,8 +238,13 @@ contract MockOffchainRelayer { } else if (instruction.messageKeys[i].keyType == 2) { // CCTP Key // clean this up - CCTPMessageLib.CCTPMessage memory message = abi.decode(instruction.messageKeys[i].encodedKey, (CCTPMessageLib.CCTPMessage)); - + CCTPMessageLib.CCTPKey memory key = abi.decode(instruction.messageKeys[i].encodedKey, (CCTPMessageLib.CCTPKey)); + for (uint8 j = 0; j < cctpMessages.length; j++) { + if (cctpKeyMatchesCCTPMessage(key, cctpMessages[j])) { + encodedSignedVaasToBeDelivered[i] = abi.encode(cctpMessages[j]); + break; + } + } } } diff --git a/test/CCTPBase.t.sol b/test/CCTPBase.t.sol index f8aa8d7..02ee6b9 100644 --- a/test/CCTPBase.t.sol +++ b/test/CCTPBase.t.sol @@ -7,7 +7,6 @@ import "../src/interfaces/IWormholeRelayer.sol"; import "../src/interfaces/IERC20.sol"; import "../src/testing/WormholeRelayerTest.sol"; -import "../src/testing/CCTPMocks.sol"; import "../src/WormholeRelayerSDK.sol"; import "../src/Utils.sol"; @@ -59,7 +58,7 @@ contract CCTPToy is CCTPSender, CCTPReceiver { IERC20(USDC).transferFrom(msg.sender, address(this), amount); - bytes memory payload = abi.encode(recipient); + bytes memory payload = abi.encode(recipient, amount); sendUSDCWithPayloadToEvm( targetChain, fromWormholeFormat(registeredSenders[targetChain]), // address (on targetChain) to send token and payload to @@ -96,43 +95,47 @@ contract WormholeSDKTest is WormholeRelayerBasicTest { CCTPToy CCTPToyTarget; ERC20Mock USDCSource; ERC20Mock USDCTarget; - MockMessageTransmitter circleMessageTransmitterSource; - MockTokenMessenger circleTokenMessengerSource; - MockMessageTransmitter circleMessageTransmitterTarget; - MockTokenMessenger circleTokenMessengerTarget; constructor() { setTestnetForkChains(2, 6); } function setUpSource() public override { - USDCSource = createAndAttestToken(sourceChain); - circleMessageTransmitterSource = new MockMessageTransmitter(USDCSource); - circleTokenMessengerSource = new MockTokenMessenger(USDCSource); + USDCSource = ERC20Mock(address(sourceChainInfo.USDC)); + mintUSDC(sourceChain, address(this), 5000e18); CCTPToySource = new CCTPToy( address(relayerSource), address(tokenBridgeSource), address(wormholeSource), - address(circleMessageTransmitterSource), - address(circleTokenMessengerSource), + address(sourceChainInfo.circleMessageTransmitter), + address(sourceChainInfo.circleTokenMessenger), address(USDCSource) ); } function setUpTarget() public override { - USDCTarget = createAndAttestToken(targetChain); - circleTokenMessengerTarget = new MockTokenMessenger(USDCTarget); - circleMessageTransmitterTarget = new MockMessageTransmitter(USDCTarget); + USDCTarget = ERC20Mock(address(targetChainInfo.USDC)); + mintUSDC(targetChain, address(this), 5000e18); CCTPToyTarget = new CCTPToy( address(relayerTarget), address(tokenBridgeTarget), address(wormholeTarget), - address(circleMessageTransmitterTarget), - address(circleTokenMessengerTarget), + address(targetChainInfo.circleMessageTransmitter), + address(targetChainInfo.circleTokenMessenger), address(USDCTarget) ); } + function setUpGeneral() public override { + vm.selectFork(sourceFork); + CCTPToySource.setRegisteredSender(targetChain, toWormholeFormat(address(CCTPToyTarget))); + + vm.selectFork(targetFork); + CCTPToyTarget.setRegisteredSender(sourceChain, toWormholeFormat(address(CCTPToySource))); + } + + + // function setUpGeneral() public override { // vm.selectFork(sourceFork); @@ -142,7 +145,7 @@ contract WormholeSDKTest is WormholeRelayerBasicTest { function testSendToken() public { vm.selectFork(sourceFork); - uint256 amount = 19e17; + uint256 amount = 100e6; USDCSource.approve(address(CCTPToySource), amount); vm.selectFork(targetFork); @@ -157,11 +160,10 @@ contract WormholeSDKTest is WormholeRelayerBasicTest { recipient, amount ); - performDelivery(); + performDelivery(true); vm.selectFork(targetFork); - // address wormholeWrappedToken = tokenBridgeTarget.wrappedAsset(sourceChain, toWormholeFormat(address(token))); - // assertEq(IERC20(wormholeWrappedToken).balanceOf(recipient), amount); + assertEq(IERC20(USDCTarget).balanceOf(recipient), amount); } // function testSendTokenWithRefund() public { From 392568dfc7215db54d3ae57638d10f08e7145770 Mon Sep 17 00:00:00 2001 From: derpy-duck <115193320+derpy-duck@users.noreply.github.com> Date: Fri, 15 Sep 2023 17:42:53 -0400 Subject: [PATCH 08/23] Remove console log --- test/CCTPBase.t.sol | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/CCTPBase.t.sol b/test/CCTPBase.t.sol index 02ee6b9..72b19f5 100644 --- a/test/CCTPBase.t.sol +++ b/test/CCTPBase.t.sol @@ -11,8 +11,6 @@ import "../src/testing/WormholeRelayerTest.sol"; import "../src/WormholeRelayerSDK.sol"; import "../src/Utils.sol"; -import "forge-std/console.sol"; - contract CCTPToy is CCTPSender, CCTPReceiver { uint256 constant GAS_LIMIT = 250_000; From 57502e88f99069d93de5bfc0ac22267f0a885541 Mon Sep 17 00:00:00 2001 From: derpy-duck <115193320+derpy-duck@users.noreply.github.com> Date: Tue, 19 Sep 2023 18:20:28 -0400 Subject: [PATCH 09/23] cctp-encoding-decoding-fix --- README.md | 22 ++++++++++++------- src/CCTPBase.sol | 11 +++++----- src/testing/helpers/CircleCCTPSimulator.sol | 2 +- .../helpers/DeliveryInstructionDecoder.sol | 10 +++++++++ src/testing/helpers/MockOffchainRelayer.sol | 7 +++--- 5 files changed, 33 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 3e8a2bb..ebc9230 100644 --- a/README.md +++ b/README.md @@ -14,18 +14,24 @@ forge install wormhole-foundation/wormhole-solidity-sdk [HelloWormhole - Simple cross-chain message sending application](https://github.com/wormhole-foundation/hello-wormhole) -[HelloToken - Simple cross-chain token sending application](https://github.com/wormhole-foundation/hello-token) +[HelloToken - Simple cross-chain token sending application](https://github.com/wormhole-foundation/hello-tokens) + +[HelloUSDC - Simple cross-chain USDC sending application using CCTP](https://github.com/wormhole-foundation/hello-usdc) ### SDK Summary - Includes interfaces to interact with contracts in the Wormhole ecosystem ([src/interfaces](https://github.com/wormhole-foundation/wormhole-solidity-sdk/tree/main/src/interfaces)) - Includes the base class ‘Base’ with helpers for common actions that will typically need to be done within ‘receiveWormholeMessages’: - - [`onlyWormholeRelayer()`](https://github.com/wormhole-foundation/wormhole-solidity-sdk/blob/main/src/WormholeRelayerSDK.sol#L26): Checking that msg.sender is the wormhole relayer contract - - [`replayProtect(bytes32 deliveryHash)`](https://github.com/wormhole-foundation/wormhole-solidity-sdk/blob/main/src/WormholeRelayerSDK.sol#L31): Checking that the current delivery has not already been processed (via the hash) + - [`onlyWormholeRelayer()`](https://github.com/wormhole-foundation/wormhole-solidity-sdk/blob/main/src/Base.sol#L24): Checking that msg.sender is the wormhole relayer contract Sometimes, Cross-chain applications may be set up such that there is one ‘spoke’ contract on every chain, which sends messages to the ‘hub’ contract. If so, we’d ideally only want to allow messages to be sent from these spoke contracts. Included are helpers for this: - - [`setRegisteredSender(uint16 sourceChain, bytes32 sourceAddress)`](https://github.com/wormhole-foundation/wormhole-solidity-sdk/blob/main/src/WormholeRelayerSDK.sol#L49): Setting the specified sender for ‘sourceChain’ to be ‘sourceAddress’ - - [`isRegisteredSender(uint16 sourceChain, bytes32 sourceAddress)`](https://github.com/wormhole-foundation/wormhole-solidity-sdk/blob/main/src/WormholeRelayerSDK.sol#L37): Checking that the sender who requested the delivery is the registered address for that chain - Look at [test/Fork.t.sol](https://github.com/wormhole-foundation/wormhole-solidity-sdk/blob/main/test/Fork.t.sol#L16) for an example usage of Base -- Included are also the ‘[TokenSender](https://github.com/wormhole-foundation/wormhole-solidity-sdk/blob/main/src/WormholeRelayerSDK.sol#L79)’ and ‘[TokenReceiver](https://github.com/wormhole-foundation/wormhole-solidity-sdk/blob/main/src/WormholeRelayerSDK.sol#L186)’ base classes with helpers for smart contracts that wish to send and receive tokens using Wormhole’s TokenBridge. See ‘[HelloToken](https://github.com/wormhole-foundation/hello-token)’ for example usage. + + - [`setRegisteredSender(uint16 sourceChain, bytes32 sourceAddress)`](https://github.com/wormhole-foundation/wormhole-solidity-sdk/blob/main/src/Base.sol#L47): Setting the specified sender for ‘sourceChain’ to be ‘sourceAddress’ + - [`isRegisteredSender(uint16 sourceChain, bytes32 sourceAddress)`](https://github.com/wormhole-foundation/wormhole-solidity-sdk/blob/main/src/Base.sol#L35): Checking that the sender who requested the delivery is the registered address for that chain + + Look at test/Counter.t.sol for an example usage of Base + +- Included are also the ‘[TokenSender](https://github.com/wormhole-foundation/wormhole-solidity-sdk/blob/main/src/TokenBase#L36)’ and ‘[TokenReceiver](https://github.com/wormhole-foundation/wormhole-solidity-sdk/blob/main/src/TokenBase.sol#L126)’ base classes with helpers for smart contracts that wish to send and receive tokens using Wormhole’s TokenBridge. See ‘[HelloToken](https://github.com/wormhole-foundation/hello-token)’ for example usage. +- Included are also the ‘[CCTPSender](https://github.com/wormhole-foundation/wormhole-solidity-sdk/blob/main/src/CCTPBase#L70)’ and ‘[CCTPReceiver](https://github.com/wormhole-foundation/wormhole-solidity-sdk/blob/main/src/CCTPBase.sol#L134)’ base classes with helpers for smart contracts that wish to send and receive both tokens using Wormhole’s TokenBridge as well as USDC using CCTP. See ‘[HelloUSDC](https://github.com/wormhole-foundation/hello-usdc)’ for example usage. - Included are helpers that help set up a local forge testing environment. See ‘[HelloWormhole](https://github.com/wormhole-foundation/hello-wormhole)’ for example usage. -**Note: This code is meant to be used as starter / reference code. Feel free to modify for use in your contracts, and also make sure to audit any code used from here as part of your contracts before deploying to mainnet.** + +**Note: This code is meant to be used as starter / reference code. Feel free to modify for use in your contracts, and also make sure to audit any code used from here as part of your contracts before deploying to mainnet.** \ No newline at end of file diff --git a/src/CCTPBase.sol b/src/CCTPBase.sol index 60a4c6c..ce8f1d2 100644 --- a/src/CCTPBase.sol +++ b/src/CCTPBase.sol @@ -14,13 +14,13 @@ import "./TokenBase.sol"; library CCTPMessageLib { uint8 constant CCTP_KEY_TYPE = 2; - // encoded using standard abi.encode + // encoded using abi.encodePacked(domain, nonce) struct CCTPKey { uint32 domain; uint64 nonce; } - // encoded using standard abi.encode + // encoded using abi.encode(message, signature) struct CCTPMessage { bytes message; bytes signature; @@ -60,10 +60,9 @@ abstract contract CCTPBase is TokenBase { } function redeemUSDC(bytes memory cctpMessage) internal returns (uint256 amount) { - CCTPMessageLib.CCTPMessage memory message = abi.decode(cctpMessage, (CCTPMessageLib.CCTPMessage)); - + (bytes memory message, bytes memory signature) = abi.decode(cctpMessage, (bytes, bytes)); uint256 beforeBalance = IERC20(USDC).balanceOf(address(this)); - circleMessageTransmitter.receiveMessage(message.message, message.signature); + circleMessageTransmitter.receiveMessage(message, signature); return IERC20(USDC).balanceOf(address(this)) - beforeBalance; } } @@ -96,7 +95,7 @@ abstract contract CCTPSender is CCTPBase { addressToBytes32CCTP(targetAddress) ); return MessageKey( - CCTPMessageLib.CCTP_KEY_TYPE, abi.encode(CCTPMessageLib.CCTPKey(getCCTPDomain(targetChain), nonce)) + CCTPMessageLib.CCTP_KEY_TYPE, abi.encodePacked(getCCTPDomain(targetChain), nonce) ); } diff --git a/src/testing/helpers/CircleCCTPSimulator.sol b/src/testing/helpers/CircleCCTPSimulator.sol index c191df6..df5669c 100644 --- a/src/testing/helpers/CircleCCTPSimulator.sol +++ b/src/testing/helpers/CircleCCTPSimulator.sol @@ -69,7 +69,7 @@ contract CircleMessageTransmitterSimulator { function parseMessageFromMessageTransmitterLog( Vm.Log memory log - ) internal view returns (bytes memory message) { + ) internal pure returns (bytes memory message) { uint256 index = 32; // length of payload diff --git a/src/testing/helpers/DeliveryInstructionDecoder.sol b/src/testing/helpers/DeliveryInstructionDecoder.sol index f5ee0a7..a5b436a 100644 --- a/src/testing/helpers/DeliveryInstructionDecoder.sol +++ b/src/testing/helpers/DeliveryInstructionDecoder.sol @@ -3,6 +3,7 @@ pragma solidity ^0.8.13; import "../../../src/interfaces/IWormholeRelayer.sol"; import "./BytesParsing.sol"; +import {CCTPMessageLib} from "../../CCTPBase.sol"; uint8 constant VERSION_VAAKEY = 1; uint8 constant VERSION_DELIVERY_OVERRIDE = 1; @@ -184,6 +185,15 @@ function decodeMessageKeyArray( } } +function decodeCCTPKey( + bytes memory encoded, + uint256 startOffset +) pure returns (CCTPMessageLib.CCTPKey memory cctpKey, uint256 offset) { + offset = startOffset; + (cctpKey.domain, offset) = encoded.asUint32Unchecked(offset); + (cctpKey.nonce, offset) = encoded.asUint64Unchecked(offset); +} + // ------------------------------------------ -------------------------------------------- function encodeBytes(bytes memory payload) pure returns (bytes memory encoded) { diff --git a/src/testing/helpers/MockOffchainRelayer.sol b/src/testing/helpers/MockOffchainRelayer.sol index 793da7c..6807230 100644 --- a/src/testing/helpers/MockOffchainRelayer.sol +++ b/src/testing/helpers/MockOffchainRelayer.sol @@ -102,7 +102,7 @@ contract MockOffchainRelayer { function cctpKeyMatchesCCTPMessage( CCTPMessageLib.CCTPKey memory cctpKey, CCTPMessageLib.CCTPMessage memory cctpMessage - ) internal view returns (bool) { + ) internal pure returns (bool) { (uint64 nonce,) = cctpMessage.message.asUint64(12); (uint32 domain,) = cctpMessage.message.asUint32(8); return @@ -237,11 +237,10 @@ contract MockOffchainRelayer { } } else if (instruction.messageKeys[i].keyType == 2) { // CCTP Key - // clean this up - CCTPMessageLib.CCTPKey memory key = abi.decode(instruction.messageKeys[i].encodedKey, (CCTPMessageLib.CCTPKey)); + (CCTPMessageLib.CCTPKey memory key,) = decodeCCTPKey(instruction.messageKeys[i].encodedKey, 0); for (uint8 j = 0; j < cctpMessages.length; j++) { if (cctpKeyMatchesCCTPMessage(key, cctpMessages[j])) { - encodedSignedVaasToBeDelivered[i] = abi.encode(cctpMessages[j]); + encodedSignedVaasToBeDelivered[i] = abi.encode(cctpMessages[j].message, cctpMessages[j].signature); break; } } From 5f6aa7efa0063747577189dd88910e65fd1ac458 Mon Sep 17 00:00:00 2001 From: derpy-duck <115193320+derpy-duck@users.noreply.github.com> Date: Tue, 26 Sep 2023 11:11:59 -0400 Subject: [PATCH 10/23] USDC automatically check amount --- foundry.toml | 1 + src/CCTPBase.sol | 20 +++++++++++++++----- 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/foundry.toml b/foundry.toml index 40ad0d1..b3b4b94 100644 --- a/foundry.toml +++ b/foundry.toml @@ -3,5 +3,6 @@ solc_version = "0.8.13" src = "src" out = "out" libs = ["lib"] +viaIR = true # See more config options https://github.com/foundry-rs/foundry/tree/master/config \ No newline at end of file diff --git a/src/CCTPBase.sol b/src/CCTPBase.sol index ce8f1d2..23dd5cb 100644 --- a/src/CCTPBase.sol +++ b/src/CCTPBase.sol @@ -106,21 +106,25 @@ abstract contract CCTPSender is CCTPBase { uint256 receiverValue, uint256 gasLimit, uint256 amount - ) internal returns (uint64) { + ) internal returns (uint64 sequence) { MessageKey[] memory messageKeys = new MessageKey[](1); messageKeys[0] = transferUSDC(amount, targetChain, targetAddress); + bytes memory userPayload = abi.encode(amount, payload); + address defaultDeliveryProvider = wormholeRelayer.getDefaultDeliveryProvider(); + (uint256 cost,) = wormholeRelayer.quoteEVMDeliveryPrice(targetChain, receiverValue, gasLimit); - return wormholeRelayer.sendToEvm{value: cost}( + + sequence = wormholeRelayer.sendToEvm{value: cost}( targetChain, targetAddress, - payload, + userPayload, receiverValue, 0, gasLimit, targetChain, address(0x0), - wormholeRelayer.getDefaultDeliveryProvider(), + defaultDeliveryProvider, messageKeys, CONSISTENCY_LEVEL_FINALIZED ); @@ -146,7 +150,13 @@ abstract contract CCTPReceiver is CCTPBase { amountUSDCReceived = redeemUSDC(additionalMessages[0]); } - receivePayloadAndUSDC(payload, amountUSDCReceived, sourceAddress, sourceChain, deliveryHash); + (uint256 amount, bytes memory userPayload) = abi.decode(payload, (uint256, bytes)); + + // Check that the correct amount was received + // It is important to verify that the 'USDC' received is + require(amount == amountUSDCReceived, "Wrong amount received"); + + receivePayloadAndUSDC(userPayload, amountUSDCReceived, sourceAddress, sourceChain, deliveryHash); } function receivePayloadAndUSDC( From 0d255cc8644267cfbd23e9b9b29e99bd8190ad4a Mon Sep 17 00:00:00 2001 From: derpy-duck <115193320+derpy-duck@users.noreply.github.com> Date: Wed, 27 Sep 2023 16:20:23 -0400 Subject: [PATCH 11/23] use source domain instead of target domain --- src/CCTPBase.sol | 12 ++++++------ src/testing/helpers/MockOffchainRelayer.sol | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/CCTPBase.sol b/src/CCTPBase.sol index 23dd5cb..7945c08 100644 --- a/src/CCTPBase.sol +++ b/src/CCTPBase.sol @@ -45,14 +45,14 @@ abstract contract CCTPBase is TokenBase { USDC = _USDC; } - function getCCTPDomain(uint16 targetChain) internal pure returns (uint32) { - if (targetChain == 2) { + function getCCTPDomain(uint16 chain) internal pure returns (uint32) { + if (chain == 2) { return 0; - } else if (targetChain == 6) { + } else if (chain == 6) { return 1; - } else if (targetChain == 23) { + } else if (chain == 23) { return 3; - } else if (targetChain == 24) { + } else if (chain == 24) { return 2; } else { revert("Wrong CCTP Domain"); @@ -95,7 +95,7 @@ abstract contract CCTPSender is CCTPBase { addressToBytes32CCTP(targetAddress) ); return MessageKey( - CCTPMessageLib.CCTP_KEY_TYPE, abi.encodePacked(getCCTPDomain(targetChain), nonce) + CCTPMessageLib.CCTP_KEY_TYPE, abi.encodePacked(getCCTPDomain(wormhole.chainId()), nonce) ); } diff --git a/src/testing/helpers/MockOffchainRelayer.sol b/src/testing/helpers/MockOffchainRelayer.sol index 6807230..6d6777e 100644 --- a/src/testing/helpers/MockOffchainRelayer.sol +++ b/src/testing/helpers/MockOffchainRelayer.sol @@ -104,7 +104,7 @@ contract MockOffchainRelayer { CCTPMessageLib.CCTPMessage memory cctpMessage ) internal pure returns (bool) { (uint64 nonce,) = cctpMessage.message.asUint64(12); - (uint32 domain,) = cctpMessage.message.asUint32(8); + (uint32 domain,) = cctpMessage.message.asUint32(4); return nonce == cctpKey.nonce && domain == cctpKey.domain; } From 422ca4ff5082edce99c83a52c9992445f7b48d8d Mon Sep 17 00:00:00 2001 From: derpy-duck <115193320+derpy-duck@users.noreply.github.com> Date: Wed, 27 Sep 2023 16:33:26 -0400 Subject: [PATCH 12/23] remove license identifier' --- src/Base.sol | 2 +- src/CCTPBase.sol | 2 +- src/TokenBase.sol | 2 +- src/Utils.sol | 2 +- src/WormholeRelayerSDK.sol | 2 +- src/testing/ERC20Mock.sol | 2 +- src/testing/WormholeRelayerTest.sol | 2 +- src/testing/helpers/BytesParsing.sol | 2 +- src/testing/helpers/DeliveryInstructionDecoder.sol | 2 +- test/CCTPBase.t.sol | 2 +- test/ChooseChains.t.sol | 2 +- test/ExtraChains.t.sol | 2 +- test/Fork.t.sol | 2 +- 13 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/Base.sol b/src/Base.sol index 3cc44c2..492fd24 100644 --- a/src/Base.sol +++ b/src/Base.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED + pragma solidity ^0.8.13; import "./interfaces/IWormholeReceiver.sol"; diff --git a/src/CCTPBase.sol b/src/CCTPBase.sol index 7945c08..900062b 100644 --- a/src/CCTPBase.sol +++ b/src/CCTPBase.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED + pragma solidity ^0.8.13; import "./interfaces/IWormholeReceiver.sol"; diff --git a/src/TokenBase.sol b/src/TokenBase.sol index bddad2f..0ebdafb 100644 --- a/src/TokenBase.sol +++ b/src/TokenBase.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED + pragma solidity ^0.8.13; import "./interfaces/IWormholeReceiver.sol"; diff --git a/src/Utils.sol b/src/Utils.sol index 6c51823..4106e78 100644 --- a/src/Utils.sol +++ b/src/Utils.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED + pragma solidity ^0.8.13; import "./interfaces/IWormholeRelayer.sol"; diff --git a/src/WormholeRelayerSDK.sol b/src/WormholeRelayerSDK.sol index 1d529c7..06c9772 100644 --- a/src/WormholeRelayerSDK.sol +++ b/src/WormholeRelayerSDK.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED + pragma solidity ^0.8.13; import "./interfaces/IWormholeReceiver.sol"; diff --git a/src/testing/ERC20Mock.sol b/src/testing/ERC20Mock.sol index 33caf2f..848ec99 100644 --- a/src/testing/ERC20Mock.sol +++ b/src/testing/ERC20Mock.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED + pragma solidity ^0.8.13; import "../interfaces/IERC20.sol"; diff --git a/src/testing/WormholeRelayerTest.sol b/src/testing/WormholeRelayerTest.sol index 6a22229..4449af5 100644 --- a/src/testing/WormholeRelayerTest.sol +++ b/src/testing/WormholeRelayerTest.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED + pragma solidity ^0.8.13; import "../../src/interfaces/IWormholeRelayer.sol"; diff --git a/src/testing/helpers/BytesParsing.sol b/src/testing/helpers/BytesParsing.sol index 56968a1..945da6e 100644 --- a/src/testing/helpers/BytesParsing.sol +++ b/src/testing/helpers/BytesParsing.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED + pragma solidity ^0.8.13; library BytesParsing { diff --git a/src/testing/helpers/DeliveryInstructionDecoder.sol b/src/testing/helpers/DeliveryInstructionDecoder.sol index a5b436a..8158317 100644 --- a/src/testing/helpers/DeliveryInstructionDecoder.sol +++ b/src/testing/helpers/DeliveryInstructionDecoder.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED + pragma solidity ^0.8.13; import "../../../src/interfaces/IWormholeRelayer.sol"; diff --git a/test/CCTPBase.t.sol b/test/CCTPBase.t.sol index 72b19f5..9d66d94 100644 --- a/test/CCTPBase.t.sol +++ b/test/CCTPBase.t.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED + pragma solidity ^0.8.13; import "../src/WormholeRelayerSDK.sol"; diff --git a/test/ChooseChains.t.sol b/test/ChooseChains.t.sol index a358e25..6ea50c3 100644 --- a/test/ChooseChains.t.sol +++ b/test/ChooseChains.t.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED + pragma solidity ^0.8.13; import "../src/WormholeRelayerSDK.sol"; diff --git a/test/ExtraChains.t.sol b/test/ExtraChains.t.sol index cb708c4..e38066a 100644 --- a/test/ExtraChains.t.sol +++ b/test/ExtraChains.t.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED + pragma solidity ^0.8.13; import "../src/WormholeRelayerSDK.sol"; diff --git a/test/Fork.t.sol b/test/Fork.t.sol index 52e3028..e0923c4 100644 --- a/test/Fork.t.sol +++ b/test/Fork.t.sol @@ -1,4 +1,4 @@ -// SPDX-License-Identifier: UNLICENSED + pragma solidity ^0.8.13; import "../src/WormholeRelayerSDK.sol"; From 9fc3cadc3c60c73b8e215f1d71558858a4f09b68 Mon Sep 17 00:00:00 2001 From: derpy-duck <115193320+derpy-duck@users.noreply.github.com> Date: Wed, 27 Sep 2023 16:53:55 -0400 Subject: [PATCH 13/23] remove comment --- test/CCTPBase.t.sol | 33 +-------------------------------- 1 file changed, 1 insertion(+), 32 deletions(-) diff --git a/test/CCTPBase.t.sol b/test/CCTPBase.t.sol index 9d66d94..cae1ace 100644 --- a/test/CCTPBase.t.sol +++ b/test/CCTPBase.t.sol @@ -124,7 +124,7 @@ contract WormholeSDKTest is WormholeRelayerBasicTest { ); } - function setUpGeneral() public override { + function setUpGeneral() public override { vm.selectFork(sourceFork); CCTPToySource.setRegisteredSender(targetChain, toWormholeFormat(address(CCTPToyTarget))); @@ -132,14 +132,6 @@ contract WormholeSDKTest is WormholeRelayerBasicTest { CCTPToyTarget.setRegisteredSender(sourceChain, toWormholeFormat(address(CCTPToySource))); } - - - // function setUpGeneral() public override { - // vm.selectFork(sourceFork); - - // vm.selectFork(targetFork); - // } - function testSendToken() public { vm.selectFork(sourceFork); @@ -164,27 +156,4 @@ contract WormholeSDKTest is WormholeRelayerBasicTest { assertEq(IERC20(USDCTarget).balanceOf(recipient), amount); } - // function testSendTokenWithRefund() public { - // vm.selectFork(sourceFork); - - // uint256 amount = 19e17; - // token.approve(address(CCTPToySource), amount); - - // vm.selectFork(targetFork); - // address recipient = 0x1234567890123456789012345678901234567890; - // address refundAddress = 0x2234567890123456789012345678901234567890; - // vm.selectFork(sourceFork); - // uint256 cost = CCTPToySource.quoteCrossChainDeposit(targetChain); - - // vm.recordLogs(); - // CCTPToySource.sendCrossChainDeposit{value: cost}( - // targetChain, recipient, amount, address(token), targetChain, refundAddress - // ); - // performDelivery(); - - // vm.selectFork(targetFork); - // address wormholeWrappedToken = tokenBridgeTarget.wrappedAsset(sourceChain, toWormholeFormat(address(token))); - // assertEq(IERC20(wormholeWrappedToken).balanceOf(recipient), amount); - // assertTrue(refundAddress.balance > 0); - // } } From cb004f89e02e9d4a049e52578c779a4adfdd1f4c Mon Sep 17 00:00:00 2001 From: derpy-duck <115193320+derpy-duck@users.noreply.github.com> Date: Wed, 27 Sep 2023 16:57:09 -0400 Subject: [PATCH 14/23] fix via ir --- foundry.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/foundry.toml b/foundry.toml index b3b4b94..dd87c33 100644 --- a/foundry.toml +++ b/foundry.toml @@ -3,6 +3,6 @@ solc_version = "0.8.13" src = "src" out = "out" libs = ["lib"] -viaIR = true +via_ir = true # See more config options https://github.com/foundry-rs/foundry/tree/master/config \ No newline at end of file From 89f92469a4a7f771874499cb869d826f4eaff85f Mon Sep 17 00:00:00 2001 From: derpy-duck <115193320+derpy-duck@users.noreply.github.com> Date: Mon, 2 Oct 2023 10:58:42 -0400 Subject: [PATCH 15/23] add base --- src/testing/WormholeRelayerTest.sol | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/src/testing/WormholeRelayerTest.sol b/src/testing/WormholeRelayerTest.sol index 4449af5..07a02ea 100644 --- a/src/testing/WormholeRelayerTest.sol +++ b/src/testing/WormholeRelayerTest.sol @@ -298,7 +298,7 @@ abstract contract WormholeRelayerTest is Test { chainInfosTestnet[23] = ChainInfo({ chainId: 23, name: "goerli - arbitrum", - url: "https://arbitrum-goerli.publicnode.com", + url: vm.envOr("ARBITRUM_TESTNET_RPC_URL, string("https://arbitrum-goerli.publicnode.com")), relayer: IWormholeRelayer(0xAd753479354283eEE1b86c9470c84D42f229FF43), tokenBridge: ITokenBridge(0x23908A62110e21C04F3A4e011d24F901F911744A), wormhole: IWormhole(0xC7A204bDBFe983FCD8d8E61D02b475D4073fF97e), @@ -309,7 +309,7 @@ abstract contract WormholeRelayerTest is Test { chainInfosTestnet[24] = ChainInfo({ chainId: 24, name: "goerli - optimism", - url: "https://optimism-goerli.publicnode.com", + url: vm.envOr("OPTIMISM_TESTNET_RPC_URL", string("https://optimism-goerli.publicnode.com")), relayer: IWormholeRelayer(0x01A957A525a5b7A72808bA9D10c389674E459891), tokenBridge: ITokenBridge(0xC7A204bDBFe983FCD8d8E61D02b475D4073fF97e), wormhole: IWormhole(0x6b9C8671cdDC8dEab9c719bB87cBd3e782bA6a35), @@ -317,7 +317,17 @@ abstract contract WormholeRelayerTest is Test { circleTokenMessenger: ITokenMessenger(0x23A04D5935ed8bC8E3eB78dB3541f0ABfB001c6e), USDC: IERC20(0xe05606174bac4A6364B31bd0eCA4bf4dD368f8C6) }); - + chainInfosTestnet[30] = ChainInfo({ + chainId: 30, + name: "goerli - base", + url: vm.envOr("BASE_TESTNET_RPC_URL", "https://goerli.base.org"), + relayer: IWormholeRelayer(0xea8029CD7FCAEFFcD1F53686430Db0Fc8ed384E1), + tokenBridge: ITokenBridge(0xA31aa3FDb7aF7Db93d18DDA4e19F811342EDF780), + wormhole: IWormhole(0x23908A62110e21C04F3A4e011d24F901F911744A), + circleMessageTransmitter: IMessageTransmitter(address(0)), + circleTokenMessenger: ITokenMessenger(address(0)), + USDC: IERC20(address(0)) + }); chainInfosMainnet[2] = ChainInfo({ chainId: 2, name: "ethereum", @@ -439,6 +449,17 @@ abstract contract WormholeRelayerTest is Test { circleTokenMessenger: ITokenMessenger(0x2B4069517957735bE00ceE0fadAE88a26365528f), USDC: IERC20(0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85) }); + chainInfosMainnet[30] = ChainInfo({ + chainId: 30, + name: "base", + url: vm.envOr("BASE_RPC_URL", string("https://mainnet.base.org")), + relayer: IWormholeRelayer(0x706f82e9bb5b0813501714ab5974216704980e31), + tokenBridge: ITokenBridge(0x8d2de8d2f73F1F4cAB472AC9A881C9b123C79627), + wormhole: IWormhole(0xbebdb6C8ddC678FfA9f8748f85C815C556Dd8ac6), + circleMessageTransmitter: IMessageTransmitter(address(0)), + circleTokenMessenger: ITokenMessenger(address(0)), + USDC: IERC20(address(0)) + }); } receive() external payable {} From 3558f7f848522816dbbdb317cacf1dbb091f9ca8 Mon Sep 17 00:00:00 2001 From: derpy-duck <115193320+derpy-duck@users.noreply.github.com> Date: Tue, 3 Oct 2023 10:39:29 -0400 Subject: [PATCH 16/23] review comments --- src/CCTPBase.sol | 71 +++-- src/testing/WormholeRelayerTest.sol | 406 +++++++++++++++++++++------- 2 files changed, 355 insertions(+), 122 deletions(-) diff --git a/src/CCTPBase.sol b/src/CCTPBase.sol index 900062b..936b7f7 100644 --- a/src/CCTPBase.sol +++ b/src/CCTPBase.sol @@ -1,4 +1,3 @@ - pragma solidity ^0.8.13; import "./interfaces/IWormholeReceiver.sol"; @@ -12,6 +11,9 @@ import "./Utils.sol"; import "./TokenBase.sol"; library CCTPMessageLib { + // The second standardized key type is a CCTP Key + // representing a CCTP transfer of USDC + // (on the IWormholeRelayer interface) uint8 constant CCTP_KEY_TYPE = 2; // encoded using abi.encodePacked(domain, nonce) @@ -41,7 +43,9 @@ abstract contract CCTPBase is TokenBase { address _USDC ) TokenBase(_wormholeRelayer, _tokenBridge, _wormhole) { circleTokenMessenger = ITokenMessenger(_circleTokenMessenger); - circleMessageTransmitter = IMessageTransmitter(_circleMessageTransmitter); + circleMessageTransmitter = IMessageTransmitter( + _circleMessageTransmitter + ); USDC = _USDC; } @@ -59,8 +63,13 @@ abstract contract CCTPBase is TokenBase { } } - function redeemUSDC(bytes memory cctpMessage) internal returns (uint256 amount) { - (bytes memory message, bytes memory signature) = abi.decode(cctpMessage, (bytes, bytes)); + function redeemUSDC( + bytes memory cctpMessage + ) internal returns (uint256 amount) { + (bytes memory message, bytes memory signature) = abi.decode( + cctpMessage, + (bytes, bytes) + ); uint256 beforeBalance = IERC20(USDC).balanceOf(address(this)); circleMessageTransmitter.receiveMessage(message, signature); return IERC20(USDC).balanceOf(address(this)) - beforeBalance; @@ -82,21 +91,25 @@ abstract contract CCTPSender is CCTPBase { * */ - function transferUSDC(uint256 amount, uint16 targetChain, address targetAddress) - internal - returns (MessageKey memory) - { + function transferUSDC( + uint256 amount, + uint16 targetChain, + address targetAddress + ) internal returns (MessageKey memory) { IERC20(USDC).approve(address(circleTokenMessenger), amount); + bytes32 targetAddressBytes32 = addressToBytes32CCTP(targetAddress); uint64 nonce = circleTokenMessenger.depositForBurnWithCaller( amount, getCCTPDomain(targetChain), - addressToBytes32CCTP(targetAddress), + targetAddressBytes32, USDC, - addressToBytes32CCTP(targetAddress) - ); - return MessageKey( - CCTPMessageLib.CCTP_KEY_TYPE, abi.encodePacked(getCCTPDomain(wormhole.chainId()), nonce) + targetAddressBytes32 ); + return + MessageKey( + CCTPMessageLib.CCTP_KEY_TYPE, + abi.encodePacked(getCCTPDomain(wormhole.chainId()), nonce) + ); } function sendUSDCWithPayloadToEvm( @@ -111,10 +124,15 @@ abstract contract CCTPSender is CCTPBase { messageKeys[0] = transferUSDC(amount, targetChain, targetAddress); bytes memory userPayload = abi.encode(amount, payload); - address defaultDeliveryProvider = wormholeRelayer.getDefaultDeliveryProvider(); + address defaultDeliveryProvider = wormholeRelayer + .getDefaultDeliveryProvider(); + + (uint256 cost, ) = wormholeRelayer.quoteEVMDeliveryPrice( + targetChain, + receiverValue, + gasLimit + ); - (uint256 cost,) = wormholeRelayer.quoteEVMDeliveryPrice(targetChain, receiverValue, gasLimit); - sequence = wormholeRelayer.sendToEvm{value: cost}( targetChain, targetAddress, @@ -143,20 +161,33 @@ abstract contract CCTPReceiver is CCTPBase { uint16 sourceChain, bytes32 deliveryHash ) external payable { - require(additionalMessages.length <= 1, "CCTP: At most one Message is supported"); + require( + additionalMessages.length <= 1, + "CCTP: At most one Message is supported" + ); uint256 amountUSDCReceived; if (additionalMessages.length == 1) { amountUSDCReceived = redeemUSDC(additionalMessages[0]); } - (uint256 amount, bytes memory userPayload) = abi.decode(payload, (uint256, bytes)); + (uint256 amount, bytes memory userPayload) = abi.decode( + payload, + (uint256, bytes) + ); // Check that the correct amount was received - // It is important to verify that the 'USDC' received is + // It is important to verify that the 'USDC' sent in by the relayer is the same amount + // that the sender sent in on the source chain require(amount == amountUSDCReceived, "Wrong amount received"); - receivePayloadAndUSDC(userPayload, amountUSDCReceived, sourceAddress, sourceChain, deliveryHash); + receivePayloadAndUSDC( + userPayload, + amountUSDCReceived, + sourceAddress, + sourceChain, + deliveryHash + ); } function receivePayloadAndUSDC( diff --git a/src/testing/WormholeRelayerTest.sol b/src/testing/WormholeRelayerTest.sol index 07a02ea..14679f6 100644 --- a/src/testing/WormholeRelayerTest.sol +++ b/src/testing/WormholeRelayerTest.sol @@ -1,4 +1,3 @@ - pragma solidity ^0.8.13; import "../../src/interfaces/IWormholeRelayer.sol"; @@ -32,8 +31,13 @@ struct ChainInfo { interface USDCMinter { function mint(address _to, uint256 _amount) external returns (bool); + function masterMinter() external returns (address); - function configureMinter(address minter, uint256 minterAllowedAmount) external returns (bool); + + function configureMinter( + address minter, + uint256 minterAllowedAmount + ) external returns (bool); } struct ActiveFork { @@ -63,8 +67,10 @@ abstract contract WormholeRelayerTest is Test { */ function setUpGeneral() public virtual {} - uint256 constant DEVNET_GUARDIAN_PK = 0xcfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0; - uint256 constant CIRCLE_ATTESTER_PK = 0xcfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0; + uint256 constant DEVNET_GUARDIAN_PK = + 0xcfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0; + uint256 constant CIRCLE_ATTESTER_PK = + 0xcfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0; // conveneince information to set up tests against testnet/mainnet forks mapping(uint16 => ChainInfo) public chainInfosTestnet; @@ -104,7 +110,8 @@ abstract contract WormholeRelayerTest is Test { // patch these in setUp() once we have the fork fork: 0, guardian: WormholeSimulator(address(0)), - circleMessageTransmitter: chainInfos[i].circleMessageTransmitter, + circleMessageTransmitter: chainInfos[i] + .circleMessageTransmitter, circleTokenMessenger: chainInfos[i].circleTokenMessenger, USDC: chainInfos[i].USDC, circleAttester: CircleMessageTransmitterSimulator(address(0)) @@ -145,11 +152,19 @@ abstract contract WormholeRelayerTest is Test { ActiveFork memory firstFork = activeForks[activeForksList[0]]; vm.selectFork(firstFork.fork); - mockOffchainRelayer = new MockOffchainRelayer(address(firstFork.wormhole), address(firstFork.guardian), address(firstFork.circleAttester)); + mockOffchainRelayer = new MockOffchainRelayer( + address(firstFork.wormhole), + address(firstFork.guardian), + address(firstFork.circleAttester) + ); // register all active forks with the 'offchain' relayer for (uint256 i = 0; i < activeForksList.length; ++i) { ActiveFork storage fork = activeForks[activeForksList[i]]; - mockOffchainRelayer.registerChain(fork.chainId, address(fork.relayer), fork.fork); + mockOffchainRelayer.registerChain( + fork.chainId, + address(fork.relayer), + fork.fork + ); } // Allow the offchain relayer to work on all forks @@ -179,7 +194,9 @@ abstract contract WormholeRelayerTest is Test { mockOffchainRelayer.relay(logs); } - function createAndAttestToken(uint16 homeChain) public returns (ERC20Mock token) { + function createAndAttestToken( + uint16 homeChain + ) public returns (ERC20Mock token) { uint256 originalFork = vm.activeFork(); ActiveFork memory home = activeForks[homeChain]; vm.selectFork(home.fork); @@ -189,11 +206,16 @@ abstract contract WormholeRelayerTest is Test { vm.recordLogs(); home.tokenBridge.attestToken(address(token), 0); - Vm.Log memory log = home.guardian.fetchWormholeMessageFromLog(vm.getRecordedLogs())[0]; - bytes memory attestation = home.guardian.fetchSignedMessageFromLogs(log, home.chainId); + Vm.Log memory log = home.guardian.fetchWormholeMessageFromLog( + vm.getRecordedLogs() + )[0]; + bytes memory attestation = home.guardian.fetchSignedMessageFromLogs( + log, + home.chainId + ); for (uint256 i = 0; i < activeForksList.length; ++i) { - if(activeForksList[i] == home.chainId) { + if (activeForksList[i] == home.chainId) { continue; } ActiveFork memory fork = activeForks[activeForksList[i]]; @@ -217,12 +239,14 @@ abstract contract WormholeRelayerTest is Test { vm.selectFork(originalFork); } - function logFork() public view { uint256 fork = vm.activeFork(); for (uint256 i = 0; i < activeForksList.length; ++i) { if (fork == activeForks[activeForksList[i]].fork) { - console.log("%s fork active", activeForks[activeForksList[i]].name); + console.log( + "%s fork active", + activeForks[activeForksList[i]].name + ); return; } } @@ -233,30 +257,56 @@ abstract contract WormholeRelayerTest is Test { chainId: 2, name: "goerli - ethereum", url: "https://ethereum-goerli.publicnode.com", - relayer: IWormholeRelayer(0x28D8F1Be96f97C1387e94A53e00eCcFb4E75175a), - tokenBridge: ITokenBridge(0xF890982f9310df57d00f659cf4fd87e65adEd8d7), + relayer: IWormholeRelayer( + 0x28D8F1Be96f97C1387e94A53e00eCcFb4E75175a + ), + tokenBridge: ITokenBridge( + 0xF890982f9310df57d00f659cf4fd87e65adEd8d7 + ), wormhole: IWormhole(0x706abc4E45D419950511e474C7B9Ed348A4a716c), - circleMessageTransmitter: IMessageTransmitter(0x26413e8157CD32011E726065a5462e97dD4d03D9), - circleTokenMessenger: ITokenMessenger(0xD0C3da58f55358142b8d3e06C1C30c5C6114EFE8), + circleMessageTransmitter: IMessageTransmitter( + 0x26413e8157CD32011E726065a5462e97dD4d03D9 + ), + circleTokenMessenger: ITokenMessenger( + 0xD0C3da58f55358142b8d3e06C1C30c5C6114EFE8 + ), USDC: IERC20(0x07865c6E87B9F70255377e024ace6630C1Eaa37F) }); chainInfosTestnet[6] = ChainInfo({ chainId: 6, name: "fuji - avalanche", - url: vm.envOr("AVALANCHE_FUJI_RPC_URL", string("https://api.avax-test.network/ext/bc/C/rpc")), - relayer: IWormholeRelayer(0xA3cF45939bD6260bcFe3D66bc73d60f19e49a8BB), - tokenBridge: ITokenBridge(0x61E44E506Ca5659E6c0bba9b678586fA2d729756), + url: vm.envOr( + "AVALANCHE_FUJI_RPC_URL", + string("https://api.avax-test.network/ext/bc/C/rpc") + ), + relayer: IWormholeRelayer( + 0xA3cF45939bD6260bcFe3D66bc73d60f19e49a8BB + ), + tokenBridge: ITokenBridge( + 0x61E44E506Ca5659E6c0bba9b678586fA2d729756 + ), wormhole: IWormhole(0x7bbcE28e64B3F8b84d876Ab298393c38ad7aac4C), - circleMessageTransmitter: IMessageTransmitter(0xa9fB1b3009DCb79E2fe346c16a604B8Fa8aE0a79), - circleTokenMessenger: ITokenMessenger(0xeb08f243E5d3FCFF26A9E38Ae5520A669f4019d0), + circleMessageTransmitter: IMessageTransmitter( + 0xa9fB1b3009DCb79E2fe346c16a604B8Fa8aE0a79 + ), + circleTokenMessenger: ITokenMessenger( + 0xeb08f243E5d3FCFF26A9E38Ae5520A669f4019d0 + ), USDC: IERC20(0x5425890298aed601595a70AB815c96711a31Bc65) }); chainInfosTestnet[14] = ChainInfo({ chainId: 14, name: "alfajores - celo", - url: vm.envOr("CELO_TESTNET_RPC_URL", string("https://alfajores-forno.celo-testnet.org")), - relayer: IWormholeRelayer(0x306B68267Deb7c5DfCDa3619E22E9Ca39C374f84), - tokenBridge: ITokenBridge(0x05ca6037eC51F8b712eD2E6Fa72219FEaE74E153), + url: vm.envOr( + "CELO_TESTNET_RPC_URL", + string("https://alfajores-forno.celo-testnet.org") + ), + relayer: IWormholeRelayer( + 0x306B68267Deb7c5DfCDa3619E22E9Ca39C374f84 + ), + tokenBridge: ITokenBridge( + 0x05ca6037eC51F8b712eD2E6Fa72219FEaE74E153 + ), wormhole: IWormhole(0x88505117CA88e7dd2eC6EA1E13f0948db2D50D56), circleMessageTransmitter: IMessageTransmitter(address(0)), circleTokenMessenger: ITokenMessenger(address(0)), @@ -265,9 +315,16 @@ abstract contract WormholeRelayerTest is Test { chainInfosTestnet[4] = ChainInfo({ chainId: 4, name: "bsc testnet", - url: vm.envOr("BSC_TESTNET_RPC_URL", string("https://bsc-testnet.public.blastapi.io")), - relayer: IWormholeRelayer(0x80aC94316391752A193C1c47E27D382b507c93F3), - tokenBridge: ITokenBridge(0x9dcF9D205C9De35334D646BeE44b2D2859712A09), + url: vm.envOr( + "BSC_TESTNET_RPC_URL", + string("https://bsc-testnet.public.blastapi.io") + ), + relayer: IWormholeRelayer( + 0x80aC94316391752A193C1c47E27D382b507c93F3 + ), + tokenBridge: ITokenBridge( + 0x9dcF9D205C9De35334D646BeE44b2D2859712A09 + ), wormhole: IWormhole(0x68605AD7b15c732a30b1BbC62BE8F2A509D74b4D), circleMessageTransmitter: IMessageTransmitter(address(0)), circleTokenMessenger: ITokenMessenger(address(0)), @@ -276,9 +333,16 @@ abstract contract WormholeRelayerTest is Test { chainInfosTestnet[5] = ChainInfo({ chainId: 5, name: "polygon mumbai", - url: vm.envOr("POLYGON_MUMBAI_RPC_URL", string("https://rpc.ankr.com/polygon_mumbai")), - relayer: IWormholeRelayer(0x0591C25ebd0580E0d4F27A82Fc2e24E7489CB5e0), - tokenBridge: ITokenBridge(0x377D55a7928c046E18eEbb61977e714d2a76472a), + url: vm.envOr( + "POLYGON_MUMBAI_RPC_URL", + string("https://rpc.ankr.com/polygon_mumbai") + ), + relayer: IWormholeRelayer( + 0x0591C25ebd0580E0d4F27A82Fc2e24E7489CB5e0 + ), + tokenBridge: ITokenBridge( + 0x377D55a7928c046E18eEbb61977e714d2a76472a + ), wormhole: IWormhole(0x0CBE91CF822c73C2315FB05100C2F714765d5c20), circleMessageTransmitter: IMessageTransmitter(address(0)), circleTokenMessenger: ITokenMessenger(address(0)), @@ -287,9 +351,16 @@ abstract contract WormholeRelayerTest is Test { chainInfosTestnet[16] = ChainInfo({ chainId: 16, name: "moonbase alpha - moonbeam", - url: vm.envOr("MOONBASE_ALPHA_RPC_URL", string("https://rpc.testnet.moonbeam.network")), - relayer: IWormholeRelayer(0x0591C25ebd0580E0d4F27A82Fc2e24E7489CB5e0), - tokenBridge: ITokenBridge(0xbc976D4b9D57E57c3cA52e1Fd136C45FF7955A96), + url: vm.envOr( + "MOONBASE_ALPHA_RPC_URL", + string("https://rpc.testnet.moonbeam.network") + ), + relayer: IWormholeRelayer( + 0x0591C25ebd0580E0d4F27A82Fc2e24E7489CB5e0 + ), + tokenBridge: ITokenBridge( + 0xbc976D4b9D57E57c3cA52e1Fd136C45FF7955A96 + ), wormhole: IWormhole(0xa5B7D85a8f27dd7907dc8FdC21FA5657D5E2F901), circleMessageTransmitter: IMessageTransmitter(address(0)), circleTokenMessenger: ITokenMessenger(address(0)), @@ -298,31 +369,60 @@ abstract contract WormholeRelayerTest is Test { chainInfosTestnet[23] = ChainInfo({ chainId: 23, name: "goerli - arbitrum", - url: vm.envOr("ARBITRUM_TESTNET_RPC_URL, string("https://arbitrum-goerli.publicnode.com")), - relayer: IWormholeRelayer(0xAd753479354283eEE1b86c9470c84D42f229FF43), - tokenBridge: ITokenBridge(0x23908A62110e21C04F3A4e011d24F901F911744A), + url: vm.envOr( + "ARBITRUM_TESTNET_RPC_URL", + string("https://arbitrum-goerli.publicnode.com") + ), + relayer: IWormholeRelayer( + 0xAd753479354283eEE1b86c9470c84D42f229FF43 + ), + tokenBridge: ITokenBridge( + 0x23908A62110e21C04F3A4e011d24F901F911744A + ), wormhole: IWormhole(0xC7A204bDBFe983FCD8d8E61D02b475D4073fF97e), - circleMessageTransmitter: IMessageTransmitter(0x109bC137cb64eAb7c0B1ddDd1Edf341467dC2D35), - circleTokenMessenger: ITokenMessenger(0x12Dcfd3Fe2E9EAc2859fd1Ed86d2ab8C5a2f9352), + circleMessageTransmitter: IMessageTransmitter( + 0x109bC137cb64eAb7c0B1ddDd1Edf341467dC2D35 + ), + circleTokenMessenger: ITokenMessenger( + 0x12Dcfd3Fe2E9EAc2859fd1Ed86d2ab8C5a2f9352 + ), USDC: IERC20(0xfd064A18f3BF249cf1f87FC203E90D8f650f2d63) }); chainInfosTestnet[24] = ChainInfo({ chainId: 24, name: "goerli - optimism", - url: vm.envOr("OPTIMISM_TESTNET_RPC_URL", string("https://optimism-goerli.publicnode.com")), - relayer: IWormholeRelayer(0x01A957A525a5b7A72808bA9D10c389674E459891), - tokenBridge: ITokenBridge(0xC7A204bDBFe983FCD8d8E61D02b475D4073fF97e), + url: vm.envOr( + "OPTIMISM_TESTNET_RPC_URL", + string("https://optimism-goerli.publicnode.com") + ), + relayer: IWormholeRelayer( + 0x01A957A525a5b7A72808bA9D10c389674E459891 + ), + tokenBridge: ITokenBridge( + 0xC7A204bDBFe983FCD8d8E61D02b475D4073fF97e + ), wormhole: IWormhole(0x6b9C8671cdDC8dEab9c719bB87cBd3e782bA6a35), - circleMessageTransmitter: IMessageTransmitter(0x9ff9a4da6f2157A9c82CE756f8fD7E0d75be8895), - circleTokenMessenger: ITokenMessenger(0x23A04D5935ed8bC8E3eB78dB3541f0ABfB001c6e), + circleMessageTransmitter: IMessageTransmitter( + 0x9ff9a4da6f2157A9c82CE756f8fD7E0d75be8895 + ), + circleTokenMessenger: ITokenMessenger( + 0x23A04D5935ed8bC8E3eB78dB3541f0ABfB001c6e + ), USDC: IERC20(0xe05606174bac4A6364B31bd0eCA4bf4dD368f8C6) }); chainInfosTestnet[30] = ChainInfo({ chainId: 30, name: "goerli - base", - url: vm.envOr("BASE_TESTNET_RPC_URL", "https://goerli.base.org"), - relayer: IWormholeRelayer(0xea8029CD7FCAEFFcD1F53686430Db0Fc8ed384E1), - tokenBridge: ITokenBridge(0xA31aa3FDb7aF7Db93d18DDA4e19F811342EDF780), + url: vm.envOr( + "BASE_TESTNET_RPC_URL", + string("https://goerli.base.org") + ), + relayer: IWormholeRelayer( + 0xea8029CD7FCAEFFcD1F53686430Db0Fc8ed384E1 + ), + tokenBridge: ITokenBridge( + 0xA31aa3FDb7aF7Db93d18DDA4e19F811342EDF780 + ), wormhole: IWormhole(0x23908A62110e21C04F3A4e011d24F901F911744A), circleMessageTransmitter: IMessageTransmitter(address(0)), circleTokenMessenger: ITokenMessenger(address(0)), @@ -331,20 +431,38 @@ abstract contract WormholeRelayerTest is Test { chainInfosMainnet[2] = ChainInfo({ chainId: 2, name: "ethereum", - url: vm.envOr("ETHEREUM_RPC_URL", string("https://rpc.ankr.com/eth")), - relayer: IWormholeRelayer(0x27428DD2d3DD32A4D7f7C497eAaa23130d894911), - tokenBridge: ITokenBridge(0x3ee18B2214AFF97000D974cf647E7C347E8fa585), + url: vm.envOr( + "ETHEREUM_RPC_URL", + string("https://rpc.ankr.com/eth") + ), + relayer: IWormholeRelayer( + 0x27428DD2d3DD32A4D7f7C497eAaa23130d894911 + ), + tokenBridge: ITokenBridge( + 0x3ee18B2214AFF97000D974cf647E7C347E8fa585 + ), wormhole: IWormhole(0x98f3c9e6E3fAce36bAAd05FE09d375Ef1464288B), - circleMessageTransmitter: IMessageTransmitter(0x0a992d191DEeC32aFe36203Ad87D7d289a738F81), - circleTokenMessenger: ITokenMessenger(0xBd3fa81B58Ba92a82136038B25aDec7066af3155), + circleMessageTransmitter: IMessageTransmitter( + 0x0a992d191DEeC32aFe36203Ad87D7d289a738F81 + ), + circleTokenMessenger: ITokenMessenger( + 0xBd3fa81B58Ba92a82136038B25aDec7066af3155 + ), USDC: IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48) }); chainInfosMainnet[4] = ChainInfo({ chainId: 4, name: "bsc", - url: vm.envOr("BSC_RPC_URL", string("https://bsc-dataseed2.defibit.io")), - relayer: IWormholeRelayer(0x27428DD2d3DD32A4D7f7C497eAaa23130d894911), - tokenBridge: ITokenBridge(0xB6F6D86a8f9879A9c87f643768d9efc38c1Da6E7), + url: vm.envOr( + "BSC_RPC_URL", + string("https://bsc-dataseed2.defibit.io") + ), + relayer: IWormholeRelayer( + 0x27428DD2d3DD32A4D7f7C497eAaa23130d894911 + ), + tokenBridge: ITokenBridge( + 0xB6F6D86a8f9879A9c87f643768d9efc38c1Da6E7 + ), wormhole: IWormhole(0x98f3c9e6E3fAce36bAAd05FE09d375Ef1464288B), circleMessageTransmitter: IMessageTransmitter(address(0)), circleTokenMessenger: ITokenMessenger(address(0)), @@ -353,20 +471,38 @@ abstract contract WormholeRelayerTest is Test { chainInfosMainnet[6] = ChainInfo({ chainId: 6, name: "avalanche", - url: vm.envOr("AVALANCHE_RPC_URL", string("https://rpc.ankr.com/avalanche")), - relayer: IWormholeRelayer(0x27428DD2d3DD32A4D7f7C497eAaa23130d894911), - tokenBridge: ITokenBridge(0x0e082F06FF657D94310cB8cE8B0D9a04541d8052), + url: vm.envOr( + "AVALANCHE_RPC_URL", + string("https://rpc.ankr.com/avalanche") + ), + relayer: IWormholeRelayer( + 0x27428DD2d3DD32A4D7f7C497eAaa23130d894911 + ), + tokenBridge: ITokenBridge( + 0x0e082F06FF657D94310cB8cE8B0D9a04541d8052 + ), wormhole: IWormhole(0x54a8e5f9c4CbA08F9943965859F6c34eAF03E26c), - circleMessageTransmitter: IMessageTransmitter(0x8186359aF5F57FbB40c6b14A588d2A59C0C29880), - circleTokenMessenger: ITokenMessenger(0x6B25532e1060CE10cc3B0A99e5683b91BFDe6982), + circleMessageTransmitter: IMessageTransmitter( + 0x8186359aF5F57FbB40c6b14A588d2A59C0C29880 + ), + circleTokenMessenger: ITokenMessenger( + 0x6B25532e1060CE10cc3B0A99e5683b91BFDe6982 + ), USDC: IERC20(0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E) }); chainInfosMainnet[10] = ChainInfo({ chainId: 10, name: "fantom", - url: vm.envOr("FANTOM_RPC_URL", string("https://rpc.ankr.com/fantom")), - relayer: IWormholeRelayer(0x27428DD2d3DD32A4D7f7C497eAaa23130d894911), - tokenBridge: ITokenBridge(0x7C9Fc5741288cDFdD83CeB07f3ea7e22618D79D2), + url: vm.envOr( + "FANTOM_RPC_URL", + string("https://rpc.ankr.com/fantom") + ), + relayer: IWormholeRelayer( + 0x27428DD2d3DD32A4D7f7C497eAaa23130d894911 + ), + tokenBridge: ITokenBridge( + 0x7C9Fc5741288cDFdD83CeB07f3ea7e22618D79D2 + ), wormhole: IWormhole(0x126783A6Cb203a3E35344528B26ca3a0489a1485), circleMessageTransmitter: IMessageTransmitter(address(0)), circleTokenMessenger: ITokenMessenger(address(0)), @@ -375,9 +511,16 @@ abstract contract WormholeRelayerTest is Test { chainInfosMainnet[13] = ChainInfo({ chainId: 13, name: "klaytn", - url: vm.envOr("KLAYTN_RPC_URL", string("https://klaytn-mainnet-rpc.allthatnode.com:8551")), - relayer: IWormholeRelayer(0x27428DD2d3DD32A4D7f7C497eAaa23130d894911), - tokenBridge: ITokenBridge(0x5b08ac39EAED75c0439FC750d9FE7E1F9dD0193F), + url: vm.envOr( + "KLAYTN_RPC_URL", + string("https://klaytn-mainnet-rpc.allthatnode.com:8551") + ), + relayer: IWormholeRelayer( + 0x27428DD2d3DD32A4D7f7C497eAaa23130d894911 + ), + tokenBridge: ITokenBridge( + 0x5b08ac39EAED75c0439FC750d9FE7E1F9dD0193F + ), wormhole: IWormhole(0x0C21603c4f3a6387e241c0091A7EA39E43E90bb7), circleMessageTransmitter: IMessageTransmitter(address(0)), circleTokenMessenger: ITokenMessenger(address(0)), @@ -387,8 +530,12 @@ abstract contract WormholeRelayerTest is Test { chainId: 14, name: "celo", url: vm.envOr("CELO_RPC_URL", string("https://forno.celo.org")), - relayer: IWormholeRelayer(0x27428DD2d3DD32A4D7f7C497eAaa23130d894911), - tokenBridge: ITokenBridge(0x796Dff6D74F3E27060B71255Fe517BFb23C93eed), + relayer: IWormholeRelayer( + 0x27428DD2d3DD32A4D7f7C497eAaa23130d894911 + ), + tokenBridge: ITokenBridge( + 0x796Dff6D74F3E27060B71255Fe517BFb23C93eed + ), wormhole: IWormhole(0xa321448d90d4e5b0A732867c18eA198e75CAC48E), circleMessageTransmitter: IMessageTransmitter(address(0)), circleTokenMessenger: ITokenMessenger(address(0)), @@ -397,9 +544,16 @@ abstract contract WormholeRelayerTest is Test { chainInfosMainnet[12] = ChainInfo({ chainId: 12, name: "acala", - url: vm.envOr("ACALA_RPC_URL", string("https://eth-rpc-acala.aca-api.network")), - relayer: IWormholeRelayer(0x27428DD2d3DD32A4D7f7C497eAaa23130d894911), - tokenBridge: ITokenBridge(0xae9d7fe007b3327AA64A32824Aaac52C42a6E624), + url: vm.envOr( + "ACALA_RPC_URL", + string("https://eth-rpc-acala.aca-api.network") + ), + relayer: IWormholeRelayer( + 0x27428DD2d3DD32A4D7f7C497eAaa23130d894911 + ), + tokenBridge: ITokenBridge( + 0xae9d7fe007b3327AA64A32824Aaac52C42a6E624 + ), wormhole: IWormhole(0xa321448d90d4e5b0A732867c18eA198e75CAC48E), circleMessageTransmitter: IMessageTransmitter(address(0)), circleTokenMessenger: ITokenMessenger(address(0)), @@ -408,9 +562,16 @@ abstract contract WormholeRelayerTest is Test { chainInfosMainnet[11] = ChainInfo({ chainId: 11, name: "karura", - url: vm.envOr("KARURA_RPC_URL", string("https://eth-rpc-karura.aca-api.network")), - relayer: IWormholeRelayer(0x27428DD2d3DD32A4D7f7C497eAaa23130d894911), - tokenBridge: ITokenBridge(0xae9d7fe007b3327AA64A32824Aaac52C42a6E624), + url: vm.envOr( + "KARURA_RPC_URL", + string("https://eth-rpc-karura.aca-api.network") + ), + relayer: IWormholeRelayer( + 0x27428DD2d3DD32A4D7f7C497eAaa23130d894911 + ), + tokenBridge: ITokenBridge( + 0xae9d7fe007b3327AA64A32824Aaac52C42a6E624 + ), wormhole: IWormhole(0xa321448d90d4e5b0A732867c18eA198e75CAC48E), circleMessageTransmitter: IMessageTransmitter(address(0)), circleTokenMessenger: ITokenMessenger(address(0)), @@ -419,9 +580,16 @@ abstract contract WormholeRelayerTest is Test { chainInfosMainnet[16] = ChainInfo({ chainId: 16, name: "moombeam", - url: vm.envOr("MOOMBEAM_RPC_URL", string("https://rpc.ankr.com/moonbeam")), - relayer: IWormholeRelayer(0x27428DD2d3DD32A4D7f7C497eAaa23130d894911), - tokenBridge: ITokenBridge(0xB1731c586ca89a23809861c6103F0b96B3F57D92), + url: vm.envOr( + "MOOMBEAM_RPC_URL", + string("https://rpc.ankr.com/moonbeam") + ), + relayer: IWormholeRelayer( + 0x27428DD2d3DD32A4D7f7C497eAaa23130d894911 + ), + tokenBridge: ITokenBridge( + 0xB1731c586ca89a23809861c6103F0b96B3F57D92 + ), wormhole: IWormhole(0xC8e2b0cD52Cf01b0Ce87d389Daa3d414d4cE29f3), circleMessageTransmitter: IMessageTransmitter(address(0)), circleTokenMessenger: ITokenMessenger(address(0)), @@ -430,31 +598,57 @@ abstract contract WormholeRelayerTest is Test { chainInfosMainnet[23] = ChainInfo({ chainId: 23, name: "arbitrum", - url: vm.envOr("ARBITRUM_RPC_URL", string("https://rpc.ankr.com/arbitrum")), - relayer: IWormholeRelayer(0x27428DD2d3DD32A4D7f7C497eAaa23130d894911), - tokenBridge: ITokenBridge(0x0b2402144Bb366A632D14B83F244D2e0e21bD39c), + url: vm.envOr( + "ARBITRUM_RPC_URL", + string("https://rpc.ankr.com/arbitrum") + ), + relayer: IWormholeRelayer( + 0x27428DD2d3DD32A4D7f7C497eAaa23130d894911 + ), + tokenBridge: ITokenBridge( + 0x0b2402144Bb366A632D14B83F244D2e0e21bD39c + ), wormhole: IWormhole(0xa5f208e072434bC67592E4C49C1B991BA79BCA46), - circleMessageTransmitter: IMessageTransmitter(0xC30362313FBBA5cf9163F0bb16a0e01f01A896ca), - circleTokenMessenger: ITokenMessenger(0x19330d10D9Cc8751218eaf51E8885D058642E08A), + circleMessageTransmitter: IMessageTransmitter( + 0xC30362313FBBA5cf9163F0bb16a0e01f01A896ca + ), + circleTokenMessenger: ITokenMessenger( + 0x19330d10D9Cc8751218eaf51E8885D058642E08A + ), USDC: IERC20(0xaf88d065e77c8cC2239327C5EDb3A432268e5831) }); chainInfosMainnet[24] = ChainInfo({ chainId: 24, name: "optimism", - url: vm.envOr("OPTIMISM_RPC_URL", string("https://rpc.ankr.com/optimism")), - relayer: IWormholeRelayer(0x27428DD2d3DD32A4D7f7C497eAaa23130d894911), - tokenBridge: ITokenBridge(0x1D68124e65faFC907325e3EDbF8c4d84499DAa8b), + url: vm.envOr( + "OPTIMISM_RPC_URL", + string("https://rpc.ankr.com/optimism") + ), + relayer: IWormholeRelayer( + 0x27428DD2d3DD32A4D7f7C497eAaa23130d894911 + ), + tokenBridge: ITokenBridge( + 0x1D68124e65faFC907325e3EDbF8c4d84499DAa8b + ), wormhole: IWormhole(0xEe91C335eab126dF5fDB3797EA9d6aD93aeC9722), - circleMessageTransmitter: IMessageTransmitter(0x4D41f22c5a0e5c74090899E5a8Fb597a8842b3e8), - circleTokenMessenger: ITokenMessenger(0x2B4069517957735bE00ceE0fadAE88a26365528f), + circleMessageTransmitter: IMessageTransmitter( + 0x4D41f22c5a0e5c74090899E5a8Fb597a8842b3e8 + ), + circleTokenMessenger: ITokenMessenger( + 0x2B4069517957735bE00ceE0fadAE88a26365528f + ), USDC: IERC20(0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85) }); chainInfosMainnet[30] = ChainInfo({ chainId: 30, name: "base", url: vm.envOr("BASE_RPC_URL", string("https://mainnet.base.org")), - relayer: IWormholeRelayer(0x706f82e9bb5b0813501714ab5974216704980e31), - tokenBridge: ITokenBridge(0x8d2de8d2f73F1F4cAB472AC9A881C9b123C79627), + relayer: IWormholeRelayer( + 0x706F82e9bb5b0813501714Ab5974216704980e31 + ), + tokenBridge: ITokenBridge( + 0x8d2de8d2f73F1F4cAB472AC9A881C9b123C79627 + ), wormhole: IWormhole(0xbebdb6C8ddC678FfA9f8748f85C815C556Dd8ac6), circleMessageTransmitter: IMessageTransmitter(address(0)), circleTokenMessenger: ITokenMessenger(address(0)), @@ -482,7 +676,7 @@ abstract contract WormholeRelayerBasicTest is WormholeRelayerTest { */ function setUpOther(ActiveFork memory fork) public virtual {} - /* + /* * aliases for activeForks */ @@ -509,9 +703,8 @@ abstract contract WormholeRelayerBasicTest is WormholeRelayerTest { CircleMessageTransmitterSimulator public circleAttesterSource; CircleMessageTransmitterSimulator public circleAttesterTarget; - - /* - * end activeForks aliases + /* + * end activeForks aliases */ constructor() WormholeRelayerTest() { @@ -557,26 +750,35 @@ abstract contract WormholeRelayerBasicTest is WormholeRelayerTest { wormholeTarget = targetChainInfo.wormhole; } - function setTestnetForkChains(uint16 _sourceChain, uint16 _targetChain) public { + function setTestnetForkChains( + uint16 _sourceChain, + uint16 _targetChain + ) public { ChainInfo[] memory forks = new ChainInfo[](2); forks[0] = chainInfosTestnet[_sourceChain]; forks[1] = chainInfosTestnet[_targetChain]; setActiveForks(forks); } - function setMainnetForkChains(uint16 _sourceChain, uint16 _targetChain) public { + function setMainnetForkChains( + uint16 _sourceChain, + uint16 _targetChain + ) public { ChainInfo[] memory forks = new ChainInfo[](2); forks[0] = chainInfosMainnet[_sourceChain]; forks[1] = chainInfosMainnet[_targetChain]; setActiveForks(forks); } - function setForkChains(bool testnet, uint16 _sourceChain, uint16 _targetChain) public { - if (testnet) { - setTestnetForkChains(_sourceChain, _targetChain); - return; - } - setMainnetForkChains(_sourceChain, _targetChain); + function setForkChains( + bool testnet, + uint16 _sourceChain, + uint16 _targetChain + ) public { + if (testnet) { + setTestnetForkChains(_sourceChain, _targetChain); + return; + } + setMainnetForkChains(_sourceChain, _targetChain); } - } From fb8b1f15b287cae2e69ad238df3da1986273c124 Mon Sep 17 00:00:00 2001 From: derpy-duck <115193320+derpy-duck@users.noreply.github.com> Date: Wed, 4 Oct 2023 12:29:06 -0400 Subject: [PATCH 17/23] comment --- src/CCTPBase.sol | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/CCTPBase.sol b/src/CCTPBase.sol index 936b7f7..2e1e05e 100644 --- a/src/CCTPBase.sol +++ b/src/CCTPBase.sol @@ -14,6 +14,11 @@ library CCTPMessageLib { // The second standardized key type is a CCTP Key // representing a CCTP transfer of USDC // (on the IWormholeRelayer interface) + + // Note - the default delivery provider only will relay CCTP transfers that were sent + // in the same transaction that this message was emitted! + // (This will always be the case if 'CCTPSender' is used) + uint8 constant CCTP_KEY_TYPE = 2; // encoded using abi.encodePacked(domain, nonce) From 012aea46dd9ebaf40ac260ed20afa92900471c8c Mon Sep 17 00:00:00 2001 From: derpy-duck <115193320+derpy-duck@users.noreply.github.com> Date: Fri, 27 Oct 2023 18:03:04 -0400 Subject: [PATCH 18/23] cctp configuration --- src/CCTPBase.sol | 82 ++++++++++++++++---------- src/TokenBase.sol | 137 ++++++++++++++++++++++++++++++++------------ test/CCTPBase.t.sol | 17 ++++-- 3 files changed, 163 insertions(+), 73 deletions(-) diff --git a/src/CCTPBase.sol b/src/CCTPBase.sol index 2e1e05e..b40e6d6 100644 --- a/src/CCTPBase.sol +++ b/src/CCTPBase.sol @@ -38,6 +38,7 @@ abstract contract CCTPBase is TokenBase { ITokenMessenger immutable circleTokenMessenger; IMessageTransmitter immutable circleMessageTransmitter; address immutable USDC; + address cctpConfigurationOwner; constructor( address _wormholeRelayer, @@ -52,32 +53,7 @@ abstract contract CCTPBase is TokenBase { _circleMessageTransmitter ); USDC = _USDC; - } - - function getCCTPDomain(uint16 chain) internal pure returns (uint32) { - if (chain == 2) { - return 0; - } else if (chain == 6) { - return 1; - } else if (chain == 23) { - return 3; - } else if (chain == 24) { - return 2; - } else { - revert("Wrong CCTP Domain"); - } - } - - function redeemUSDC( - bytes memory cctpMessage - ) internal returns (uint256 amount) { - (bytes memory message, bytes memory signature) = abi.decode( - cctpMessage, - (bytes, bytes) - ); - uint256 beforeBalance = IERC20(USDC).balanceOf(address(this)); - circleMessageTransmitter.receiveMessage(message, signature); - return IERC20(USDC).balanceOf(address(this)) - beforeBalance; + cctpConfigurationOwner = msg.sender; } } @@ -86,11 +62,45 @@ abstract contract CCTPSender is CCTPBase { using CCTPMessageLib for *; + mapping(uint16 => uint32) public chainIdToCCTPDomain; + /** - * transferTokens wraps common boilerplate for sending tokens to another chain using IWormholeRelayer - * - approves tokenBridge to spend 'amount' of 'token' - * - emits token transfer VAA - * - returns VAA key for inclusion in WormholeRelayer `additionalVaas` argument + * Sets the CCTP Domain corresponding to chain 'chain' to be 'cctpDomain' + * So that transfers of USDC to chain 'chain' use the target CCTP domain 'cctpDomain' + * + * This action can only be performed by 'cctpConfigurationOwner', who is set to be the deployer + * + * Currently, cctp domains are: + * Ethereum: Wormhole chain id 2, cctp domain 0 + * Avalanche: Wormhole chain id 6, cctp domain 1 + * Optimism: Wormhole chain id 24, cctp domain 2 + * Arbitrum: Wormhole chain id 23, cctp domain 3 + * Base: Wormhole chain id 30, cctp domain 6 + * + * These can be set via: + * setCCTPDomain(2, 0); + * setCCTPDomain(6, 1); + * setCCTPDomain(24, 2); + * setCCTPDomain(23, 3); + * setCCTPDomain(30, 6); + */ + function setCCTPDomain(uint16 chain, uint32 cctpDomain) public { + require( + msg.sender == cctpConfigurationOwner, + "Not allowed to set CCTP Domain" + ); + chainIdToCCTPDomain[chain] = cctpDomain; + } + + function getCCTPDomain(uint16 chain) internal view returns (uint32) { + return chainIdToCCTPDomain[chain]; + } + + /** + * transferUSDC wraps common boilerplate for sending tokens to another chain using IWormholeRelayer + * - approves the Circle TokenMessenger contract to spend 'amount' of USDC + * - calls Circle's 'depositForBurnWithCaller' + * - returns key for inclusion in WormholeRelayer `additionalVaas` argument * * Note: this requires that only the targetAddress can redeem transfers. * @@ -159,6 +169,18 @@ abstract contract CCTPSender is CCTPBase { } abstract contract CCTPReceiver is CCTPBase { + function redeemUSDC( + bytes memory cctpMessage + ) internal returns (uint256 amount) { + (bytes memory message, bytes memory signature) = abi.decode( + cctpMessage, + (bytes, bytes) + ); + uint256 beforeBalance = IERC20(USDC).balanceOf(address(this)); + circleMessageTransmitter.receiveMessage(message, signature); + return IERC20(USDC).balanceOf(address(this)) - beforeBalance; + } + function receiveWormholeMessages( bytes memory payload, bytes[] memory additionalMessages, diff --git a/src/TokenBase.sol b/src/TokenBase.sol index 0ebdafb..80db85c 100644 --- a/src/TokenBase.sol +++ b/src/TokenBase.sol @@ -1,4 +1,3 @@ - pragma solidity ^0.8.13; import "./interfaces/IWormholeReceiver.sol"; @@ -12,24 +11,32 @@ import "./Utils.sol"; abstract contract TokenBase is Base { ITokenBridge public immutable tokenBridge; - constructor(address _wormholeRelayer, address _tokenBridge, address _wormhole) Base(_wormholeRelayer, _wormhole) { + constructor( + address _wormholeRelayer, + address _tokenBridge, + address _wormhole + ) Base(_wormholeRelayer, _wormhole) { tokenBridge = ITokenBridge(_tokenBridge); } - function getDecimals(address tokenAddress) internal view returns (uint8 decimals) { + function getDecimals( + address tokenAddress + ) internal view returns (uint8 decimals) { // query decimals - (, bytes memory queriedDecimals) = address(tokenAddress).staticcall(abi.encodeWithSignature("decimals()")); + (, bytes memory queriedDecimals) = address(tokenAddress).staticcall( + abi.encodeWithSignature("decimals()") + ); decimals = abi.decode(queriedDecimals, (uint8)); } - function getTokenAddressOnThisChain(uint16 tokenHomeChain, bytes32 tokenHomeAddress) - internal - view - returns (address tokenAddressOnThisChain) - { - return tokenHomeChain == wormhole.chainId() - ? fromWormholeFormat(tokenHomeAddress) - : tokenBridge.wrappedAsset(tokenHomeChain, tokenHomeAddress); + function getTokenAddressOnThisChain( + uint16 tokenHomeChain, + bytes32 tokenHomeAddress + ) internal view returns (address tokenAddressOnThisChain) { + return + tokenHomeChain == wormhole.chainId() + ? fromWormholeFormat(tokenHomeAddress) + : tokenBridge.wrappedAsset(tokenHomeChain, tokenHomeAddress); } } @@ -45,11 +52,20 @@ abstract contract TokenSender is TokenBase { * the offchain relayer and the target contract would have to be hardened against this. * */ - function transferTokens(address token, uint256 amount, uint16 targetChain, address targetAddress) - internal - returns (VaaKey memory) - { - return transferTokens(token, amount, targetChain, targetAddress, bytes("")); + function transferTokens( + address token, + uint256 amount, + uint16 targetChain, + address targetAddress + ) internal returns (VaaKey memory) { + return + transferTokens( + token, + amount, + targetChain, + targetAddress, + bytes("") + ); } /** @@ -73,14 +89,22 @@ abstract contract TokenSender is TokenBase { bytes memory payload ) internal returns (VaaKey memory) { IERC20(token).approve(address(tokenBridge), amount); - uint64 sequence = tokenBridge.transferTokensWithPayload{value: wormhole.messageFee()}( - token, amount, targetChain, toWormholeFormat(targetAddress), 0, payload + uint64 sequence = tokenBridge.transferTokensWithPayload{ + value: wormhole.messageFee() + }( + token, + amount, + targetChain, + toWormholeFormat(targetAddress), + 0, + payload ); - return VaaKey({ - emitterAddress: toWormholeFormat(address(tokenBridge)), - chainId: wormhole.chainId(), - sequence: sequence - }); + return + VaaKey({ + emitterAddress: toWormholeFormat(address(tokenBridge)), + chainId: wormhole.chainId(), + sequence: sequence + }); } function sendTokenWithPayloadToEvm( @@ -95,10 +119,20 @@ abstract contract TokenSender is TokenBase { VaaKey[] memory vaaKeys = new VaaKey[](1); vaaKeys[0] = transferTokens(token, amount, targetChain, targetAddress); - (uint256 cost,) = wormholeRelayer.quoteEVMDeliveryPrice(targetChain, receiverValue, gasLimit); - return wormholeRelayer.sendVaasToEvm{value: cost}( - targetChain, targetAddress, payload, receiverValue, gasLimit, vaaKeys + (uint256 cost, ) = wormholeRelayer.quoteEVMDeliveryPrice( + targetChain, + receiverValue, + gasLimit ); + return + wormholeRelayer.sendVaasToEvm{value: cost}( + targetChain, + targetAddress, + payload, + receiverValue, + gasLimit, + vaaKeys + ); } function sendTokenWithPayloadToEvm( @@ -115,12 +149,23 @@ abstract contract TokenSender is TokenBase { VaaKey[] memory vaaKeys = new VaaKey[](1); vaaKeys[0] = transferTokens(token, amount, targetChain, targetAddress); - (uint256 cost,) = wormholeRelayer.quoteEVMDeliveryPrice(targetChain, receiverValue, gasLimit); - return wormholeRelayer.sendVaasToEvm{value: cost}( - targetChain, targetAddress, payload, receiverValue, gasLimit, vaaKeys, refundChain, refundAddress + (uint256 cost, ) = wormholeRelayer.quoteEVMDeliveryPrice( + targetChain, + receiverValue, + gasLimit ); + return + wormholeRelayer.sendVaasToEvm{value: cost}( + targetChain, + targetAddress, + payload, + receiverValue, + gasLimit, + vaaKeys, + refundChain, + refundAddress + ); } - } abstract contract TokenReceiver is TokenBase { @@ -139,25 +184,35 @@ abstract contract TokenReceiver is TokenBase { uint16 sourceChain, bytes32 deliveryHash ) external payable { - TokenReceived[] memory receivedTokens = new TokenReceived[](additionalVaas.length); + TokenReceived[] memory receivedTokens = new TokenReceived[]( + additionalVaas.length + ); for (uint256 i = 0; i < additionalVaas.length; ++i) { IWormhole.VM memory parsed = wormhole.parseVM(additionalVaas[i]); require( - parsed.emitterAddress == tokenBridge.bridgeContracts(parsed.emitterChainId), "Not a Token Bridge VAA" + parsed.emitterAddress == + tokenBridge.bridgeContracts(parsed.emitterChainId), + "Not a Token Bridge VAA" ); - ITokenBridge.TransferWithPayload memory transfer = tokenBridge.parseTransferWithPayload(parsed.payload); + ITokenBridge.TransferWithPayload memory transfer = tokenBridge + .parseTransferWithPayload(parsed.payload); require( - transfer.to == toWormholeFormat(address(this)) && transfer.toChain == wormhole.chainId(), + transfer.to == toWormholeFormat(address(this)) && + transfer.toChain == wormhole.chainId(), "Token was not sent to this address" ); tokenBridge.completeTransferWithPayload(additionalVaas[i]); - address thisChainTokenAddress = getTokenAddressOnThisChain(transfer.tokenChain, transfer.tokenAddress); + address thisChainTokenAddress = getTokenAddressOnThisChain( + transfer.tokenChain, + transfer.tokenAddress + ); uint8 decimals = getDecimals(thisChainTokenAddress); uint256 denormalizedAmount = transfer.amount; - if (decimals > 8) denormalizedAmount *= uint256(10) ** (decimals - 8); + if (decimals > 8) + denormalizedAmount *= uint256(10) ** (decimals - 8); receivedTokens[i] = TokenReceived({ tokenHomeAddress: transfer.tokenAddress, @@ -169,7 +224,13 @@ abstract contract TokenReceiver is TokenBase { } // call into overriden method - receivePayloadAndTokens(payload, receivedTokens, sourceAddress, sourceChain, deliveryHash); + receivePayloadAndTokens( + payload, + receivedTokens, + sourceAddress, + sourceChain, + deliveryHash + ); } function receivePayloadAndTokens( diff --git a/test/CCTPBase.t.sol b/test/CCTPBase.t.sol index cae1ace..0c819cf 100644 --- a/test/CCTPBase.t.sol +++ b/test/CCTPBase.t.sol @@ -1,4 +1,3 @@ - pragma solidity ^0.8.13; import "../src/WormholeRelayerSDK.sol"; @@ -30,7 +29,10 @@ contract CCTPToy is CCTPSender, CCTPReceiver { _circleTokenMessenger, _USDC ) - {} + { + setCCTPDomain(2, 0); + setCCTPDomain(6, 1); + } function quoteCrossChainDeposit( uint16 targetChain @@ -126,10 +128,16 @@ contract WormholeSDKTest is WormholeRelayerBasicTest { function setUpGeneral() public override { vm.selectFork(sourceFork); - CCTPToySource.setRegisteredSender(targetChain, toWormholeFormat(address(CCTPToyTarget))); + CCTPToySource.setRegisteredSender( + targetChain, + toWormholeFormat(address(CCTPToyTarget)) + ); vm.selectFork(targetFork); - CCTPToyTarget.setRegisteredSender(sourceChain, toWormholeFormat(address(CCTPToySource))); + CCTPToyTarget.setRegisteredSender( + sourceChain, + toWormholeFormat(address(CCTPToySource)) + ); } function testSendToken() public { @@ -155,5 +163,4 @@ contract WormholeSDKTest is WormholeRelayerBasicTest { vm.selectFork(targetFork); assertEq(IERC20(USDCTarget).balanceOf(recipient), amount); } - } From 7e66617158bc9e1b18232f49630de24d812201e7 Mon Sep 17 00:00:00 2001 From: derpy-duck <115193320+derpy-duck@users.noreply.github.com> Date: Mon, 30 Oct 2023 11:47:22 -0400 Subject: [PATCH 19/23] CCTPAndTokenBase --- src/CCTPAndTokenBase.sol | 456 ++++++++++++++++++++++++++++++ src/CCTPBase.sol | 20 +- src/TokenBase.sol | 47 +-- src/WormholeRelayerSDK.sol | 2 +- test/CCTPAndTokenBridgeBase.t.sol | 247 ++++++++++++++++ test/CCTPBase.t.sol | 4 - 6 files changed, 746 insertions(+), 30 deletions(-) create mode 100644 src/CCTPAndTokenBase.sol create mode 100644 test/CCTPAndTokenBridgeBase.t.sol diff --git a/src/CCTPAndTokenBase.sol b/src/CCTPAndTokenBase.sol new file mode 100644 index 0000000..9b51f76 --- /dev/null +++ b/src/CCTPAndTokenBase.sol @@ -0,0 +1,456 @@ +pragma solidity ^0.8.13; + +import "./interfaces/IWormholeReceiver.sol"; +import "./interfaces/IWormholeRelayer.sol"; +import "./interfaces/ITokenBridge.sol"; +import {IERC20} from "./interfaces/IERC20.sol"; +import "./interfaces/CCTPInterfaces/ITokenMessenger.sol"; +import "./interfaces/CCTPInterfaces/IMessageTransmitter.sol"; + +import "./Utils.sol"; +import "./TokenBase.sol"; +import "./CCTPBase.sol"; + +abstract contract CCTPAndTokenBase is CCTPBase { + ITokenBridge public immutable tokenBridge; + + enum Transfer { + TOKEN_BRIDGE, + CCTP + } + + constructor( + address _wormholeRelayer, + address _tokenBridge, + address _wormhole, + address _circleMessageTransmitter, + address _circleTokenMessenger, + address _USDC + ) + CCTPBase( + _wormholeRelayer, + _wormhole, + _circleMessageTransmitter, + _circleTokenMessenger, + _USDC + ) + { + tokenBridge = ITokenBridge(_tokenBridge); + } +} + +abstract contract CCTPAndTokenSender is CCTPAndTokenBase { + // CCTP Sender functions, taken from "./CCTPBase.sol" + + uint8 internal constant CONSISTENCY_LEVEL_FINALIZED = 15; + + using CCTPMessageLib for *; + + mapping(uint16 => uint32) public chainIdToCCTPDomain; + + /** + * Sets the CCTP Domain corresponding to chain 'chain' to be 'cctpDomain' + * So that transfers of USDC to chain 'chain' use the target CCTP domain 'cctpDomain' + * + * This action can only be performed by 'cctpConfigurationOwner', who is set to be the deployer + * + * Currently, cctp domains are: + * Ethereum: Wormhole chain id 2, cctp domain 0 + * Avalanche: Wormhole chain id 6, cctp domain 1 + * Optimism: Wormhole chain id 24, cctp domain 2 + * Arbitrum: Wormhole chain id 23, cctp domain 3 + * Base: Wormhole chain id 30, cctp domain 6 + * + * These can be set via: + * setCCTPDomain(2, 0); + * setCCTPDomain(6, 1); + * setCCTPDomain(24, 2); + * setCCTPDomain(23, 3); + * setCCTPDomain(30, 6); + */ + function setCCTPDomain(uint16 chain, uint32 cctpDomain) public { + require( + msg.sender == cctpConfigurationOwner, + "Not allowed to set CCTP Domain" + ); + chainIdToCCTPDomain[chain] = cctpDomain; + } + + function getCCTPDomain(uint16 chain) internal view returns (uint32) { + return chainIdToCCTPDomain[chain]; + } + + /** + * transferUSDC wraps common boilerplate for sending tokens to another chain using IWormholeRelayer + * - approves the Circle TokenMessenger contract to spend 'amount' of USDC + * - calls Circle's 'depositForBurnWithCaller' + * - returns key for inclusion in WormholeRelayer `additionalVaas` argument + * + * Note: this requires that only the targetAddress can redeem transfers. + * + */ + + function transferUSDC( + uint256 amount, + uint16 targetChain, + address targetAddress + ) internal returns (MessageKey memory) { + IERC20(USDC).approve(address(circleTokenMessenger), amount); + bytes32 targetAddressBytes32 = addressToBytes32CCTP(targetAddress); + uint64 nonce = circleTokenMessenger.depositForBurnWithCaller( + amount, + getCCTPDomain(targetChain), + targetAddressBytes32, + USDC, + targetAddressBytes32 + ); + return + MessageKey( + CCTPMessageLib.CCTP_KEY_TYPE, + abi.encodePacked(getCCTPDomain(wormhole.chainId()), nonce) + ); + } + + // Publishes a CCTP transfer of 'amount' of USDC + // and requests a delivery of the transfer along with 'payload' to 'targetAddress' on 'targetChain' + // + // The second step is done by publishing a wormhole message representing a request + // to call 'receiveWormholeMessages' on the address 'targetAddress' on chain 'targetChain' + // with the payload 'abi.encode(Transfer.CCTP, amount, payload)' + // (we encode a Transfer enum to distinguish this from a TokenBridge transfer) + // (and we encode the amount so it can be checked on the target chain) + function sendUSDCWithPayloadToEvm( + uint16 targetChain, + address targetAddress, + bytes memory payload, + uint256 receiverValue, + uint256 gasLimit, + uint256 amount + ) internal returns (uint64 sequence) { + MessageKey[] memory messageKeys = new MessageKey[](1); + messageKeys[0] = transferUSDC(amount, targetChain, targetAddress); + + bytes memory userPayload = abi.encode(Transfer.CCTP, amount, payload); + address defaultDeliveryProvider = wormholeRelayer + .getDefaultDeliveryProvider(); + + (uint256 cost, ) = wormholeRelayer.quoteEVMDeliveryPrice( + targetChain, + receiverValue, + gasLimit + ); + + sequence = wormholeRelayer.sendToEvm{value: cost}( + targetChain, + targetAddress, + userPayload, + receiverValue, + 0, + gasLimit, + targetChain, + address(0x0), + defaultDeliveryProvider, + messageKeys, + CONSISTENCY_LEVEL_FINALIZED + ); + } + + function addressToBytes32CCTP(address addr) private pure returns (bytes32) { + return bytes32(uint256(uint160(addr))); + } + + // TokenBridge Sender functions, taken from "./TokenBase.sol" + + /** + * transferTokens wraps common boilerplate for sending tokens to another chain using IWormholeRelayer + * - approves tokenBridge to spend 'amount' of 'token' + * - emits token transfer VAA + * - returns VAA key for inclusion in WormholeRelayer `additionalVaas` argument + * + * Note: this function uses transferTokensWithPayload instead of transferTokens since the former requires that only the targetAddress + * can redeem transfers. Otherwise it's possible for another address to redeem the transfer before the targetContract is invoked by + * the offchain relayer and the target contract would have to be hardened against this. + * + */ + function transferTokens( + address token, + uint256 amount, + uint16 targetChain, + address targetAddress + ) internal returns (VaaKey memory) { + return + transferTokens( + token, + amount, + targetChain, + targetAddress, + bytes("") + ); + } + + /** + * transferTokens wraps common boilerplate for sending tokens to another chain using IWormholeRelayer. + * A payload can be included in the transfer vaa. By including a payload here instead of the deliveryVaa, + * fewer trust assumptions are placed on the WormholeRelayer contract. + * + * - approves tokenBridge to spend 'amount' of 'token' + * - emits token transfer VAA + * - returns VAA key for inclusion in WormholeRelayer `additionalVaas` argument + * + * Note: this function uses transferTokensWithPayload instead of transferTokens since the former requires that only the targetAddress + * can redeem transfers. Otherwise it's possible for another address to redeem the transfer before the targetContract is invoked by + * the offchain relayer and the target contract would have to be hardened against this. + */ + function transferTokens( + address token, + uint256 amount, + uint16 targetChain, + address targetAddress, + bytes memory payload + ) internal returns (VaaKey memory) { + IERC20(token).approve(address(tokenBridge), amount); + uint64 sequence = tokenBridge.transferTokensWithPayload{ + value: wormhole.messageFee() + }( + token, + amount, + targetChain, + toWormholeFormat(targetAddress), + 0, + payload + ); + return + VaaKey({ + emitterAddress: toWormholeFormat(address(tokenBridge)), + chainId: wormhole.chainId(), + sequence: sequence + }); + } + + // Publishes a wormhole message representing a 'TokenBridge' transfer of 'amount' of 'token' + // and requests a delivery of the transfer along with 'payload' to 'targetAddress' on 'targetChain' + // + // The second step is done by publishing a wormhole message representing a request + // to call 'receiveWormholeMessages' on the address 'targetAddress' on chain 'targetChain' + // with the payload 'abi.encode(Transfer.TOKEN_BRIDGE, payload)' + // (we encode a Transfer enum to distinguish this from a CCTP transfer) + function sendTokenWithPayloadToEvm( + uint16 targetChain, + address targetAddress, + bytes memory payload, + uint256 receiverValue, + uint256 gasLimit, + address token, + uint256 amount + ) internal returns (uint64) { + VaaKey[] memory vaaKeys = new VaaKey[](1); + vaaKeys[0] = transferTokens(token, amount, targetChain, targetAddress); + + (uint256 cost, ) = wormholeRelayer.quoteEVMDeliveryPrice( + targetChain, + receiverValue, + gasLimit + ); + return + wormholeRelayer.sendVaasToEvm{value: cost}( + targetChain, + targetAddress, + abi.encode(Transfer.TOKEN_BRIDGE, payload), + receiverValue, + gasLimit, + vaaKeys + ); + } + + function sendTokenWithPayloadToEvm( + uint16 targetChain, + address targetAddress, + bytes memory payload, + uint256 receiverValue, + uint256 gasLimit, + address token, + uint256 amount, + uint16 refundChain, + address refundAddress + ) internal returns (uint64) { + VaaKey[] memory vaaKeys = new VaaKey[](1); + vaaKeys[0] = transferTokens(token, amount, targetChain, targetAddress); + + (uint256 cost, ) = wormholeRelayer.quoteEVMDeliveryPrice( + targetChain, + receiverValue, + gasLimit + ); + return + wormholeRelayer.sendVaasToEvm{value: cost}( + targetChain, + targetAddress, + abi.encode(Transfer.TOKEN_BRIDGE, payload), + receiverValue, + gasLimit, + vaaKeys, + refundChain, + refundAddress + ); + } +} + +abstract contract CCTPAndTokenReceiver is CCTPAndTokenBase { + function redeemUSDC( + bytes memory cctpMessage + ) internal returns (uint256 amount) { + (bytes memory message, bytes memory signature) = abi.decode( + cctpMessage, + (bytes, bytes) + ); + uint256 beforeBalance = IERC20(USDC).balanceOf(address(this)); + circleMessageTransmitter.receiveMessage(message, signature); + return IERC20(USDC).balanceOf(address(this)) - beforeBalance; + } + + struct TokenReceived { + bytes32 tokenHomeAddress; + uint16 tokenHomeChain; + address tokenAddress; // wrapped address if tokenHomeChain !== this chain, else tokenHomeAddress (in evm address format) + uint256 amount; + uint256 amountNormalized; // if decimals > 8, normalized to 8 decimal places + } + + function getDecimals( + address tokenAddress + ) internal view returns (uint8 decimals) { + // query decimals + (, bytes memory queriedDecimals) = address(tokenAddress).staticcall( + abi.encodeWithSignature("decimals()") + ); + decimals = abi.decode(queriedDecimals, (uint8)); + } + + function getTokenAddressOnThisChain( + uint16 tokenHomeChain, + bytes32 tokenHomeAddress + ) internal view returns (address tokenAddressOnThisChain) { + return + tokenHomeChain == wormhole.chainId() + ? fromWormholeFormat(tokenHomeAddress) + : tokenBridge.wrappedAsset(tokenHomeChain, tokenHomeAddress); + } + + function receiveWormholeMessages( + bytes memory payload, + bytes[] memory additionalMessages, + bytes32 sourceAddress, + uint16 sourceChain, + bytes32 deliveryHash + ) external payable { + Transfer transferType = abi.decode(payload, (Transfer)); + if (transferType == Transfer.TOKEN_BRIDGE) { + TokenReceived[] memory receivedTokens = new TokenReceived[]( + additionalMessages.length + ); + + for (uint256 i = 0; i < additionalMessages.length; ++i) { + IWormhole.VM memory parsed = wormhole.parseVM( + additionalMessages[i] + ); + require( + parsed.emitterAddress == + tokenBridge.bridgeContracts(parsed.emitterChainId), + "Not a Token Bridge VAA" + ); + ITokenBridge.TransferWithPayload memory transfer = tokenBridge + .parseTransferWithPayload(parsed.payload); + require( + transfer.to == toWormholeFormat(address(this)) && + transfer.toChain == wormhole.chainId(), + "Token was not sent to this address" + ); + + tokenBridge.completeTransferWithPayload(additionalMessages[i]); + + address thisChainTokenAddress = getTokenAddressOnThisChain( + transfer.tokenChain, + transfer.tokenAddress + ); + uint8 decimals = getDecimals(thisChainTokenAddress); + uint256 denormalizedAmount = transfer.amount; + if (decimals > 8) + denormalizedAmount *= uint256(10) ** (decimals - 8); + + receivedTokens[i] = TokenReceived({ + tokenHomeAddress: transfer.tokenAddress, + tokenHomeChain: transfer.tokenChain, + tokenAddress: thisChainTokenAddress, + amount: denormalizedAmount, + amountNormalized: transfer.amount + }); + } + + (, bytes memory userPayload) = abi.decode( + payload, + (Transfer, bytes) + ); + + // call into overriden method + receivePayloadAndTokens( + userPayload, + receivedTokens, + sourceAddress, + sourceChain, + deliveryHash + ); + } else if (transferType == Transfer.CCTP) { + // Currently, 'sendUSDCWithPayloadToEVM' only sends one CCTP transfer + // That can be modified if the integrator desires to send multiple CCTP transfers + // in which case the following code would have to be modified to support + // redeeming these multiple transfers and checking that their 'amount's are accurate + require( + additionalMessages.length <= 1, + "CCTP: At most one Message is supported" + ); + + uint256 amountUSDCReceived; + if (additionalMessages.length == 1) { + amountUSDCReceived = redeemUSDC(additionalMessages[0]); + } + + (, uint256 amount, bytes memory userPayload) = abi.decode( + payload, + (Transfer, uint256, bytes) + ); + + // Check that the correct amount was received + // It is important to verify that the 'USDC' sent in by the relayer is the same amount + // that the sender sent in on the source chain + require(amount == amountUSDCReceived, "Wrong amount received"); + + receivePayloadAndUSDC( + userPayload, + amountUSDCReceived, + sourceAddress, + sourceChain, + deliveryHash + ); + } else { + revert("Invalid transfer type"); + } + } + + // Implement this function to handle in-bound deliveries that include a CCTP transfer + function receivePayloadAndUSDC( + bytes memory payload, + uint256 amountUSDCReceived, + bytes32 sourceAddress, + uint16 sourceChain, + bytes32 deliveryHash + ) internal virtual {} + + // Implement this function to handle in-bound deliveries that include a TokenBridge transfer + function receivePayloadAndTokens( + bytes memory payload, + TokenReceived[] memory receivedTokens, + bytes32 sourceAddress, + uint16 sourceChain, + bytes32 deliveryHash + ) internal virtual {} +} diff --git a/src/CCTPBase.sol b/src/CCTPBase.sol index b40e6d6..19f8b9a 100644 --- a/src/CCTPBase.sol +++ b/src/CCTPBase.sol @@ -2,13 +2,12 @@ pragma solidity ^0.8.13; import "./interfaces/IWormholeReceiver.sol"; import "./interfaces/IWormholeRelayer.sol"; -import "./interfaces/ITokenBridge.sol"; import {IERC20} from "./interfaces/IERC20.sol"; import "./interfaces/CCTPInterfaces/ITokenMessenger.sol"; import "./interfaces/CCTPInterfaces/IMessageTransmitter.sol"; import "./Utils.sol"; -import "./TokenBase.sol"; +import "./Base.sol"; library CCTPMessageLib { // The second standardized key type is a CCTP Key @@ -34,7 +33,7 @@ library CCTPMessageLib { } } -abstract contract CCTPBase is TokenBase { +abstract contract CCTPBase is Base { ITokenMessenger immutable circleTokenMessenger; IMessageTransmitter immutable circleMessageTransmitter; address immutable USDC; @@ -42,12 +41,11 @@ abstract contract CCTPBase is TokenBase { constructor( address _wormholeRelayer, - address _tokenBridge, address _wormhole, address _circleMessageTransmitter, address _circleTokenMessenger, address _USDC - ) TokenBase(_wormholeRelayer, _tokenBridge, _wormhole) { + ) Base(_wormholeRelayer, _wormhole) { circleTokenMessenger = ITokenMessenger(_circleTokenMessenger); circleMessageTransmitter = IMessageTransmitter( _circleMessageTransmitter @@ -127,6 +125,13 @@ abstract contract CCTPSender is CCTPBase { ); } + // Publishes a CCTP transfer of 'amount' of USDC + // and requests a delivery of the transfer along with 'payload' to 'targetAddress' on 'targetChain' + // + // The second step is done by publishing a wormhole message representing a request + // to call 'receiveWormholeMessages' on the address 'targetAddress' on chain 'targetChain' + // with the payload 'abi.encode(amount, payload)' + // (and we encode the amount so it can be checked on the target chain) function sendUSDCWithPayloadToEvm( uint16 targetChain, address targetAddress, @@ -188,6 +193,10 @@ abstract contract CCTPReceiver is CCTPBase { uint16 sourceChain, bytes32 deliveryHash ) external payable { + // Currently, 'sendUSDCWithPayloadToEVM' only sends one CCTP transfer + // That can be modified if the integrator desires to send multiple CCTP transfers + // in which case the following code would have to be modified to support + // redeeming these multiple transfers and checking that their 'amount's are accurate require( additionalMessages.length <= 1, "CCTP: At most one Message is supported" @@ -217,6 +226,7 @@ abstract contract CCTPReceiver is CCTPBase { ); } + // Implement this function to handle in-bound deliveries that include a CCTP transfer function receivePayloadAndUSDC( bytes memory payload, uint256 amountUSDCReceived, diff --git a/src/TokenBase.sol b/src/TokenBase.sol index 80db85c..0e20e64 100644 --- a/src/TokenBase.sol +++ b/src/TokenBase.sol @@ -18,26 +18,6 @@ abstract contract TokenBase is Base { ) Base(_wormholeRelayer, _wormhole) { tokenBridge = ITokenBridge(_tokenBridge); } - - function getDecimals( - address tokenAddress - ) internal view returns (uint8 decimals) { - // query decimals - (, bytes memory queriedDecimals) = address(tokenAddress).staticcall( - abi.encodeWithSignature("decimals()") - ); - decimals = abi.decode(queriedDecimals, (uint8)); - } - - function getTokenAddressOnThisChain( - uint16 tokenHomeChain, - bytes32 tokenHomeAddress - ) internal view returns (address tokenAddressOnThisChain) { - return - tokenHomeChain == wormhole.chainId() - ? fromWormholeFormat(tokenHomeAddress) - : tokenBridge.wrappedAsset(tokenHomeChain, tokenHomeAddress); - } } abstract contract TokenSender is TokenBase { @@ -107,6 +87,12 @@ abstract contract TokenSender is TokenBase { }); } + // Publishes a wormhole message representing a 'TokenBridge' transfer of 'amount' of 'token' + // and requests a delivery of the transfer along with 'payload' to 'targetAddress' on 'targetChain' + // + // The second step is done by publishing a wormhole message representing a request + // to call 'receiveWormholeMessages' on the address 'targetAddress' on chain 'targetChain' + // with the payload 'payload' function sendTokenWithPayloadToEvm( uint16 targetChain, address targetAddress, @@ -177,6 +163,26 @@ abstract contract TokenReceiver is TokenBase { uint256 amountNormalized; // if decimals > 8, normalized to 8 decimal places } + function getDecimals( + address tokenAddress + ) internal view returns (uint8 decimals) { + // query decimals + (, bytes memory queriedDecimals) = address(tokenAddress).staticcall( + abi.encodeWithSignature("decimals()") + ); + decimals = abi.decode(queriedDecimals, (uint8)); + } + + function getTokenAddressOnThisChain( + uint16 tokenHomeChain, + bytes32 tokenHomeAddress + ) internal view returns (address tokenAddressOnThisChain) { + return + tokenHomeChain == wormhole.chainId() + ? fromWormholeFormat(tokenHomeAddress) + : tokenBridge.wrappedAsset(tokenHomeChain, tokenHomeAddress); + } + function receiveWormholeMessages( bytes memory payload, bytes[] memory additionalVaas, @@ -233,6 +239,7 @@ abstract contract TokenReceiver is TokenBase { ); } + // Implement this function to handle in-bound deliveries that include a TokenBridge transfer function receivePayloadAndTokens( bytes memory payload, TokenReceived[] memory receivedTokens, diff --git a/src/WormholeRelayerSDK.sol b/src/WormholeRelayerSDK.sol index 06c9772..511cf01 100644 --- a/src/WormholeRelayerSDK.sol +++ b/src/WormholeRelayerSDK.sol @@ -1,4 +1,3 @@ - pragma solidity ^0.8.13; import "./interfaces/IWormholeReceiver.sol"; @@ -7,3 +6,4 @@ import "./Utils.sol"; import {Base} from "./Base.sol"; import {TokenBase, TokenReceiver, TokenSender} from "./TokenBase.sol"; import {CCTPBase, CCTPReceiver, CCTPSender} from "./CCTPBase.sol"; +import {CCTPAndTokenBase, CCTPAndTokenReceiver, CCTPAndTokenSender} from "./CCTPAndTokenBase.sol"; diff --git a/test/CCTPAndTokenBridgeBase.t.sol b/test/CCTPAndTokenBridgeBase.t.sol new file mode 100644 index 0000000..2f32bb5 --- /dev/null +++ b/test/CCTPAndTokenBridgeBase.t.sol @@ -0,0 +1,247 @@ +pragma solidity ^0.8.13; + +import "../src/WormholeRelayerSDK.sol"; +import "../src/interfaces/IWormholeReceiver.sol"; +import "../src/interfaces/IWormholeRelayer.sol"; +import "../src/interfaces/IERC20.sol"; + +import "../src/testing/WormholeRelayerTest.sol"; + +import "../src/WormholeRelayerSDK.sol"; +import "../src/Utils.sol"; + +contract CCTPAndTokenBridgeToy is CCTPAndTokenSender, CCTPAndTokenReceiver { + uint256 constant GAS_LIMIT = 250_000; + + constructor( + address _wormholeRelayer, + address _tokenBridge, + address _wormhole, + address _circleMessageTransmitter, + address _circleTokenMessenger, + address _USDC + ) + CCTPAndTokenBase( + _wormholeRelayer, + _tokenBridge, + _wormhole, + _circleMessageTransmitter, + _circleTokenMessenger, + _USDC + ) + { + setCCTPDomain(2, 0); + setCCTPDomain(6, 1); + } + + function quoteCrossChainDeposit( + uint16 targetChain + ) public view returns (uint256 cost) { + // Cost of delivering token and payload to targetChain + (cost, ) = wormholeRelayer.quoteEVMDeliveryPrice( + targetChain, + 0, + GAS_LIMIT + ); + } + + function sendCrossChainDeposit( + uint16 targetChain, + address recipient, + uint256 amount, + address token + ) public payable { + uint256 cost = quoteCrossChainDeposit(targetChain); + require( + msg.value == cost, + "msg.value must be quoteCrossChainDeposit(targetChain)" + ); + + IERC20(token).transferFrom(msg.sender, address(this), amount); + + bytes memory payload = abi.encode(recipient, amount); + sendTokenWithPayloadToEvm( + targetChain, + fromWormholeFormat(registeredSenders[targetChain]), // address (on targetChain) to send token and payload to + payload, + 0, // receiver value + GAS_LIMIT, + token, + amount + ); + } + + function sendCrossChainUSDCDeposit( + uint16 targetChain, + address recipient, + uint256 amount + ) public payable { + uint256 cost = quoteCrossChainDeposit(targetChain); + require( + msg.value == cost, + "msg.value must be quoteCrossChainDeposit(targetChain)" + ); + + IERC20(USDC).transferFrom(msg.sender, address(this), amount); + + bytes memory payload = abi.encode(recipient, amount); + sendUSDCWithPayloadToEvm( + targetChain, + fromWormholeFormat(registeredSenders[targetChain]), // address (on targetChain) to send token and payload to + payload, + 0, // receiver value + GAS_LIMIT, + amount + ); + } + + function receivePayloadAndUSDC( + bytes memory payload, + uint256 amount, + bytes32 sourceAddress, + uint16 sourceChain, + bytes32 // deliveryHash + ) + internal + override + onlyWormholeRelayer + isRegisteredSender(sourceChain, sourceAddress) + { + (address recipient, uint256 expectedAmount) = abi.decode( + payload, + (address, uint256) + ); + require(amount == expectedAmount, "amount != payload.expectedAmount"); + IERC20(USDC).transfer(recipient, amount); + } + + function receivePayloadAndTokens( + bytes memory payload, + TokenReceived[] memory receivedTokens, + bytes32 sourceAddress, + uint16 sourceChain, + bytes32 // deliveryHash + ) + internal + override + onlyWormholeRelayer + isRegisteredSender(sourceChain, sourceAddress) + { + require(receivedTokens.length == 1, "Expected 1 token transfers"); + address recipient = abi.decode(payload, (address)); + IERC20(receivedTokens[0].tokenAddress).transfer( + recipient, + receivedTokens[0].amount + ); + } +} + +contract WormholeSDKTest is WormholeRelayerBasicTest { + CCTPAndTokenBridgeToy CCTPAndTokenBridgeToySource; + CCTPAndTokenBridgeToy CCTPAndTokenBridgeToyTarget; + ERC20Mock USDCSource; + ERC20Mock USDCTarget; + ERC20Mock public token; + + constructor() { + setTestnetForkChains(2, 6); + } + + function setUpSource() public override { + USDCSource = ERC20Mock(address(sourceChainInfo.USDC)); + mintUSDC(sourceChain, address(this), 5000e18); + CCTPAndTokenBridgeToySource = new CCTPAndTokenBridgeToy( + address(relayerSource), + address(tokenBridgeSource), + address(wormholeSource), + address(sourceChainInfo.circleMessageTransmitter), + address(sourceChainInfo.circleTokenMessenger), + address(USDCSource) + ); + token = createAndAttestToken(sourceChain); + } + + function setUpTarget() public override { + USDCTarget = ERC20Mock(address(targetChainInfo.USDC)); + mintUSDC(targetChain, address(this), 5000e18); + CCTPAndTokenBridgeToyTarget = new CCTPAndTokenBridgeToy( + address(relayerTarget), + address(tokenBridgeTarget), + address(wormholeTarget), + address(targetChainInfo.circleMessageTransmitter), + address(targetChainInfo.circleTokenMessenger), + address(USDCTarget) + ); + } + + function setUpGeneral() public override { + vm.selectFork(sourceFork); + CCTPAndTokenBridgeToySource.setRegisteredSender( + targetChain, + toWormholeFormat(address(CCTPAndTokenBridgeToyTarget)) + ); + + vm.selectFork(targetFork); + CCTPAndTokenBridgeToyTarget.setRegisteredSender( + sourceChain, + toWormholeFormat(address(CCTPAndTokenBridgeToySource)) + ); + } + + function testSendUSDC() public { + vm.selectFork(sourceFork); + + uint256 amount = 100e6; + USDCSource.approve(address(CCTPAndTokenBridgeToySource), amount); + + vm.selectFork(targetFork); + address recipient = 0x1234567890123456789012345678901234567890; + + vm.selectFork(sourceFork); + uint256 cost = CCTPAndTokenBridgeToySource.quoteCrossChainDeposit( + targetChain + ); + + vm.recordLogs(); + CCTPAndTokenBridgeToySource.sendCrossChainUSDCDeposit{value: cost}( + targetChain, + recipient, + amount + ); + performDelivery(true); + + vm.selectFork(targetFork); + assertEq(IERC20(USDCTarget).balanceOf(recipient), amount); + } + + function testSendToken() public { + vm.selectFork(sourceFork); + + uint256 amount = 19e17; + token.approve(address(CCTPAndTokenBridgeToySource), amount); + + vm.selectFork(targetFork); + address recipient = 0x1234567890123456789012345678901234567890; + + vm.selectFork(sourceFork); + uint256 cost = CCTPAndTokenBridgeToySource.quoteCrossChainDeposit( + targetChain + ); + + vm.recordLogs(); + CCTPAndTokenBridgeToySource.sendCrossChainDeposit{value: cost}( + targetChain, + recipient, + amount, + address(token) + ); + performDelivery(); + + vm.selectFork(targetFork); + address wormholeWrappedToken = tokenBridgeTarget.wrappedAsset( + sourceChain, + toWormholeFormat(address(token)) + ); + assertEq(IERC20(wormholeWrappedToken).balanceOf(recipient), amount); + } +} diff --git a/test/CCTPBase.t.sol b/test/CCTPBase.t.sol index 0c819cf..9f75fb6 100644 --- a/test/CCTPBase.t.sol +++ b/test/CCTPBase.t.sol @@ -15,7 +15,6 @@ contract CCTPToy is CCTPSender, CCTPReceiver { constructor( address _wormholeRelayer, - address _tokenBridge, address _wormhole, address _circleMessageTransmitter, address _circleTokenMessenger, @@ -23,7 +22,6 @@ contract CCTPToy is CCTPSender, CCTPReceiver { ) CCTPBase( _wormholeRelayer, - _tokenBridge, _wormhole, _circleMessageTransmitter, _circleTokenMessenger, @@ -105,7 +103,6 @@ contract WormholeSDKTest is WormholeRelayerBasicTest { mintUSDC(sourceChain, address(this), 5000e18); CCTPToySource = new CCTPToy( address(relayerSource), - address(tokenBridgeSource), address(wormholeSource), address(sourceChainInfo.circleMessageTransmitter), address(sourceChainInfo.circleTokenMessenger), @@ -118,7 +115,6 @@ contract WormholeSDKTest is WormholeRelayerBasicTest { mintUSDC(targetChain, address(this), 5000e18); CCTPToyTarget = new CCTPToy( address(relayerTarget), - address(tokenBridgeTarget), address(wormholeTarget), address(targetChainInfo.circleMessageTransmitter), address(targetChainInfo.circleTokenMessenger), From 123c93612dcfcfc202be1aef4af9044b9406de5e Mon Sep 17 00:00:00 2001 From: derpy-duck <115193320+derpy-duck@users.noreply.github.com> Date: Mon, 30 Oct 2023 11:55:30 -0400 Subject: [PATCH 20/23] Add base support cctp --- src/testing/WormholeRelayerTest.sol | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/testing/WormholeRelayerTest.sol b/src/testing/WormholeRelayerTest.sol index 14679f6..b6851d5 100644 --- a/src/testing/WormholeRelayerTest.sol +++ b/src/testing/WormholeRelayerTest.sol @@ -424,9 +424,13 @@ abstract contract WormholeRelayerTest is Test { 0xA31aa3FDb7aF7Db93d18DDA4e19F811342EDF780 ), wormhole: IWormhole(0x23908A62110e21C04F3A4e011d24F901F911744A), - circleMessageTransmitter: IMessageTransmitter(address(0)), - circleTokenMessenger: ITokenMessenger(address(0)), - USDC: IERC20(address(0)) + circleMessageTransmitter: IMessageTransmitter( + address(0x9ff9a4da6f2157A9c82CE756f8fD7E0d75be8895) + ), + circleTokenMessenger: ITokenMessenger( + address(0x877b8e8c9e2383077809787ED6F279ce01CB4cc8) + ), + USDC: IERC20(address(0xF175520C52418dfE19C8098071a252da48Cd1C19)) }); chainInfosMainnet[2] = ChainInfo({ chainId: 2, @@ -650,9 +654,13 @@ abstract contract WormholeRelayerTest is Test { 0x8d2de8d2f73F1F4cAB472AC9A881C9b123C79627 ), wormhole: IWormhole(0xbebdb6C8ddC678FfA9f8748f85C815C556Dd8ac6), - circleMessageTransmitter: IMessageTransmitter(address(0)), - circleTokenMessenger: ITokenMessenger(address(0)), - USDC: IERC20(address(0)) + circleMessageTransmitter: IMessageTransmitter( + address(0xAD09780d193884d503182aD4588450C416D6F9D4) + ), + circleTokenMessenger: ITokenMessenger( + address(0x1682Ae6375C4E4A97e4B583BC394c861A46D8962) + ), + USDC: IERC20(address(0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913)) }); } From 51b944b76e340ce024fae42cd080beaa1840442a Mon Sep 17 00:00:00 2001 From: derpy-duck <115193320+derpy-duck@users.noreply.github.com> Date: Wed, 1 Nov 2023 13:47:31 -0400 Subject: [PATCH 21/23] Improve IWormholeRelayer and IWormholeReceiver docstrings --- src/interfaces/IWormholeReceiver.sol | 24 ++- src/interfaces/IWormholeRelayer.sol | 275 +++++++++++++++------------ 2 files changed, 166 insertions(+), 133 deletions(-) diff --git a/src/interfaces/IWormholeReceiver.sol b/src/interfaces/IWormholeReceiver.sol index 6ef7d89..d865afc 100644 --- a/src/interfaces/IWormholeReceiver.sol +++ b/src/interfaces/IWormholeReceiver.sol @@ -12,11 +12,7 @@ interface IWormholeReceiver { * * NOTE: This function should be restricted such that only the Wormhole Relayer contract can call it. * - * We also recommend that this function: - * - Stores all received `deliveryHash`s in a mapping `(bytes32 => bool)`, and - * on every call, checks that deliveryHash has not already been stored in the - * map (This is to prevent other users maliciously trying to relay the same message) - * - Checks that `sourceChain` and `sourceAddress` are indeed who + * We also recommend that this function checks that `sourceChain` and `sourceAddress` are indeed who * you expect to have requested the calling of `send` on the source chain * * The invocation of this function corresponding to the `send` request will have msg.value equal @@ -26,22 +22,24 @@ interface IWormholeReceiver { * specified by the send requester, this delivery will result in a `ReceiverFailure`. * * @param payload - an arbitrary message which was included in the delivery by the - * requester. - * @param additionalVaas - Additional VAAs which were requested to be included in this delivery. - * They are guaranteed to all be included and in the same order as was specified in the - * delivery request. + * requester. This message's signature will already have been verified (as long as msg.sender is the Wormhole Relayer contract) + * @param additionalMessages - Additional messages which were requested to be included in this delivery. + * Note: There are no contract-level guarantees that the messages in this array are what was requested + * - so any sensitive information here should be verified! + * This field can be used to perform and relay TokenBridge or CCTP transfers, and there are example + * usages of this at + * https://github.com/wormhole-foundation/hello-token + * https://github.com/wormhole-foundation/hello-cctp + * * @param sourceAddress - the (wormhole format) address on the sending chain which requested * this delivery. * @param sourceChain - the wormhole chain ID where this delivery was requested. * @param deliveryHash - the VAA hash of the deliveryVAA. * - * NOTE: These signedVaas are NOT verified by the Wormhole core contract prior to being provided - * to this call. Always make sure `parseAndVerify()` is called on the Wormhole core contract - * before trusting the content of a raw VAA, otherwise the VAA may be invalid or malicious. */ function receiveWormholeMessages( bytes memory payload, - bytes[] memory additionalVaas, + bytes[] memory additionalMessages, bytes32 sourceAddress, uint16 sourceChain, bytes32 deliveryHash diff --git a/src/interfaces/IWormholeRelayer.sol b/src/interfaces/IWormholeRelayer.sol index b0c28be..520a1cc 100644 --- a/src/interfaces/IWormholeRelayer.sol +++ b/src/interfaces/IWormholeRelayer.sol @@ -4,11 +4,11 @@ pragma solidity ^0.8.0; /** * @title WormholeRelayer - * @author - * @notice This project allows developers to build cross-chain applications powered by Wormhole without needing to + * @author + * @notice This project allows developers to build cross-chain applications powered by Wormhole without needing to * write and run their own relaying infrastructure - * - * We implement the IWormholeRelayer interface that allows users to request a delivery provider to relay a payload (and/or additional VAAs) + * + * We implement the IWormholeRelayer interface that allows users to request a delivery provider to relay a payload (and/or additional messages) * to a chain and address of their choice. */ @@ -33,29 +33,38 @@ struct MessageKey { bytes encodedKey; } - interface IWormholeRelayerBase { event SendEvent( - uint64 indexed sequence, uint256 deliveryQuote, uint256 paymentForExtraReceiverValue + uint64 indexed sequence, + uint256 deliveryQuote, + uint256 paymentForExtraReceiverValue ); - function getRegisteredWormholeRelayerContract(uint16 chainId) external view returns (bytes32); + function getRegisteredWormholeRelayerContract( + uint16 chainId + ) external view returns (bytes32); /** * @notice Returns true if a delivery has been attempted for the given deliveryHash * Note: invalid deliveries where the tx reverts are not considered attempted */ - function deliveryAttempted(bytes32 deliveryHash) external view returns (bool attempted); + function deliveryAttempted( + bytes32 deliveryHash + ) external view returns (bool attempted); /** * @notice block number at which a delivery was successfully executed */ - function deliverySuccessBlock(bytes32 deliveryHash) external view returns (uint256 blockNumber); + function deliverySuccessBlock( + bytes32 deliveryHash + ) external view returns (uint256 blockNumber); /** * @notice block number of the latest attempt to execute a delivery that failed */ - function deliveryFailureBlock(bytes32 deliveryHash) external view returns (uint256 blockNumber); + function deliveryFailureBlock( + bytes32 deliveryHash + ) external view returns (uint256 blockNumber); } /** @@ -63,21 +72,20 @@ interface IWormholeRelayerBase { * @notice The interface to request deliveries */ interface IWormholeRelayerSend is IWormholeRelayerBase { - /** * @notice Publishes an instruction for the default delivery provider - * to relay a payload to the address `targetAddress` on chain `targetChain` + * to relay a payload to the address `targetAddress` on chain `targetChain` * with gas limit `gasLimit` and `msg.value` equal to `receiverValue` - * + * * `targetAddress` must implement the IWormholeReceiver interface - * + * * This function must be called with `msg.value` equal to `quoteEVMDeliveryPrice(targetChain, receiverValue, gasLimit)` - * - * Any refunds (from leftover gas) will be paid to the delivery provider. In order to receive the refunds, use the `sendPayloadToEvm` function + * + * Any refunds (from leftover gas) will be paid to the delivery provider. In order to receive the refunds, use the `sendPayloadToEvm` function * with `refundChain` and `refundAddress` as parameters - * + * * @param targetChain in Wormhole Chain ID format - * @param targetAddress address to call on targetChain (that implements IWormholeReceiver) + * @param targetAddress address to call on targetChain (that implements IWormholeReceiver) * @param payload arbitrary bytes to pass in as parameter in call to `targetAddress` * @param receiverValue msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units) * @param gasLimit gas limit with which to call `targetAddress`. @@ -93,16 +101,16 @@ interface IWormholeRelayerSend is IWormholeRelayerBase { /** * @notice Publishes an instruction for the default delivery provider - * to relay a payload to the address `targetAddress` on chain `targetChain` + * to relay a payload to the address `targetAddress` on chain `targetChain` * with gas limit `gasLimit` and `msg.value` equal to `receiverValue` - * + * * Any refunds (from leftover gas) will be sent to `refundAddress` on chain `refundChain` * `targetAddress` must implement the IWormholeReceiver interface - * + * * This function must be called with `msg.value` equal to `quoteEVMDeliveryPrice(targetChain, receiverValue, gasLimit)` - * + * * @param targetChain in Wormhole Chain ID format - * @param targetAddress address to call on targetChain (that implements IWormholeReceiver) + * @param targetAddress address to call on targetChain (that implements IWormholeReceiver) * @param payload arbitrary bytes to pass in as parameter in call to `targetAddress` * @param receiverValue msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units) * @param gasLimit gas limit with which to call `targetAddress`. Any units of gas unused will be refunded according to the @@ -123,21 +131,21 @@ interface IWormholeRelayerSend is IWormholeRelayerBase { /** * @notice Publishes an instruction for the default delivery provider - * to relay a payload and VAAs specified by `vaaKeys` to the address `targetAddress` on chain `targetChain` + * to relay a payload and VAAs specified by `vaaKeys` to the address `targetAddress` on chain `targetChain` * with gas limit `gasLimit` and `msg.value` equal to `receiverValue` - * + * * `targetAddress` must implement the IWormholeReceiver interface - * + * * This function must be called with `msg.value` equal to `quoteEVMDeliveryPrice(targetChain, receiverValue, gasLimit)` - * - * Any refunds (from leftover gas) will be paid to the delivery provider. In order to receive the refunds, use the `sendVaasToEvm` function + * + * Any refunds (from leftover gas) will be paid to the delivery provider. In order to receive the refunds, use the `sendVaasToEvm` function * with `refundChain` and `refundAddress` as parameters - * + * * @param targetChain in Wormhole Chain ID format - * @param targetAddress address to call on targetChain (that implements IWormholeReceiver) + * @param targetAddress address to call on targetChain (that implements IWormholeReceiver) * @param payload arbitrary bytes to pass in as parameter in call to `targetAddress` * @param receiverValue msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units) - * @param gasLimit gas limit with which to call `targetAddress`. + * @param gasLimit gas limit with which to call `targetAddress`. * @param vaaKeys Additional VAAs to pass in as parameter in call to `targetAddress` * @return sequence sequence number of published VAA containing delivery instructions */ @@ -152,19 +160,19 @@ interface IWormholeRelayerSend is IWormholeRelayerBase { /** * @notice Publishes an instruction for the default delivery provider - * to relay a payload and VAAs specified by `vaaKeys` to the address `targetAddress` on chain `targetChain` + * to relay a payload and VAAs specified by `vaaKeys` to the address `targetAddress` on chain `targetChain` * with gas limit `gasLimit` and `msg.value` equal to `receiverValue` - * + * * Any refunds (from leftover gas) will be sent to `refundAddress` on chain `refundChain` * `targetAddress` must implement the IWormholeReceiver interface - * + * * This function must be called with `msg.value` equal to `quoteEVMDeliveryPrice(targetChain, receiverValue, gasLimit)` - * + * * @param targetChain in Wormhole Chain ID format - * @param targetAddress address to call on targetChain (that implements IWormholeReceiver) + * @param targetAddress address to call on targetChain (that implements IWormholeReceiver) * @param payload arbitrary bytes to pass in as parameter in call to `targetAddress` * @param receiverValue msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units) - * @param gasLimit gas limit with which to call `targetAddress`. Any units of gas unused will be refunded according to the + * @param gasLimit gas limit with which to call `targetAddress`. Any units of gas unused will be refunded according to the * `targetChainRefundPerGasUnused` rate quoted by the delivery provider * @param vaaKeys Additional VAAs to pass in as parameter in call to `targetAddress` * @param refundChain The chain to deliver any refund to, in Wormhole Chain ID format @@ -183,30 +191,30 @@ interface IWormholeRelayerSend is IWormholeRelayerBase { ) external payable returns (uint64 sequence); /** - * @notice Publishes an instruction for the delivery provider at `deliveryProviderAddress` - * to relay a payload and VAAs specified by `vaaKeys` to the address `targetAddress` on chain `targetChain` - * with gas limit `gasLimit` and `msg.value` equal to + * @notice Publishes an instruction for the delivery provider at `deliveryProviderAddress` + * to relay a payload and VAAs specified by `vaaKeys` to the address `targetAddress` on chain `targetChain` + * with gas limit `gasLimit` and `msg.value` equal to * receiverValue + (arbitrary amount that is paid for by paymentForExtraReceiverValue of this chain's wei) in targetChain wei. - * + * * Any refunds (from leftover gas) will be sent to `refundAddress` on chain `refundChain` * `targetAddress` must implement the IWormholeReceiver interface - * - * This function must be called with `msg.value` equal to + * + * This function must be called with `msg.value` equal to * quoteEVMDeliveryPrice(targetChain, receiverValue, gasLimit, deliveryProviderAddress) + paymentForExtraReceiverValue - * + * * @param targetChain in Wormhole Chain ID format - * @param targetAddress address to call on targetChain (that implements IWormholeReceiver) + * @param targetAddress address to call on targetChain (that implements IWormholeReceiver) * @param payload arbitrary bytes to pass in as parameter in call to `targetAddress` * @param receiverValue msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units) - * @param paymentForExtraReceiverValue amount (in current chain currency units) to spend on extra receiverValue + * @param paymentForExtraReceiverValue amount (in current chain currency units) to spend on extra receiverValue * (in addition to the `receiverValue` specified) - * @param gasLimit gas limit with which to call `targetAddress`. Any units of gas unused will be refunded according to the + * @param gasLimit gas limit with which to call `targetAddress`. Any units of gas unused will be refunded according to the * `targetChainRefundPerGasUnused` rate quoted by the delivery provider * @param refundChain The chain to deliver any refund to, in Wormhole Chain ID format * @param refundAddress The address on `refundChain` to deliver any refund to * @param deliveryProviderAddress The address of the desired delivery provider's implementation of IDeliveryProvider * @param vaaKeys Additional VAAs to pass in as parameter in call to `targetAddress` - * @param consistencyLevel Consistency level with which to publish the delivery instructions - see + * @param consistencyLevel Consistency level with which to publish the delivery instructions - see * https://book.wormhole.com/wormhole/3_coreLayerContracts.html?highlight=consistency#consistency-levels * @return sequence sequence number of published VAA containing delivery instructions */ @@ -225,33 +233,33 @@ interface IWormholeRelayerSend is IWormholeRelayerBase { ) external payable returns (uint64 sequence); /** - * @notice Publishes an instruction for the delivery provider at `deliveryProviderAddress` - * to relay a payload and external messages specified by `messageKeys` to the address `targetAddress` on chain `targetChain` - * with gas limit `gasLimit` and `msg.value` equal to + * @notice Publishes an instruction for the delivery provider at `deliveryProviderAddress` + * to relay a payload and external messages specified by `messageKeys` to the address `targetAddress` on chain `targetChain` + * with gas limit `gasLimit` and `msg.value` equal to * receiverValue + (arbitrary amount that is paid for by paymentForExtraReceiverValue of this chain's wei) in targetChain wei. - * + * * Any refunds (from leftover gas) will be sent to `refundAddress` on chain `refundChain` * `targetAddress` must implement the IWormholeReceiver interface - * - * This function must be called with `msg.value` equal to + * + * This function must be called with `msg.value` equal to * quoteEVMDeliveryPrice(targetChain, receiverValue, gasLimit, deliveryProviderAddress) + paymentForExtraReceiverValue * - * MessageKeys can specify wormhole messages (VaaKeys) or other types of messages (ex. USDC CCTP attestations). Ensure the selected - * Note: DeliveryProvider supports all the MessageKey.keyType values specified or it will not be delivered! - * + * Note: MessageKeys can specify wormhole messages (VaaKeys) or other types of messages (ex. USDC CCTP attestations). Ensure the selected + * DeliveryProvider supports all the MessageKey.keyType values specified or it will not be delivered! + * * @param targetChain in Wormhole Chain ID format - * @param targetAddress address to call on targetChain (that implements IWormholeReceiver) + * @param targetAddress address to call on targetChain (that implements IWormholeReceiver) * @param payload arbitrary bytes to pass in as parameter in call to `targetAddress` * @param receiverValue msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units) - * @param paymentForExtraReceiverValue amount (in current chain currency units) to spend on extra receiverValue + * @param paymentForExtraReceiverValue amount (in current chain currency units) to spend on extra receiverValue * (in addition to the `receiverValue` specified) - * @param gasLimit gas limit with which to call `targetAddress`. Any units of gas unused will be refunded according to the + * @param gasLimit gas limit with which to call `targetAddress`. Any units of gas unused will be refunded according to the * `targetChainRefundPerGasUnused` rate quoted by the delivery provider * @param refundChain The chain to deliver any refund to, in Wormhole Chain ID format * @param refundAddress The address on `refundChain` to deliver any refund to * @param deliveryProviderAddress The address of the desired delivery provider's implementation of IDeliveryProvider * @param messageKeys Additional messagess to pass in as parameter in call to `targetAddress` - * @param consistencyLevel Consistency level with which to publish the delivery instructions - see + * @param consistencyLevel Consistency level with which to publish the delivery instructions - see * https://book.wormhole.com/wormhole/3_coreLayerContracts.html?highlight=consistency#consistency-levels * @return sequence sequence number of published VAA containing delivery instructions */ @@ -268,24 +276,24 @@ interface IWormholeRelayerSend is IWormholeRelayerBase { MessageKey[] memory messageKeys, uint8 consistencyLevel ) external payable returns (uint64 sequence); - + /** - * @notice Publishes an instruction for the delivery provider at `deliveryProviderAddress` - * to relay a payload and VAAs specified by `vaaKeys` to the address `targetAddress` on chain `targetChain` - * with `msg.value` equal to + * @notice Publishes an instruction for the delivery provider at `deliveryProviderAddress` + * to relay a payload and VAAs specified by `vaaKeys` to the address `targetAddress` on chain `targetChain` + * with `msg.value` equal to * receiverValue + (arbitrary amount that is paid for by paymentForExtraReceiverValue of this chain's wei) in targetChain wei. - * + * * Any refunds (from leftover gas) will be sent to `refundAddress` on chain `refundChain` * `targetAddress` must implement the IWormholeReceiver interface - * - * This function must be called with `msg.value` equal to - * quoteDeliveryPrice(targetChain, receiverValue, encodedExecutionParameters, deliveryProviderAddress) + paymentForExtraReceiverValue - * + * + * This function must be called with `msg.value` equal to + * quoteDeliveryPrice(targetChain, receiverValue, encodedExecutionParameters, deliveryProviderAddress) + paymentForExtraReceiverValue + * * @param targetChain in Wormhole Chain ID format * @param targetAddress address to call on targetChain (that implements IWormholeReceiver), in Wormhole bytes32 format * @param payload arbitrary bytes to pass in as parameter in call to `targetAddress` * @param receiverValue msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units) - * @param paymentForExtraReceiverValue amount (in current chain currency units) to spend on extra receiverValue + * @param paymentForExtraReceiverValue amount (in current chain currency units) to spend on extra receiverValue * (in addition to the `receiverValue` specified) * @param encodedExecutionParameters encoded information on how to execute delivery that may impact pricing * e.g. for version EVM_V1, this is a struct that encodes the `gasLimit` with which to call `targetAddress` @@ -293,7 +301,7 @@ interface IWormholeRelayerSend is IWormholeRelayerBase { * @param refundAddress The address on `refundChain` to deliver any refund to, in Wormhole bytes32 format * @param deliveryProviderAddress The address of the desired delivery provider's implementation of IDeliveryProvider * @param vaaKeys Additional VAAs to pass in as parameter in call to `targetAddress` - * @param consistencyLevel Consistency level with which to publish the delivery instructions - see + * @param consistencyLevel Consistency level with which to publish the delivery instructions - see * https://book.wormhole.com/wormhole/3_coreLayerContracts.html?highlight=consistency#consistency-levels * @return sequence sequence number of published VAA containing delivery instructions */ @@ -312,25 +320,25 @@ interface IWormholeRelayerSend is IWormholeRelayerBase { ) external payable returns (uint64 sequence); /** - * @notice Publishes an instruction for the delivery provider at `deliveryProviderAddress` - * to relay a payload and VAAs specified by `vaaKeys` to the address `targetAddress` on chain `targetChain` - * with `msg.value` equal to + * @notice Publishes an instruction for the delivery provider at `deliveryProviderAddress` + * to relay a payload and VAAs specified by `vaaKeys` to the address `targetAddress` on chain `targetChain` + * with `msg.value` equal to * receiverValue + (arbitrary amount that is paid for by paymentForExtraReceiverValue of this chain's wei) in targetChain wei. - * + * * Any refunds (from leftover gas) will be sent to `refundAddress` on chain `refundChain` * `targetAddress` must implement the IWormholeReceiver interface - * - * This function must be called with `msg.value` equal to - * quoteDeliveryPrice(targetChain, receiverValue, encodedExecutionParameters, deliveryProviderAddress) + paymentForExtraReceiverValue * - * MessageKeys can specify wormhole messages (VaaKeys) or other types of messages (ex. USDC CCTP attestations). Ensure the selected - * Note: DeliveryProvider supports all the MessageKey.keyType values specified or it will not be delivered! - * + * This function must be called with `msg.value` equal to + * quoteDeliveryPrice(targetChain, receiverValue, encodedExecutionParameters, deliveryProviderAddress) + paymentForExtraReceiverValue + * + * Note: MessageKeys can specify wormhole messages (VaaKeys) or other types of messages (ex. USDC CCTP attestations). Ensure the selected + * DeliveryProvider supports all the MessageKey.keyType values specified or it will not be delivered! + * * @param targetChain in Wormhole Chain ID format * @param targetAddress address to call on targetChain (that implements IWormholeReceiver), in Wormhole bytes32 format * @param payload arbitrary bytes to pass in as parameter in call to `targetAddress` * @param receiverValue msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units) - * @param paymentForExtraReceiverValue amount (in current chain currency units) to spend on extra receiverValue + * @param paymentForExtraReceiverValue amount (in current chain currency units) to spend on extra receiverValue * (in addition to the `receiverValue` specified) * @param encodedExecutionParameters encoded information on how to execute delivery that may impact pricing * e.g. for version EVM_V1, this is a struct that encodes the `gasLimit` with which to call `targetAddress` @@ -338,7 +346,7 @@ interface IWormholeRelayerSend is IWormholeRelayerBase { * @param refundAddress The address on `refundChain` to deliver any refund to, in Wormhole bytes32 format * @param deliveryProviderAddress The address of the desired delivery provider's implementation of IDeliveryProvider * @param messageKeys Additional messagess to pass in as parameter in call to `targetAddress` - * @param consistencyLevel Consistency level with which to publish the delivery instructions - see + * @param consistencyLevel Consistency level with which to publish the delivery instructions - see * https://book.wormhole.com/wormhole/3_coreLayerContracts.html?highlight=consistency#consistency-levels * @return sequence sequence number of published VAA containing delivery instructions */ @@ -357,22 +365,22 @@ interface IWormholeRelayerSend is IWormholeRelayerBase { ) external payable returns (uint64 sequence); /** - * @notice Requests a previously published delivery instruction to be redelivered + * @notice Requests a previously published delivery instruction to be redelivered * (e.g. with a different delivery provider) * - * This function must be called with `msg.value` equal to + * This function must be called with `msg.value` equal to * quoteEVMDeliveryPrice(targetChain, newReceiverValue, newGasLimit, newDeliveryProviderAddress) - * + * * @notice *** This will only be able to succeed if the following is true ** * - newGasLimit >= gas limit of the old instruction * - newReceiverValue >= receiver value of the old instruction * - newDeliveryProvider's `targetChainRefundPerGasUnused` >= old relay provider's `targetChainRefundPerGasUnused` - * - * @param deliveryVaaKey VaaKey identifying the wormhole message containing the + * + * @param deliveryVaaKey VaaKey identifying the wormhole message containing the * previously published delivery instructions * @param targetChain The target chain that the original delivery targeted. Must match targetChain from original delivery instructions * @param newReceiverValue new msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units) - * @param newGasLimit gas limit with which to call `targetAddress`. Any units of gas unused will be refunded according to the + * @param newGasLimit gas limit with which to call `targetAddress`. Any units of gas unused will be refunded according to the * `targetChainRefundPerGasUnused` rate quoted by the delivery provider, to the refund chain and address specified in the original request * @param newDeliveryProviderAddress The address of the desired delivery provider's implementation of IDeliveryProvider * @return sequence sequence number of published VAA containing redelivery instructions @@ -380,7 +388,6 @@ interface IWormholeRelayerSend is IWormholeRelayerBase { * @notice *** This will only be able to succeed if the following is true ** * - newGasLimit >= gas limit of the old instruction * - newReceiverValue >= receiver value of the old instruction - * - newDeliveryProvider's `targetChainRefundPerGasUnused` >= old relay provider's `targetChainRefundPerGasUnused` */ function resendToEvm( VaaKey memory deliveryVaaKey, @@ -391,13 +398,13 @@ interface IWormholeRelayerSend is IWormholeRelayerBase { ) external payable returns (uint64 sequence); /** - * @notice Requests a previously published delivery instruction to be redelivered - * + * @notice Requests a previously published delivery instruction to be redelivered + * * - * This function must be called with `msg.value` equal to + * This function must be called with `msg.value` equal to * quoteDeliveryPrice(targetChain, newReceiverValue, newEncodedExecutionParameters, newDeliveryProviderAddress) - * - * @param deliveryVaaKey VaaKey identifying the wormhole message containing the + * + * @param deliveryVaaKey VaaKey identifying the wormhole message containing the * previously published delivery instructions * @param targetChain The target chain that the original delivery targeted. Must match targetChain from original delivery instructions * @param newReceiverValue new msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units) @@ -405,7 +412,7 @@ interface IWormholeRelayerSend is IWormholeRelayerBase { * e.g. for version EVM_V1, this is a struct that encodes the `gasLimit` with which to call `targetAddress` * @param newDeliveryProviderAddress The address of the desired delivery provider's implementation of IDeliveryProvider * @return sequence sequence number of published VAA containing redelivery instructions - * + * * @notice *** This will only be able to succeed if the following is true ** * - (For EVM_V1) newGasLimit >= gas limit of the old instruction * - newReceiverValue >= receiver value of the old instruction @@ -421,41 +428,59 @@ interface IWormholeRelayerSend is IWormholeRelayerBase { /** * @notice Returns the price to request a relay to chain `targetChain`, using the default delivery provider - * + * * @param targetChain in Wormhole Chain ID format * @param receiverValue msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units) - * @param gasLimit gas limit with which to call `targetAddress`. + * @param gasLimit gas limit with which to call `targetAddress`. * @return nativePriceQuote Price, in units of current chain currency, that the delivery provider charges to perform the relay - * @return targetChainRefundPerGasUnused amount of target chain currency that will be refunded per unit of gas unused, - * if a refundAddress is specified + * @return targetChainRefundPerGasUnused amount of target chain currency that will be refunded per unit of gas unused, + * if a refundAddress is specified. + * Note: This value can be overridden by the delivery provider on the target chain. The returned value here should be considered to be a + * promise by the delivery provider of the amount of refund per gas unused that will be returned to the refundAddress at the target chain. + * If a delivery provider decides to override, this will be visible as part of the emitted Delivery event on the target chain. */ function quoteEVMDeliveryPrice( uint16 targetChain, uint256 receiverValue, uint256 gasLimit - ) external view returns (uint256 nativePriceQuote, uint256 targetChainRefundPerGasUnused); + ) + external + view + returns ( + uint256 nativePriceQuote, + uint256 targetChainRefundPerGasUnused + ); /** * @notice Returns the price to request a relay to chain `targetChain`, using delivery provider `deliveryProviderAddress` - * + * * @param targetChain in Wormhole Chain ID format * @param receiverValue msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units) - * @param gasLimit gas limit with which to call `targetAddress`. + * @param gasLimit gas limit with which to call `targetAddress`. * @param deliveryProviderAddress The address of the desired delivery provider's implementation of IDeliveryProvider * @return nativePriceQuote Price, in units of current chain currency, that the delivery provider charges to perform the relay - * @return targetChainRefundPerGasUnused amount of target chain currency that will be refunded per unit of gas unused, + * @return targetChainRefundPerGasUnused amount of target chain currency that will be refunded per unit of gas unused, * if a refundAddress is specified + * Note: This value can be overridden by the delivery provider on the target chain. The returned value here should be considered to be a + * promise by the delivery provider of the amount of refund per gas unused that will be returned to the refundAddress at the target chain. + * If a delivery provider decides to override, this will be visible as part of the emitted Delivery event on the target chain. */ function quoteEVMDeliveryPrice( uint16 targetChain, uint256 receiverValue, uint256 gasLimit, address deliveryProviderAddress - ) external view returns (uint256 nativePriceQuote, uint256 targetChainRefundPerGasUnused); + ) + external + view + returns ( + uint256 nativePriceQuote, + uint256 targetChainRefundPerGasUnused + ); /** * @notice Returns the price to request a relay to chain `targetChain`, using delivery provider `deliveryProviderAddress` - * + * * @param targetChain in Wormhole Chain ID format * @param receiverValue msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units) * @param encodedExecutionParameters encoded information on how to execute delivery that may impact pricing @@ -464,7 +489,7 @@ interface IWormholeRelayerSend is IWormholeRelayerBase { * @return nativePriceQuote Price, in units of current chain currency, that the delivery provider charges to perform the relay * @return encodedExecutionInfo encoded information on how the delivery will be executed * e.g. for version EVM_V1, this is a struct that encodes the `gasLimit` and `targetChainRefundPerGasUnused` - * (which is the amount of target chain currency that will be refunded per unit of gas unused, + * (which is the amount of target chain currency that will be refunded per unit of gas unused, * if a refundAddress is specified) */ function quoteDeliveryPrice( @@ -472,12 +497,15 @@ interface IWormholeRelayerSend is IWormholeRelayerBase { uint256 receiverValue, bytes memory encodedExecutionParameters, address deliveryProviderAddress - ) external view returns (uint256 nativePriceQuote, bytes memory encodedExecutionInfo); + ) + external + view + returns (uint256 nativePriceQuote, bytes memory encodedExecutionInfo); /** * @notice Returns the (extra) amount of target chain currency that `targetAddress` * will be called with, if the `paymentForExtraReceiverValue` field is set to `currentChainAmount` - * + * * @param targetChain in Wormhole Chain ID format * @param currentChainAmount The value that `paymentForExtraReceiverValue` will be set to * @param deliveryProviderAddress The address of the desired delivery provider's implementation of IDeliveryProvider @@ -495,12 +523,15 @@ interface IWormholeRelayerSend is IWormholeRelayerBase { * @return deliveryProvider The address of (the default delivery provider)'s contract on this source * chain. This must be a contract that implements IDeliveryProvider. */ - function getDefaultDeliveryProvider() external view returns (address deliveryProvider); + function getDefaultDeliveryProvider() + external + view + returns (address deliveryProvider); } /** * @title IWormholeRelayerDelivery - * @notice The interface to execute deliveries. Only relevant for Delivery Providers + * @notice The interface to execute deliveries. Only relevant for Delivery Providers */ interface IWormholeRelayerDelivery is IWormholeRelayerBase { enum DeliveryStatus { @@ -513,7 +544,8 @@ interface IWormholeRelayerDelivery is IWormholeRelayerBase { REFUND_FAIL, CROSS_CHAIN_REFUND_SENT, CROSS_CHAIN_REFUND_FAIL_PROVIDER_NOT_SUPPORTED, - CROSS_CHAIN_REFUND_FAIL_NOT_ENOUGH + CROSS_CHAIN_REFUND_FAIL_NOT_ENOUGH, + NO_REFUND_REQUESTED } /** @@ -524,17 +556,17 @@ interface IWormholeRelayerDelivery is IWormholeRelayerBase { * corresponding to this delivery request * @custom:member deliveryVaaHash - The hash of the delivery VAA corresponding to this delivery * request - * @custom:member gasUsed - The amount of gas that was used to call your target contract + * @custom:member gasUsed - The amount of gas that was used to call your target contract * @custom:member status: * - RECEIVER_FAILURE, if the target contract reverts - * - SUCCESS, if the target contract doesn't revert + * - SUCCESS, if the target contract doesn't revert * @custom:member additionalStatusInfo: * - If status is SUCCESS, then this is empty. * - If status is RECEIVER_FAILURE, this is `RETURNDATA_TRUNCATION_THRESHOLD` bytes of the * return data (i.e. potentially truncated revert reason information). * @custom:member refundStatus - Result of the refund. REFUND_SUCCESS or REFUND_FAIL are for * refunds where targetChain=refundChain; the others are for targetChain!=refundChain, - * where a cross chain refund is necessary + * where a cross chain refund is necessary, or if the default code path is used where no refund is requested (NO_REFUND_REQUESTED) * @custom:member overridesInfo: * - If not an override: empty bytes array * - Otherwise: An encoded `DeliveryOverride` @@ -553,14 +585,14 @@ interface IWormholeRelayerDelivery is IWormholeRelayerBase { /** * @notice The delivery provider calls `deliver` to relay messages as described by one delivery instruction - * + * * The delivery provider must pass in the specified (by VaaKeys[]) signed wormhole messages (VAAs) from the source chain * as well as the signed wormhole message with the delivery instructions (the delivery VAA) * * The messages will be relayed to the target address (with the specified gas limit and receiver value) iff the following checks are met: * - the delivery VAA has a valid signature * - the delivery VAA's emitter is one of these WormholeRelayer contracts - * - the delivery provider passed in at least enough of this chain's currency as msg.value (enough meaning the maximum possible refund) + * - the delivery provider passed in at least enough of this chain's currency as msg.value (enough meaning the maximum possible refund) * - the instruction's target chain is this chain * - the relayed signed VAAs match the descriptions in container.messages (the VAA hashes match, or the emitter address, sequence number pair matches, depending on the description given) * @@ -597,7 +629,10 @@ error InvalidMsgValue(uint256 msgValue, uint256 totalFee); error RequestedGasLimitTooLow(); -error DeliveryProviderDoesNotSupportTargetChain(address relayer, uint16 chainId); +error DeliveryProviderDoesNotSupportTargetChain( + address relayer, + uint16 chainId +); error DeliveryProviderCannotReceivePayment(); error DeliveryProviderDoesNotSupportMessageKeyType(uint8 keyType); @@ -634,4 +669,4 @@ error InsufficientRelayerFunds(uint256 msgValue, uint256 minimum); //When a bytes32 field can't be converted into a 20 byte EVM address, because the 12 padding bytes // are non-zero (duplicated from Utils.sol) -error NotAnEvmAddress(bytes32); \ No newline at end of file +error NotAnEvmAddress(bytes32); From 71ca620f43fb13c8b6e749ce265a52e33e2a8a67 Mon Sep 17 00:00:00 2001 From: derpy-duck <115193320+derpy-duck@users.noreply.github.com> Date: Thu, 2 Nov 2023 12:20:17 -0400 Subject: [PATCH 22/23] better message for IWormholeReceiver --- src/interfaces/IWormholeReceiver.sol | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/interfaces/IWormholeReceiver.sol b/src/interfaces/IWormholeReceiver.sol index d865afc..fa5e594 100644 --- a/src/interfaces/IWormholeReceiver.sol +++ b/src/interfaces/IWormholeReceiver.sol @@ -25,7 +25,11 @@ interface IWormholeReceiver { * requester. This message's signature will already have been verified (as long as msg.sender is the Wormhole Relayer contract) * @param additionalMessages - Additional messages which were requested to be included in this delivery. * Note: There are no contract-level guarantees that the messages in this array are what was requested - * - so any sensitive information here should be verified! + * so **you should verify any sensitive information given here!** + * + * For example, if a 'VaaKey' was specified on the source chain, then MAKE SURE the corresponding message here + * has valid signatures (by calling `parseAndVerifyVM(message)` on the Wormhole core contract) + * * This field can be used to perform and relay TokenBridge or CCTP transfers, and there are example * usages of this at * https://github.com/wormhole-foundation/hello-token From 88580b0e48a0f4874a1d60509967a45bb7f729c8 Mon Sep 17 00:00:00 2001 From: derpy-duck <115193320+derpy-duck@users.noreply.github.com> Date: Fri, 3 Nov 2023 10:30:09 -0400 Subject: [PATCH 23/23] remove replay protection --- src/Base.sol | 29 +++++---- test/Fork.t.sol | 156 ++++++++++++++++++++++++++++++++++++------------ 2 files changed, 135 insertions(+), 50 deletions(-) diff --git a/src/Base.sol b/src/Base.sol index 492fd24..d54481a 100644 --- a/src/Base.sol +++ b/src/Base.sol @@ -1,4 +1,3 @@ - pragma solidity ^0.8.13; import "./interfaces/IWormholeReceiver.sol"; @@ -10,8 +9,6 @@ abstract contract Base { IWormholeRelayer public immutable wormholeRelayer; IWormhole public immutable wormhole; - mapping(bytes32 => bool) public seenDeliveryVaaHashes; - address registrationOwner; mapping(uint16 => bytes32) registeredSenders; @@ -22,18 +19,18 @@ abstract contract Base { } modifier onlyWormholeRelayer() { - require(msg.sender == address(wormholeRelayer), "Msg.sender is not Wormhole Relayer"); - _; - } - - modifier replayProtect(bytes32 deliveryHash) { - require(!seenDeliveryVaaHashes[deliveryHash], "Message already processed"); - seenDeliveryVaaHashes[deliveryHash] = true; + require( + msg.sender == address(wormholeRelayer), + "Msg.sender is not Wormhole Relayer" + ); _; } modifier isRegisteredSender(uint16 sourceChain, bytes32 sourceAddress) { - require(registeredSenders[sourceChain] == sourceAddress, "Not registered sender"); + require( + registeredSenders[sourceChain] == sourceAddress, + "Not registered sender" + ); _; } @@ -44,8 +41,14 @@ abstract contract Base { * Assumes only one sender per chain is valid * Sender is the address that called 'send' on the Wormhole Relayer contract on the source chain) */ - function setRegisteredSender(uint16 sourceChain, bytes32 sourceAddress) public { - require(msg.sender == registrationOwner, "Not allowed to set registered sender"); + function setRegisteredSender( + uint16 sourceChain, + bytes32 sourceAddress + ) public { + require( + msg.sender == registrationOwner, + "Not allowed to set registered sender" + ); registeredSenders[sourceChain] = sourceAddress; } } diff --git a/test/Fork.t.sol b/test/Fork.t.sol index e0923c4..c20b56e 100644 --- a/test/Fork.t.sol +++ b/test/Fork.t.sol @@ -1,4 +1,3 @@ - pragma solidity ^0.8.13; import "../src/WormholeRelayerSDK.sol"; @@ -18,15 +17,23 @@ contract Toy is Base { uint256 public payloadReceived; - constructor(address _wormholeRelayer, address _wormhole) Base(_wormholeRelayer, _wormhole) {} + constructor( + address _wormholeRelayer, + address _wormhole + ) Base(_wormholeRelayer, _wormhole) {} function receiveWormholeMessages( bytes memory payload, bytes[] memory, bytes32 sourceAddress, uint16 sourceChain, - bytes32 deliveryHash - ) public payable onlyWormholeRelayer replayProtect(deliveryHash) isRegisteredSender(sourceChain, sourceAddress) { + bytes32 // deliveryHash + ) + public + payable + onlyWormholeRelayer + isRegisteredSender(sourceChain, sourceAddress) + { payloadReceived = abi.decode(payload, (uint256)); console.log("Toy received message"); @@ -36,19 +43,29 @@ contract Toy is Base { } contract TokenToy is TokenSender, TokenReceiver { - constructor (address _wormholeRelayer, address _bridge, address _wormhole) TokenBase(_wormholeRelayer, _bridge, _wormhole) {} - + constructor( + address _wormholeRelayer, + address _bridge, + address _wormhole + ) TokenBase(_wormholeRelayer, _bridge, _wormhole) {} + uint256 constant GAS_LIMIT = 250_000; - - function quoteCrossChainDeposit(uint16 targetChain) public view returns (uint256 cost) { + + function quoteCrossChainDeposit( + uint16 targetChain + ) public view returns (uint256 cost) { // Cost of delivering token and payload to targetChain uint256 deliveryCost; - (deliveryCost,) = wormholeRelayer.quoteEVMDeliveryPrice(targetChain, 0, GAS_LIMIT); + (deliveryCost, ) = wormholeRelayer.quoteEVMDeliveryPrice( + targetChain, + 0, + GAS_LIMIT + ); // Total cost: delivery cost + cost of publishing the 'sending token' wormhole message cost = deliveryCost + wormhole.messageFee(); } - + function sendCrossChainDeposit( uint16 targetChain, address recipient, @@ -56,17 +73,20 @@ contract TokenToy is TokenSender, TokenReceiver { address token ) public payable { uint256 cost = quoteCrossChainDeposit(targetChain); - require(msg.value == cost, "msg.value must be quoteCrossChainDeposit(targetChain)"); + require( + msg.value == cost, + "msg.value must be quoteCrossChainDeposit(targetChain)" + ); IERC20(token).transferFrom(msg.sender, address(this), amount); bytes memory payload = abi.encode(recipient); sendTokenWithPayloadToEvm( - targetChain, + targetChain, fromWormholeFormat(registeredSenders[targetChain]), // address (on targetChain) to send token and payload to - payload, + payload, 0, // receiver value - GAS_LIMIT, + GAS_LIMIT, token, // address of IERC20 token contract amount ); @@ -81,17 +101,20 @@ contract TokenToy is TokenSender, TokenReceiver { address refundAddress ) public payable { uint256 cost = quoteCrossChainDeposit(targetChain); - require(msg.value == cost, "msg.value must be quoteCrossChainDeposit(targetChain)"); + require( + msg.value == cost, + "msg.value must be quoteCrossChainDeposit(targetChain)" + ); IERC20(token).transferFrom(msg.sender, address(this), amount); bytes memory payload = abi.encode(recipient); sendTokenWithPayloadToEvm( - targetChain, + targetChain, fromWormholeFormat(registeredSenders[targetChain]), // address (on targetChain) to send token and payload to - payload, + payload, 0, // receiver value - GAS_LIMIT, + GAS_LIMIT, token, // address of IERC20 token contract amount, refundChain, @@ -105,10 +128,18 @@ contract TokenToy is TokenSender, TokenReceiver { bytes32 sourceAddress, uint16 sourceChain, bytes32 // deliveryHash - ) internal override onlyWormholeRelayer isRegisteredSender(sourceChain, sourceAddress) { + ) + internal + override + onlyWormholeRelayer + isRegisteredSender(sourceChain, sourceAddress) + { require(receivedTokens.length == 1, "Expected 1 token transfers"); address recipient = abi.decode(payload, (address)); - IERC20(receivedTokens[0].tokenAddress).transfer(recipient, receivedTokens[0].amount); + IERC20(receivedTokens[0].tokenAddress).transfer( + recipient, + receivedTokens[0].amount + ); } } @@ -121,33 +152,62 @@ contract WormholeSDKTest is WormholeRelayerBasicTest { function setUpSource() public override { toySource = new Toy(address(relayerSource), address(wormholeSource)); - toySource.setRegisteredSender(targetChain, toWormholeFormat(address(this))); + toySource.setRegisteredSender( + targetChain, + toWormholeFormat(address(this)) + ); - tokenToySource = new TokenToy(address(relayerSource), address(tokenBridgeSource), address(wormholeSource)); + tokenToySource = new TokenToy( + address(relayerSource), + address(tokenBridgeSource), + address(wormholeSource) + ); token = createAndAttestToken(sourceChain); } function setUpTarget() public override { toyTarget = new Toy(address(relayerTarget), address(wormholeTarget)); - toyTarget.setRegisteredSender(sourceChain, toWormholeFormat(address(this))); + toyTarget.setRegisteredSender( + sourceChain, + toWormholeFormat(address(this)) + ); - tokenToyTarget = new TokenToy(address(relayerTarget), address(tokenBridgeTarget), address(wormholeTarget)); + tokenToyTarget = new TokenToy( + address(relayerTarget), + address(tokenBridgeTarget), + address(wormholeTarget) + ); } function setUpGeneral() public override { vm.selectFork(sourceFork); - tokenToySource.setRegisteredSender(targetChain, toWormholeFormat(address(tokenToyTarget))); + tokenToySource.setRegisteredSender( + targetChain, + toWormholeFormat(address(tokenToyTarget)) + ); vm.selectFork(targetFork); - tokenToyTarget.setRegisteredSender(sourceChain, toWormholeFormat(address(tokenToySource))); + tokenToyTarget.setRegisteredSender( + sourceChain, + toWormholeFormat(address(tokenToySource)) + ); } function testSendMessage() public { - vm.recordLogs(); - (uint256 cost,) = relayerSource.quoteEVMDeliveryPrice(targetChain, 1e17, 100_000); - relayerSource.sendPayloadToEvm{value: cost}(targetChain, address(toyTarget), abi.encode(55), 1e17, 100_000); + (uint256 cost, ) = relayerSource.quoteEVMDeliveryPrice( + targetChain, + 1e17, + 100_000 + ); + relayerSource.sendPayloadToEvm{value: cost}( + targetChain, + address(toyTarget), + abi.encode(55), + 1e17, + 100_000 + ); performDelivery(); vm.selectFork(targetFork); @@ -158,8 +218,18 @@ contract WormholeSDKTest is WormholeRelayerBasicTest { vm.selectFork(targetFork); vm.recordLogs(); - (uint256 cost,) = relayerTarget.quoteEVMDeliveryPrice(sourceChain, 1e17, 100_000); - relayerTarget.sendPayloadToEvm{value: cost}(sourceChain, address(toySource), abi.encode(56), 1e17, 100_000); + (uint256 cost, ) = relayerTarget.quoteEVMDeliveryPrice( + sourceChain, + 1e17, + 100_000 + ); + relayerTarget.sendPayloadToEvm{value: cost}( + sourceChain, + address(toySource), + abi.encode(56), + 1e17, + 100_000 + ); performDelivery(); vm.selectFork(sourceFork); @@ -167,7 +237,6 @@ contract WormholeSDKTest is WormholeRelayerBasicTest { } function testSendToken() public { - vm.selectFork(sourceFork); uint256 amount = 19e17; @@ -181,17 +250,22 @@ contract WormholeSDKTest is WormholeRelayerBasicTest { vm.recordLogs(); tokenToySource.sendCrossChainDeposit{value: cost}( - targetChain, recipient, amount, address(token) + targetChain, + recipient, + amount, + address(token) ); performDelivery(); vm.selectFork(targetFork); - address wormholeWrappedToken = tokenBridgeTarget.wrappedAsset(sourceChain, toWormholeFormat(address(token))); + address wormholeWrappedToken = tokenBridgeTarget.wrappedAsset( + sourceChain, + toWormholeFormat(address(token)) + ); assertEq(IERC20(wormholeWrappedToken).balanceOf(recipient), amount); } function testSendTokenWithRefund() public { - vm.selectFork(sourceFork); uint256 amount = 19e17; @@ -205,12 +279,20 @@ contract WormholeSDKTest is WormholeRelayerBasicTest { vm.recordLogs(); tokenToySource.sendCrossChainDeposit{value: cost}( - targetChain, recipient, amount, address(token), targetChain, refundAddress + targetChain, + recipient, + amount, + address(token), + targetChain, + refundAddress ); performDelivery(); vm.selectFork(targetFork); - address wormholeWrappedToken = tokenBridgeTarget.wrappedAsset(sourceChain, toWormholeFormat(address(token))); + address wormholeWrappedToken = tokenBridgeTarget.wrappedAsset( + sourceChain, + toWormholeFormat(address(token)) + ); assertEq(IERC20(wormholeWrappedToken).balanceOf(recipient), amount); assertTrue(refundAddress.balance > 0); }