From 189fd62d6e9628fb28e694fcea16d5a591080684 Mon Sep 17 00:00:00 2001 From: Andreas <41449730+nonergodic@users.noreply.github.com> Date: Wed, 28 Aug 2024 16:18:19 -0700 Subject: [PATCH] Merge testing refactor (#52) * refactor and enhance testing utilities * fix WormholeOverride.sign() by using provided params * enable decodeDeposit from bytes directly * further cleanup refactor and extend README * fix formating --- README.md | 10 +- foundry.toml | 6 + src/WormholeRelayer/CCTPAndTokenBase.sol | 2 +- src/WormholeRelayer/CCTPBase.sol | 2 +- src/WormholeRelayer/TokenBase.sol | 2 +- src/interfaces/ITokenBridge.sol | 3 +- src/interfaces/cctp/IMessageTransmitter.sol | 93 +++- src/interfaces/cctp/IReceiver.sol | 33 -- src/interfaces/cctp/IRelayer.sol | 71 --- src/interfaces/cctp/ITokenMessenger.sol | 121 +++-- src/interfaces/cctp/ITokenMinter.sol | 40 ++ src/interfaces/cctp/shared/IOwnable2Step.sol | 18 + src/interfaces/cctp/shared/IPausable.sol | 20 + src/interfaces/token/IERC20.sol | 81 +-- src/interfaces/token/IUSDC.sol | 17 - src/interfaces/token/IWETH.sol | 6 +- src/libraries/WormholeCctpMessages.sol | 200 ++++++++ src/testing/CctpMessages.sol | 145 ++++++ src/testing/CctpOverride.sol | 84 ++++ src/testing/Constants.sol | 9 + src/testing/ERC20Mock.sol | 2 +- src/testing/LogUtils.sol | 50 ++ src/testing/UsdcDealer.sol | 26 + src/testing/WormholeCctpSimulator.sol | 176 +++++++ src/testing/WormholeOverride.sol | 214 ++++++++ .../WormholeRelayer/MockOffchainRelayer.sol | 184 ++++--- src/testing/WormholeRelayerTest.sol | 127 ++--- src/testing/helpers/BytesLib.sol | 476 ------------------ src/testing/helpers/CircleCCTPSimulator.sol | 156 ------ src/testing/helpers/WormholeSimulator.sol | 281 ----------- 30 files changed, 1285 insertions(+), 1370 deletions(-) delete mode 100644 src/interfaces/cctp/IReceiver.sol delete mode 100644 src/interfaces/cctp/IRelayer.sol create mode 100644 src/interfaces/cctp/ITokenMinter.sol create mode 100644 src/interfaces/cctp/shared/IOwnable2Step.sol create mode 100644 src/interfaces/cctp/shared/IPausable.sol delete mode 100644 src/interfaces/token/IUSDC.sol create mode 100644 src/libraries/WormholeCctpMessages.sol create mode 100644 src/testing/CctpMessages.sol create mode 100644 src/testing/CctpOverride.sol create mode 100644 src/testing/Constants.sol create mode 100644 src/testing/LogUtils.sol create mode 100644 src/testing/UsdcDealer.sol create mode 100644 src/testing/WormholeCctpSimulator.sol create mode 100644 src/testing/WormholeOverride.sol delete mode 100644 src/testing/helpers/BytesLib.sol delete mode 100644 src/testing/helpers/CircleCCTPSimulator.sol delete mode 100644 src/testing/helpers/WormholeSimulator.sol diff --git a/README.md b/README.md index 0859594..b07d96f 100644 --- a/README.md +++ b/README.md @@ -28,10 +28,18 @@ forge install wormhole-foundation/wormhole-solidity-sdk@v0.1.0 **EVM Version** -One hazard of developing EVM contracts in a cross-chain environment is that different chains have varying levels EVM-equivalence. This means you have to ensure that all chains that you are planning to deploy to support all EIPs/opcodes that you rely on. +One hazard of developing EVM contracts in a cross-chain environment is that different chains have varying levels of "EVM-equivalence". This means you have to ensure that all chains that you are planning to deploy to support all EIPs/opcodes that you rely on. For example, if you are using a solc version newer than `0.8.19` and are planning to deploy to a chain that does not support [PUSH0 opcode](https://eips.ethereum.org/EIPS/eip-3855) (introduced as part of the Shanghai hardfork), you should set `evm_version = "paris"` in your `foundry.toml`, since the default EVM version of solc was advanced from Paris to Shanghai as part of solc's `0.8.20` release. +**Testing** + +It is strongly recommended that you run the forge test suite of this SDK with your own compiler version to catch potential errors that stem from differences in compiler versions early. Yes, strictly speaking the Solidity version pragma should prevent these issues, but better to be safe than sorry, especially given that some components make extensive use of inline assembly. + +**IERC20 Remapping** + +This SDK comes with its own IERC20 interface. Given that projects tend to combine different SDKs, there's often this annoying issue of clashes of IERC20 interfaces, even though the are effectively the same. We handle this issue by importing `IERC20/IERC20.sol` which allows remapping the `IERC20/` prefix to whatever directory contains `IERC20.sol` in your project, thus providing an override mechanism that should allow dealing with this problem seamlessly until forge allows remapping of individual files. + ## Philosophy/Creeds In This House We Believe: diff --git a/foundry.toml b/foundry.toml index 886ebb4..69d38c1 100644 --- a/foundry.toml +++ b/foundry.toml @@ -6,10 +6,16 @@ out = "out" libs = ["lib"] via_ir = true +#currently, forge does not allow remapping of individual files +# (see here: https://github.com/foundry-rs/foundry/issues/7527#issuecomment-2269444829) +#so for now we are using the IERC20 prefix as a workaround that allows users +# to override the IERC20 interface with whatever they use in their project +# (well, as long as their file is also called IERC20.sol and is interface compatible) remappings = [ "ds-test/=lib/forge-std/lib/ds-test/src/", "forge-std/=lib/forge-std/src/", "wormhole-sdk/=src/", + "IERC20/=src/interfaces/token/", ] # See more config options https://github.com/foundry-rs/foundry/tree/master/config diff --git a/src/WormholeRelayer/CCTPAndTokenBase.sol b/src/WormholeRelayer/CCTPAndTokenBase.sol index 92a7920..0f9312f 100644 --- a/src/WormholeRelayer/CCTPAndTokenBase.sol +++ b/src/WormholeRelayer/CCTPAndTokenBase.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: Apache 2 pragma solidity ^0.8.19; +import "IERC20/IERC20.sol"; import "wormhole-sdk/interfaces/IWormholeReceiver.sol"; import "wormhole-sdk/interfaces/IWormholeRelayer.sol"; import "wormhole-sdk/interfaces/ITokenBridge.sol"; -import "wormhole-sdk/interfaces/token/IERC20.sol"; import "wormhole-sdk/interfaces/cctp/ITokenMessenger.sol"; import "wormhole-sdk/interfaces/cctp/IMessageTransmitter.sol"; import "wormhole-sdk/Utils.sol"; diff --git a/src/WormholeRelayer/CCTPBase.sol b/src/WormholeRelayer/CCTPBase.sol index 856e7c8..44e76b8 100644 --- a/src/WormholeRelayer/CCTPBase.sol +++ b/src/WormholeRelayer/CCTPBase.sol @@ -1,9 +1,9 @@ // SPDX-License-Identifier: Apache 2 pragma solidity ^0.8.19; +import "IERC20/IERC20.sol"; import "wormhole-sdk/interfaces/IWormholeReceiver.sol"; import "wormhole-sdk/interfaces/IWormholeRelayer.sol"; -import "wormhole-sdk/interfaces/token/IERC20.sol"; import "wormhole-sdk/interfaces/cctp/ITokenMessenger.sol"; import "wormhole-sdk/interfaces/cctp/IMessageTransmitter.sol"; import "wormhole-sdk/Utils.sol"; diff --git a/src/WormholeRelayer/TokenBase.sol b/src/WormholeRelayer/TokenBase.sol index ac90738..ff0edd3 100644 --- a/src/WormholeRelayer/TokenBase.sol +++ b/src/WormholeRelayer/TokenBase.sol @@ -1,10 +1,10 @@ // SPDX-License-Identifier: Apache 2 pragma solidity ^0.8.19; +import "IERC20/IERC20.sol"; import "wormhole-sdk/interfaces/IWormholeReceiver.sol"; import "wormhole-sdk/interfaces/IWormholeRelayer.sol"; import "wormhole-sdk/interfaces/ITokenBridge.sol"; -import "wormhole-sdk/interfaces/token/IERC20.sol"; import "wormhole-sdk/Utils.sol"; import {Base} from "./Base.sol"; diff --git a/src/interfaces/ITokenBridge.sol b/src/interfaces/ITokenBridge.sol index 281d4d6..675b3e6 100644 --- a/src/interfaces/ITokenBridge.sol +++ b/src/interfaces/ITokenBridge.sol @@ -3,7 +3,6 @@ pragma solidity ^0.8.0; -import "./token/IWETH.sol"; import "./IWormhole.sol"; interface ITokenBridge { @@ -149,7 +148,7 @@ interface ITokenBridge { function tokenImplementation() external view returns (address); - function WETH() external view returns (IWETH); + function WETH() external view returns (address); function outstandingBridged(address token) external view returns (uint256); diff --git a/src/interfaces/cctp/IMessageTransmitter.sol b/src/interfaces/cctp/IMessageTransmitter.sol index c1a12b3..5f9bdc2 100644 --- a/src/interfaces/cctp/IMessageTransmitter.sol +++ b/src/interfaces/cctp/IMessageTransmitter.sol @@ -1,27 +1,76 @@ -/* - * 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. - */ +// SPDX-License-Identifier: Apache 2 +// Copyright (c) 2022, Circle Internet Financial Limited. +// +// stripped, flattened version of: +// https://github.com/circlefin/evm-cctp-contracts/blob/master/src/MessageTransmitter.sol + pragma solidity ^0.8.0; -import "./IRelayer.sol"; -import "./IReceiver.sol"; +import {IOwnable2Step} from "./shared/IOwnable2Step.sol"; +import {IPausable} from "./shared/IPausable.sol"; + +interface IAttestable { + event AttesterEnabled(address indexed attester); + event AttesterDisabled(address indexed attester); + + event SignatureThresholdUpdated(uint256 oldSignatureThreshold, uint256 newSignatureThreshold); + event AttesterManagerUpdated( + address indexed previousAttesterManager, + address indexed newAttesterManager + ); + + function attesterManager() external view returns (address); + function isEnabledAttester(address attester) external view returns (bool); + function getNumEnabledAttesters() external view returns (uint256); + function getEnabledAttester(uint256 index) external view returns (address); + + function updateAttesterManager(address newAttesterManager) external; + function setSignatureThreshold(uint256 newSignatureThreshold) external; + function enableAttester(address attester) external; + function disableAttester(address attester) external; +} + +interface IMessageTransmitter is IAttestable, IPausable, IOwnable2Step { + event MessageSent(bytes message); + + event MessageReceived( + address indexed caller, + uint32 sourceDomain, + uint64 indexed nonce, + bytes32 sender, + bytes messageBody + ); + + function localDomain() external view returns (uint32); + function version() external view returns (uint32); + function maxMessageBodySize() external view returns (uint256); + function nextAvailableNonce() external view returns (uint64); + function usedNonces(bytes32 nonce) external view returns (bool); + + function sendMessage( + uint32 destinationDomain, + bytes32 recipient, + bytes calldata messageBody + ) external returns (uint64); + + function sendMessageWithCaller( + uint32 destinationDomain, + bytes32 recipient, + bytes32 destinationCaller, + bytes calldata messageBody + ) external returns (uint64); + + function replaceMessage( + bytes calldata originalMessage, + bytes calldata originalAttestation, + bytes calldata newMessageBody, + bytes32 newDestinationCaller + ) external; -/** - * @title IMessageTransmitter - * @notice Interface for message transmitters, which both relay and receive messages. - */ -interface IMessageTransmitter is IRelayer, IReceiver { + function receiveMessage( + bytes calldata message, + bytes calldata attestation + ) external returns (bool success); + function setMaxMessageBodySize(uint256 newMaxMessageBodySize) external; } diff --git a/src/interfaces/cctp/IReceiver.sol b/src/interfaces/cctp/IReceiver.sol deleted file mode 100644 index ff8821d..0000000 --- a/src/interfaces/cctp/IReceiver.sol +++ /dev/null @@ -1,33 +0,0 @@ -/* - * 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/cctp/IRelayer.sol b/src/interfaces/cctp/IRelayer.sol deleted file mode 100644 index a7f50d5..0000000 --- a/src/interfaces/cctp/IRelayer.sol +++ /dev/null @@ -1,71 +0,0 @@ -/* - * 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/cctp/ITokenMessenger.sol b/src/interfaces/cctp/ITokenMessenger.sol index 832e4f8..4940dbe 100644 --- a/src/interfaces/cctp/ITokenMessenger.sol +++ b/src/interfaces/cctp/ITokenMessenger.sol @@ -1,53 +1,72 @@ -/* - * 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. - */ +// SPDX-License-Identifier: Apache 2 +// Copyright (c) 2022, Circle Internet Financial Limited. +// +// stripped version of: +// https://github.com/circlefin/evm-cctp-contracts/blob/master/src/MessageTransmitter.sol + pragma solidity ^0.8.0; -/** - * @title TokenMessenger - * @notice Sends messages and receives messages to/from MessageTransmitters - * and to/from TokenMinters - */ -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); -} +import {IOwnable2Step} from "./shared/IOwnable2Step.sol"; + +import {IMessageTransmitter} from "./IMessageTransmitter.sol"; +import {ITokenMinter} from "./ITokenMinter.sol"; + +interface ITokenMessenger is IOwnable2Step { + event DepositForBurn( + uint64 indexed nonce, + address indexed burnToken, + uint256 amount, + address indexed depositor, + bytes32 mintRecipient, + uint32 destinationDomain, + bytes32 destinationTokenMessenger, + bytes32 destinationCaller + ); + + event MintAndWithdraw(address indexed mintRecipient, uint256 amount, address indexed mintToken); + + event RemoteTokenMessengerAdded(uint32 domain, bytes32 tokenMessenger); + event RemoteTokenMessengerRemoved(uint32 domain, bytes32 tokenMessenger); + + event LocalMinterAdded(address localMinter); + event LocalMinterRemoved(address localMinter); + + function messageBodyVersion() external view returns (uint32); + function localMessageTransmitter() external view returns (IMessageTransmitter); + function localMinter() external view returns (ITokenMinter); + function remoteTokenMessengers(uint32 domain) external view returns (bytes32); + + function depositForBurn( + uint256 amount, + uint32 destinationDomain, + bytes32 mintRecipient, + address burnToken + ) external returns (uint64 nonce); + + function depositForBurnWithCaller( + uint256 amount, + uint32 destinationDomain, + bytes32 mintRecipient, + address burnToken, + bytes32 destinationCaller + ) external returns (uint64 nonce); + + function replaceDepositForBurn( + bytes calldata originalMessage, + bytes calldata originalAttestation, + bytes32 newDestinationCaller, + bytes32 newMintRecipient + ) external; + + function handleReceiveMessage( + uint32 remoteDomain, + bytes32 sender, + bytes calldata messageBody + ) external returns (bool); + + function addRemoteTokenMessenger(uint32 domain, bytes32 tokenMessenger) external; + function removeRemoteTokenMessenger(uint32 domain) external; + + function addLocalMinter(address newLocalMinter) external; + function removeLocalMinter() external; +} \ No newline at end of file diff --git a/src/interfaces/cctp/ITokenMinter.sol b/src/interfaces/cctp/ITokenMinter.sol new file mode 100644 index 0000000..219dc05 --- /dev/null +++ b/src/interfaces/cctp/ITokenMinter.sol @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: Apache 2 +// Copyright (c) 2022, Circle Internet Financial Limited. +//stripped version of: +//https://github.com/circlefin/evm-cctp-contracts/blob/master/src/interfaces/ITokenMinter.sol + +pragma solidity ^0.8.0; + +import {IOwnable2Step} from "./shared/IOwnable2Step.sol"; +import {IPausable} from "./shared/IPausable.sol"; + +interface ITokenController { + event TokenPairLinked(address localToken, uint32 remoteDomain, bytes32 remoteToken); + event TokenPairUnlinked(address localToken, uint32 remoteDomain, bytes32 remoteToken); + + event SetBurnLimitPerMessage(address indexed token, uint256 burnLimitPerMessage); + event SetTokenController(address tokenController); + + function burnLimitsPerMessage(address token) external view returns (uint256); + function remoteTokensToLocalTokens(bytes32 sourceIdHash) external view returns (address); + function tokenController() external view returns (address); + + function linkTokenPair(address localToken, uint32 remoteDomain, bytes32 remoteToken) external; + function unlinkTokenPair(address localToken, uint32 remoteDomain, bytes32 remoteToken) external; + function setMaxBurnAmountPerMessage(address localToken, uint256 burnLimitPerMessage) external; +} + +interface ITokenMinter is ITokenController, IPausable, IOwnable2Step { + event LocalTokenMessengerAdded(address localTokenMessenger); + event LocalTokenMessengerRemoved(address localTokenMessenger); + + function localTokenMessenger() external view returns (address); + function getLocalToken(uint32 remoteDomain, bytes32 remoteToken) external view returns (address); + + function mint(uint32 sourceDomain, bytes32 burnToken, address to, uint256 amount) external; + function burn(address burnToken, uint256 burnAmount) external; + + function addLocalTokenMessenger(address newLocalTokenMessenger) external; + function removeLocalTokenMessenger() external; + function setTokenController(address newTokenController) external; +} diff --git a/src/interfaces/cctp/shared/IOwnable2Step.sol b/src/interfaces/cctp/shared/IOwnable2Step.sol new file mode 100644 index 0000000..a3aa0a0 --- /dev/null +++ b/src/interfaces/cctp/shared/IOwnable2Step.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: Apache 2 +// Copyright (c) 2022, Circle Internet Financial Limited. +// +// stripped, flattened version of: +// https://github.com/circlefin/evm-cctp-contracts/blob/master/src/roles/Ownable2Step.sol + +pragma solidity ^0.8.0; + +interface IOwnable2Step { + event OwnershipTransferStarted(address indexed previousOwner, address indexed newOwner); + event OwnershipTransferred(address indexed previousOwner, address indexed newOwner); + + function transferOwnership(address newOwner) external; + function acceptOwnership() external; + + function owner() external view returns (address); + function pendingOwner() external view returns (address); +} diff --git a/src/interfaces/cctp/shared/IPausable.sol b/src/interfaces/cctp/shared/IPausable.sol new file mode 100644 index 0000000..2b34a10 --- /dev/null +++ b/src/interfaces/cctp/shared/IPausable.sol @@ -0,0 +1,20 @@ +// SPDX-License-Identifier: Apache 2 +// Copyright (c) 2022, Circle Internet Financial Limited. +// +// stripped version of: +// https://github.com/circlefin/evm-cctp-contracts/blob/master/src/roles/Pausable.sol + +pragma solidity ^0.8.0; + +interface IPausable { + event Pause(); + event Unpause(); + event PauserChanged(address indexed newAddress); + + function paused() external view returns (bool); + function pauser() external view returns (address); + + function pause() external; + function unpause() external; + function updatePauser(address newPauser) external; +} diff --git a/src/interfaces/token/IERC20.sol b/src/interfaces/token/IERC20.sol index 66c4e4d..0dd84dd 100644 --- a/src/interfaces/token/IERC20.sol +++ b/src/interfaces/token/IERC20.sol @@ -1,78 +1,17 @@ -// SPDX-License-Identifier: MIT -// OpenZeppelin Contracts (last updated v4.6.0) (token/ERC20/IERC20.sol) +// SPDX-License-Identifier: Apache 2 pragma solidity ^0.8.0; -/** - * @dev Interface of the ERC20 standard as defined in the EIP. - */ +//https://eips.ethereum.org/EIPS/eip-20 interface IERC20 { - /** - * @dev Emitted when `value` tokens are moved from one account (`from`) to - * another (`to`). - * - * Note that `value` may be zero. - */ - event Transfer(address indexed from, address indexed to, uint256 value); + event Transfer(address indexed from, address indexed to, uint256 value); + event Approval(address indexed owner, address indexed spender, uint256 value); - /** - * @dev Emitted when the allowance of a `spender` for an `owner` is set by - * a call to {approve}. `value` is the new allowance. - */ - event Approval(address indexed owner, address indexed spender, uint256 value); + function totalSupply() external view returns (uint256); + function balanceOf(address account) external view returns (uint256); + function allowance(address owner, address spender) external view returns (uint256); - /** - * @dev Returns the amount of tokens in existence. - */ - function totalSupply() external view returns (uint256); - - /** - * @dev Returns the amount of tokens owned by `account`. - */ - function balanceOf(address account) external view returns (uint256); - - /** - * @dev Moves `amount` tokens from the caller's account to `to`. - * - * Returns a boolean value indicating whether the operation succeeded. - * - * Emits a {Transfer} event. - */ - function transfer(address to, uint256 amount) external returns (bool); - - /** - * @dev Returns the remaining number of tokens that `spender` will be - * allowed to spend on behalf of `owner` through {transferFrom}. This is - * zero by default. - * - * This value changes when {approve} or {transferFrom} are called. - */ - function allowance(address owner, address spender) external view returns (uint256); - - /** - * @dev Sets `amount` as the allowance of `spender` over the caller's tokens. - * - * Returns a boolean value indicating whether the operation succeeded. - * - * IMPORTANT: Beware that changing an allowance with this method brings the risk - * that someone may use both the old and the new allowance by unfortunate - * transaction ordering. One possible solution to mitigate this race - * condition is to first reduce the spender's allowance to 0 and set the - * desired value afterwards: - * https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729 - * - * Emits an {Approval} event. - */ - function approve(address spender, uint256 amount) external returns (bool); - - /** - * @dev Moves `amount` tokens from `from` to `to` using the - * allowance mechanism. `amount` is then deducted from the caller's - * allowance. - * - * Returns a boolean value indicating whether the operation succeeded. - * - * Emits a {Transfer} event. - */ - function transferFrom(address from, address to, uint256 amount) external returns (bool); + function transfer(address to, uint256 amount) external returns (bool); + function approve(address spender, uint256 amount) external returns (bool); + function transferFrom(address from, address to, uint256 amount) external returns (bool); } diff --git a/src/interfaces/token/IUSDC.sol b/src/interfaces/token/IUSDC.sol deleted file mode 100644 index f94debe..0000000 --- a/src/interfaces/token/IUSDC.sol +++ /dev/null @@ -1,17 +0,0 @@ -// SPDX-License-Identifier: Apache 2 - -pragma solidity ^0.8.0; - -import "./IERC20.sol"; - -interface IUSDC is IERC20 { - function mint(address to, uint256 amount) external; - - function configureMinter(address minter, uint256 minterAllowedAmount) external; - - function masterMinter() external view returns (address); - - function owner() external view returns (address); - - function blacklister() external view returns (address); -} diff --git a/src/interfaces/token/IWETH.sol b/src/interfaces/token/IWETH.sol index e3c9c8d..51b8530 100644 --- a/src/interfaces/token/IWETH.sol +++ b/src/interfaces/token/IWETH.sol @@ -2,9 +2,9 @@ pragma solidity ^0.8.0; -import "./IERC20.sol"; +import "IERC20/IERC20.sol"; interface IWETH is IERC20 { - function deposit() external payable; - function withdraw(uint256 amount) external; + function deposit() external payable; + function withdraw(uint256 amount) external; } diff --git a/src/libraries/WormholeCctpMessages.sol b/src/libraries/WormholeCctpMessages.sol new file mode 100644 index 0000000..ef4841d --- /dev/null +++ b/src/libraries/WormholeCctpMessages.sol @@ -0,0 +1,200 @@ +// SPDX-License-Identifier: Apache 2 +pragma solidity ^0.8.19; + +import {IWormhole} from "wormhole-sdk/interfaces/IWormhole.sol"; +import {BytesParsing} from "wormhole-sdk/libraries/BytesParsing.sol"; +import {toUniversalAddress} from "wormhole-sdk/Utils.sol"; + +library WormholeCctpMessages { + using { toUniversalAddress } for address; + using BytesParsing for bytes; + + // Payload IDs. + // + // NOTE: This library reserves payloads 1 through 10 for future use. When using this library, + // please consider starting your own Wormhole message payloads at 11. + uint8 private constant DEPOSIT = 1; + uint8 private constant RESERVED_2 = 2; + uint8 private constant RESERVED_3 = 3; + uint8 private constant RESERVED_4 = 4; + uint8 private constant RESERVED_5 = 5; + uint8 private constant RESERVED_6 = 6; + uint8 private constant RESERVED_7 = 7; + uint8 private constant RESERVED_8 = 8; + uint8 private constant RESERVED_9 = 9; + uint8 private constant RESERVED_10 = 10; + + error MissingPayload(); + error PayloadTooLarge(uint256); + error InvalidMessage(); + + /** + * @dev NOTE: This method encodes the Wormhole message payload assuming the payload ID == 1. + */ + function encodeDeposit( + address token, + uint256 amount, + uint32 sourceCctpDomain, + uint32 targetCctpDomain, + uint64 cctpNonce, + bytes32 burnSource, + bytes32 mintRecipient, + bytes memory payload + ) internal pure returns (bytes memory encoded) { + encoded = encodeDeposit( + token.toUniversalAddress(), + DEPOSIT, + amount, + sourceCctpDomain, + targetCctpDomain, + cctpNonce, + burnSource, + mintRecipient, + payload + ); + } + + /** + * @dev NOTE: This method encodes the Wormhole message payload assuming the payload ID == 1. + */ + function encodeDeposit( + bytes32 universalTokenAddress, + uint256 amount, + uint32 sourceCctpDomain, + uint32 targetCctpDomain, + uint64 cctpNonce, + bytes32 burnSource, + bytes32 mintRecipient, + bytes memory payload + ) internal pure returns (bytes memory encoded) { + encoded = encodeDeposit( + universalTokenAddress, + DEPOSIT, + amount, + sourceCctpDomain, + targetCctpDomain, + cctpNonce, + burnSource, + mintRecipient, + payload + ); + } + + function encodeDeposit( + address token, + uint8 payloadId, + uint256 amount, + uint32 sourceCctpDomain, + uint32 targetCctpDomain, + uint64 cctpNonce, + bytes32 burnSource, + bytes32 mintRecipient, + bytes memory payload + ) internal pure returns (bytes memory encoded) { + encoded = encodeDeposit( + token.toUniversalAddress(), + payloadId, + amount, + sourceCctpDomain, + targetCctpDomain, + cctpNonce, + burnSource, + mintRecipient, + payload + ); + } + + function encodeDeposit( + bytes32 universalTokenAddress, + uint8 payloadId, + uint256 amount, + uint32 sourceCctpDomain, + uint32 targetCctpDomain, + uint64 cctpNonce, + bytes32 burnSource, + bytes32 mintRecipient, + bytes memory payload + ) internal pure returns (bytes memory encoded) { + uint256 payloadLen = payload.length; + if (payloadLen == 0) { + revert MissingPayload(); + } else if (payloadLen > type(uint16).max) { + revert PayloadTooLarge(payloadLen); + } + + encoded = abi.encodePacked( + payloadId, + universalTokenAddress, + amount, + sourceCctpDomain, + targetCctpDomain, + cctpNonce, + burnSource, + mintRecipient, + uint16(payloadLen), + payload + ); + } + + // left in for backwards compatibility + function decodeDeposit(IWormhole.VM memory vaa) + internal + pure + returns ( + bytes32 token, + uint256 amount, + uint32 sourceCctpDomain, + uint32 targetCctpDomain, + uint64 cctpNonce, + bytes32 burnSource, + bytes32 mintRecipient, + bytes memory payload + ) + { + return decodeDeposit(vaa.payload); + } + + function decodeDeposit(bytes memory encoded) + internal + pure + returns ( + bytes32 token, + uint256 amount, + uint32 sourceCctpDomain, + uint32 targetCctpDomain, + uint64 cctpNonce, + bytes32 burnSource, + bytes32 mintRecipient, + bytes memory payload + ) + { + uint256 offset = _checkPayloadId(encoded, 0, DEPOSIT); + + (token, offset) = encoded.asBytes32Unchecked(offset); + (amount, offset) = encoded.asUint256Unchecked(offset); + (sourceCctpDomain, offset) = encoded.asUint32Unchecked(offset); + (targetCctpDomain, offset) = encoded.asUint32Unchecked(offset); + (cctpNonce, offset) = encoded.asUint64Unchecked(offset); + (burnSource, offset) = encoded.asBytes32Unchecked(offset); + (mintRecipient, offset) = encoded.asBytes32Unchecked(offset); + uint16 payloadLength; + (payloadLength, offset) = encoded.asUint16Unchecked(offset); + (payload, offset) = encoded.sliceUnchecked(offset, payloadLength); + + encoded.checkLength(offset); + } + + // ---------------------------------------- private ------------------------------------------- + + function _checkPayloadId( + bytes memory encoded, + uint256 startOffset, + uint8 expectedPayloadId + ) private pure returns (uint256 offset) { + uint8 parsedPayloadId; + (parsedPayloadId, offset) = encoded.asUint8Unchecked(startOffset); + + if (parsedPayloadId != expectedPayloadId) + revert InvalidMessage(); + } +} diff --git a/src/testing/CctpMessages.sol b/src/testing/CctpMessages.sol new file mode 100644 index 0000000..ba2f36a --- /dev/null +++ b/src/testing/CctpMessages.sol @@ -0,0 +1,145 @@ +// SPDX-License-Identifier: Apache 2 +pragma solidity ^0.8.19; + +import {BytesParsing} from "wormhole-sdk/libraries/BytesParsing.sol"; + +//Message format emitted by Circle MessageTransmitter - akin to Wormhole CoreBridge +// see: https://github.com/circlefin/evm-cctp-contracts/blob/master/src/messages/Message.sol +// +//Unlike the Wormhole CoreBridge which broadcasts, Circle Messages always have an intended +// destination and recipient. +// +//Cctp messages are "redeemed" by calling receiveMessage() on the Circle Message Transmitter +// which in turn invokes handleReceiveMessage() on the recipient of the message: +// see: https://github.com/circlefin/evm-cctp-contracts/blob/adb2a382b09ea574f4d18d8af5b6706e8ed9b8f2/src/MessageTransmitter.sol#L294-L295 +//So even messages that originate from the TokenMessenger are first sent to the MessageTransmitter +// whereas Wormhole TokenBridge messages must be redeemed with the TokenBridge, which internally +// verifies the veracity of the VAA with the CoreBridge. +//To provide a similar restriction like the TokenBridge's redeemWithPayload() function which can +// only be called by the recipient of the TokenBridge transferWithPayload message, Circle provides +// an additional, optional field named destinationCaller which must be the caller of +// receiveMessage() when it has been specified (i.e. the field is != 0). +struct CctpHeader { + //uint32 headerVersion; + uint32 sourceDomain; + uint32 destinationDomain; + uint64 nonce; + //caller of the Circle Message Transmitter -> for us always the foreign TokenMessenger + bytes32 sender; + //caller of the Circle Message Transmitter -> for us always the local TokenMessenger + bytes32 recipient; + bytes32 destinationCaller; +} + +struct CctpMessage { + CctpHeader header; + bytes messageBody; +} + +struct CctpTokenBurnMessage { + CctpHeader header; + //uint32 bodyVersion; + //the address of the USDC contract on the foreign domain whose tokens were burned + bytes32 burnToken; + //always our local WormholeCctpTokenMessenger contract (e.g. CircleIntegration, TokenRouter)a + bytes32 mintRecipient; + uint256 amount; + //address of caller of depositAndBurn on the foreign chain - for us always foreignCaller + bytes32 messageSender; +} + +library CctpMessages { + using BytesParsing for bytes; + + uint private constant _CCTP_HEADER_SIZE = 3*4 + 8 + 3*32; + uint private constant _CCTP_TOKEN_BURN_MESSAGE_SIZE = _CCTP_HEADER_SIZE + 4 + 4*32; + + //returned by MessageTransmitter.version() - see here: + //https://github.com/circlefin/evm-cctp-contracts/blob/1662356f9e60bb3f18cb6d09f95f628f0cc3637f/src/MessageTransmitter.sol#L238 + uint32 constant MESSAGE_TRANSMITTER_HEADER_VERSION = 0; + + //returned by TokenMessenger.messageBodyVersion() - see here: + //https://github.com/circlefin/evm-cctp-contracts/blob/1662356f9e60bb3f18cb6d09f95f628f0cc3637f/src/TokenMessenger.sol#L107 + uint32 constant TOKEN_MESSENGER_BODY_VERSION = 0; + + function encode(CctpHeader memory header) internal pure returns (bytes memory) { + return abi.encodePacked( + MESSAGE_TRANSMITTER_HEADER_VERSION, + header.sourceDomain, + header.destinationDomain, + header.nonce, + header.sender, + header.recipient, + header.destinationCaller + ); + } + + function encode(CctpMessage memory message) internal pure returns (bytes memory) { + return abi.encodePacked( + encode(message.header), + message.messageBody + ); + } + + function encode(CctpTokenBurnMessage memory burnMsg) internal pure returns (bytes memory) { + return abi.encodePacked( + encode(burnMsg.header), + TOKEN_MESSENGER_BODY_VERSION, + burnMsg.burnToken, + burnMsg.mintRecipient, + burnMsg.amount, + burnMsg.messageSender + ); + } + + function isCctpTokenBurnMessage(bytes memory encoded) internal pure returns (bool) { + if (encoded.length != _CCTP_TOKEN_BURN_MESSAGE_SIZE) + return false; + + (uint headerVersion,) = encoded.asUint32Unchecked(0); + (uint bodyVersion, ) = encoded.asUint32Unchecked(_CCTP_HEADER_SIZE); + return headerVersion == MESSAGE_TRANSMITTER_HEADER_VERSION && + bodyVersion == TOKEN_MESSENGER_BODY_VERSION; + } + + function decodeCctpHeader( + bytes memory encoded + ) internal pure returns (CctpHeader memory ret) { + uint offset; + uint32 version; + (version, offset) = encoded.asUint32Unchecked(offset); + require(version == MESSAGE_TRANSMITTER_HEADER_VERSION, "cctp msg header version mismatch"); + (ret.sourceDomain, offset) = encoded.asUint32Unchecked(offset); + (ret.destinationDomain, offset) = encoded.asUint32Unchecked(offset); + (ret.nonce, offset) = encoded.asUint64Unchecked(offset); + (ret.sender, offset) = encoded.asBytes32Unchecked(offset); + (ret.recipient, offset) = encoded.asBytes32Unchecked(offset); + (ret.destinationCaller, offset) = encoded.asBytes32Unchecked(offset); + encoded.checkLength(offset); + } + + function decodeCctpMessage( + bytes memory encoded + ) internal pure returns (CctpMessage memory ret) { + (bytes memory encHeader, uint offset) = encoded.sliceUnchecked(0, _CCTP_HEADER_SIZE); + ret.header = decodeCctpHeader(encHeader); + (ret.messageBody, offset) = encoded.slice(offset, encoded.length - offset); //checked! + return ret; + } + + function decodeCctpTokenBurnMessage( + bytes memory encoded + ) internal pure returns (CctpTokenBurnMessage memory ret) { + (bytes memory encHeader, uint offset) = encoded.sliceUnchecked(0, _CCTP_HEADER_SIZE); + ret.header = decodeCctpHeader(encHeader); + uint32 version; + (version, offset) = encoded.asUint32Unchecked(offset); + require(version == TOKEN_MESSENGER_BODY_VERSION, "cctp msg body version mismatch"); + (ret.burnToken, offset) = encoded.asBytes32Unchecked(offset); + (ret.mintRecipient, offset) = encoded.asBytes32Unchecked(offset); + (ret.amount, offset) = encoded.asUint256Unchecked(offset); + (ret.messageSender, offset) = encoded.asBytes32Unchecked(offset); + encoded.checkLength(offset); + return ret; + } +} \ No newline at end of file diff --git a/src/testing/CctpOverride.sol b/src/testing/CctpOverride.sol new file mode 100644 index 0000000..8bf1c98 --- /dev/null +++ b/src/testing/CctpOverride.sol @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: Apache 2 +pragma solidity ^0.8.19; + +import "forge-std/Vm.sol"; + +import {IMessageTransmitter} from "wormhole-sdk/interfaces/cctp/IMessageTransmitter.sol"; +import {VM_ADDRESS, DEVNET_GUARDIAN_PRIVATE_KEY} from "./Constants.sol"; +import "./CctpMessages.sol"; +import "./LogUtils.sol"; + +//create fake CCTP attestations for forge tests +library CctpOverride { + using CctpMessages for CctpTokenBurnMessage; + using CctpMessages for bytes; + using LogUtils for Vm.Log[]; + + Vm constant vm = Vm(VM_ADDRESS); + + // keccak256("attesterPrivateKey") - 1 + bytes32 private constant _ATTESTER_PK_SLOT = + 0xb60bdf9c1f1404b33ce5538637aeb77ae8bc4e523cec04106ff4fbe1df885bf2; + + function setUpOverride(IMessageTransmitter messageTransmitter) internal { + setUpOverride(messageTransmitter, DEVNET_GUARDIAN_PRIVATE_KEY); + } + + function setUpOverride(IMessageTransmitter messageTransmitter, uint256 signer) internal { + if (attesterPrivateKey(messageTransmitter) == signer) + return; + + require(attesterPrivateKey(messageTransmitter) == 0, "CctpOverride: already set up"); + + require(messageTransmitter.version() == CctpMessages.MESSAGE_TRANSMITTER_HEADER_VERSION); + + //as pioneered in WormholeOverride + vm.store(address(messageTransmitter), _ATTESTER_PK_SLOT, bytes32(signer)); + + //usurp power + vm.startPrank(messageTransmitter.attesterManager()); + messageTransmitter.setSignatureThreshold(1); + messageTransmitter.enableAttester(vm.addr(attesterPrivateKey(messageTransmitter))); + vm.stopPrank(); + } + + function attesterPrivateKey( + IMessageTransmitter messageTransmitter + ) internal view returns (uint256 pk) { + pk = uint256(vm.load(address(messageTransmitter), _ATTESTER_PK_SLOT)); + } + + //we only care about burn msgs, hence we don't implement a more generic sign() and fetch() + + function sign( + IMessageTransmitter messageTransmitter, + CctpTokenBurnMessage memory message + ) internal view returns (bytes memory signature) { + (uint8 v, bytes32 r, bytes32 s) = + vm.sign(attesterPrivateKey(messageTransmitter), keccak256(message.encode())); + return abi.encodePacked(r, s, v); + } + + function fetchBurnMessages( + IMessageTransmitter messageTransmitter, + Vm.Log[] memory logs + ) internal pure returns (CctpTokenBurnMessage[] memory ret) { unchecked { + Vm.Log[] memory encodedBurnLogs = logs.filter( + address(messageTransmitter), + keccak256("MessageSent(bytes)"), + _isLoggedTokenBurnMessage + ); + + ret = new CctpTokenBurnMessage[](encodedBurnLogs.length); + for (uint i; i < encodedBurnLogs.length; ++i) + ret[i++] = _logDataToActualBytes(encodedBurnLogs[i].data).decodeCctpTokenBurnMessage(); + }} + + function _logDataToActualBytes(bytes memory data) private pure returns (bytes memory) { + return abi.decode(data, (bytes)); + } + + function _isLoggedTokenBurnMessage(bytes memory data) private pure returns (bool) { + return CctpMessages.isCctpTokenBurnMessage(_logDataToActualBytes(data)); + } +} diff --git a/src/testing/Constants.sol b/src/testing/Constants.sol new file mode 100644 index 0000000..ae9e220 --- /dev/null +++ b/src/testing/Constants.sol @@ -0,0 +1,9 @@ +// SPDX-License-Identifier: Apache 2 +pragma solidity ^0.8.19; + +address constant VM_ADDRESS = address(bytes20(uint160(uint256(keccak256("hevm cheat code"))))); + +uint256 constant DEVNET_GUARDIAN_PRIVATE_KEY = + 0xcfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0; +//corresponding guardian address: 0xbeFA429d57cD18b7F8A4d91A2da9AB4AF05d0FBe +//should be programmatically recovered via vm.addr(DEVNET_GUARDIAN_PRIVATE_KEY); \ No newline at end of file diff --git a/src/testing/ERC20Mock.sol b/src/testing/ERC20Mock.sol index a3d54a2..d7ee38e 100644 --- a/src/testing/ERC20Mock.sol +++ b/src/testing/ERC20Mock.sol @@ -1,7 +1,7 @@ pragma solidity ^0.8.19; -import "wormhole-sdk/interfaces/token/IERC20.sol"; +import "IERC20/IERC20.sol"; /* * ERC20 impl from solmate diff --git a/src/testing/LogUtils.sol b/src/testing/LogUtils.sol new file mode 100644 index 0000000..add3d67 --- /dev/null +++ b/src/testing/LogUtils.sol @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: Apache 2 +pragma solidity ^0.8.19; + +import {Vm} from "forge-std/Vm.sol"; + +library LogUtils { + function filter( + Vm.Log[] memory logs, + address emitter + ) internal pure returns (Vm.Log[] memory) { + return filter(logs, emitter, bytes32(0), _noDataFilter); + } + + function filter( + Vm.Log[] memory logs, + bytes32 topic + ) internal pure returns (Vm.Log[] memory) { + return filter(logs, address(0), topic, _noDataFilter); + } + + function filter( + Vm.Log[] memory logs, + address emitter, + bytes32 topic + ) internal pure returns (Vm.Log[] memory) { + return filter(logs, emitter, topic, _noDataFilter); + } + + function filter( + Vm.Log[] memory logs, + address emitter, + bytes32 topic, + function(bytes memory) pure returns (bool) dataFilter + ) internal pure returns (Vm.Log[] memory ret) { unchecked { + ret = new Vm.Log[](logs.length); + uint count; + for (uint i; i < logs.length; ++i) + if ((topic == bytes32(0) || logs[i].topics[0] == topic) && + (emitter == address(0) || logs[i].emitter == emitter) && + dataFilter(logs[i].data)) + ret[count++] = logs[i]; + + //trim length + assembly { mstore(ret, count) } + }} + + function _noDataFilter(bytes memory) private pure returns (bool) { + return true; + } +} diff --git a/src/testing/UsdcDealer.sol b/src/testing/UsdcDealer.sol new file mode 100644 index 0000000..4611db4 --- /dev/null +++ b/src/testing/UsdcDealer.sol @@ -0,0 +1,26 @@ +// SPDX-License-Identifier: Apache 2 +pragma solidity ^0.8.19; + +import {Vm} from "forge-std/Vm.sol"; + +import "IERC20/IERC20.sol"; +import {VM_ADDRESS} from "./Constants.sol"; + +interface IUSDC is IERC20 { + function masterMinter() external view returns (address); + + function mint(address to, uint256 amount) external; + function configureMinter(address minter, uint256 minterAllowedAmount) external; +} + +//for some reason, using forge's `deal()` to mint usdc does not work reliably +// hence this workaround +library UsdcDealer { + Vm constant vm = Vm(VM_ADDRESS); + + function deal(IUSDC usdc, address to, uint256 amount) internal { + vm.prank(usdc.masterMinter()); + usdc.configureMinter(address(this), amount); + usdc.mint(address(to), amount); + } +} diff --git a/src/testing/WormholeCctpSimulator.sol b/src/testing/WormholeCctpSimulator.sol new file mode 100644 index 0000000..b9fb016 --- /dev/null +++ b/src/testing/WormholeCctpSimulator.sol @@ -0,0 +1,176 @@ +// SPDX-License-Identifier: Apache 2 +pragma solidity ^0.8.19; + +//import "forge-std/Test.sol"; +import {Vm} from "forge-std/Vm.sol"; + +import "wormhole-sdk/interfaces/cctp/ITokenMessenger.sol"; + +import "wormhole-sdk/interfaces/IWormhole.sol"; +import {WormholeCctpMessages} from "wormhole-sdk/libraries/WormholeCctpMessages.sol"; +import {toUniversalAddress} from "wormhole-sdk/Utils.sol"; + +import {VM_ADDRESS} from "./Constants.sol"; +import "./CctpOverride.sol"; +import "./WormholeOverride.sol"; + +//faked foreign call chain: +// foreignCaller -> foreignSender -> FOREIGN_TOKEN_MESSENGER -> foreign MessageTransmitter +//example: +// foreignCaller = swap layer +// foreignSender = liquidity layer - implements WormholeCctpTokenMessenger +// emits WormholeCctpMessages.Deposit VAA with a RedeemFill payload + +//local call chain using faked vaa and circle attestation: +// test -> intermediate contract(s) -> mintRecipient -> MessageTransmitter -> TokenMessenger +//example: +// intermediate contract = swap layer +// mintRecipient = liquidity layer + +//using values that are easily recognizable in an encoded payload +uint32 constant FOREIGN_DOMAIN = 0xDDDDDDDD; +bytes32 constant FOREIGN_TOKEN_MESSENGER = + 0xEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE; +bytes32 constant FOREIGN_USDC = + 0xCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCCC; + +//simulates a foreign WormholeCctpTokenMessenger +contract WormholeCctpSimulator { + using WormholeOverride for IWormhole; + using CctpMessages for CctpTokenBurnMessage; + using CctpOverride for IMessageTransmitter; + using CctpMessages for bytes; + using { toUniversalAddress } for address; + + Vm constant vm = Vm(VM_ADDRESS); + + IWormhole immutable wormhole; + IMessageTransmitter immutable messageTransmitter; + ITokenMessenger immutable tokenMessenger; + uint16 immutable foreignChain; + + uint64 foreignNonce; + uint64 foreignSequence; + bytes32 foreignCaller; //address that calls foreignSender to burn their tokens and emit a message + bytes32 foreignSender; //address that sends tokens by calling TokenMessenger.depositForBurn + address mintRecipient; //recipient of cctp messages + address destinationCaller; //by default mintRecipient + + constructor( + IWormhole wormhole_, + address tokenMessenger_, + uint16 foreignChain_, + bytes32 foreignSender_, //contract that invokes the core bridge and calls depositForBurn + address mintRecipient_, + address usdc + ) { + wormhole = wormhole_; + tokenMessenger = ITokenMessenger(tokenMessenger_); + foreignChain = foreignChain_; + foreignSender = foreignSender_; + mintRecipient = mintRecipient_; + destinationCaller = mintRecipient; + messageTransmitter = tokenMessenger.localMessageTransmitter(); + + wormhole.setUpOverride(); + messageTransmitter.setUpOverride(); + + foreignNonce = 0xBBBBBBBBBBBBBBBB; + foreignSequence = 0xAAAAAAAAAAAAAAAA; + //default value - can be overridden if desired + foreignCaller = 0xCA11E2000CA11E200CA11E200CA11E200CA11E200CA11E200CA11E2000CA11E2; + + //register our fake foreign circle token messenger + vm.prank(tokenMessenger.owner()); + tokenMessenger.addRemoteTokenMessenger(FOREIGN_DOMAIN, FOREIGN_TOKEN_MESSENGER); + + //register our fake foreign usdc + // The Circle TokenMessenger has been implemented in a way that supports multiple tokens + // so we have to establish the link between our fake foreign USDC with the actual local + // USDC. + ITokenMinter localMinter = tokenMessenger.localMinter(); + vm.prank(localMinter.tokenController()); + localMinter.linkTokenPair(usdc, FOREIGN_DOMAIN, FOREIGN_USDC); + } + + //to reduce boilerplate, we use setters to avoid arguments that are likely the same + function setMintRecipient(address mintRecipient_) external { + mintRecipient = mintRecipient_; + } + + //setting address(0) disables the check in MessageTransmitter + function setDestinationCaller(address destinationCaller_) external { + destinationCaller = destinationCaller_; + } + + function setForeignCaller(bytes32 foreignCaller_) external { + foreignCaller = foreignCaller_; + } + + function setForeignSender(bytes32 foreignSender_) external { + foreignSender = foreignSender_; + } + + //for creating "pure" cctp transfers (no associated Wormhole vaa) + function craftCctpTokenBurnMessage( + uint256 amount + ) external returns ( + bytes memory encodedCctpMessage, + bytes memory cctpAttestation + ) { + (, encodedCctpMessage, cctpAttestation) = _craftCctpTokenBurnMessage(amount); + } + + //for creating cctp + associated vaa transfers + function craftWormholeCctpRedeemParams( + uint256 amount, + bytes memory payload + ) external returns ( + bytes memory encodedVaa, + bytes memory encodedCctpMessage, + bytes memory cctpAttestation + ) { + CctpTokenBurnMessage memory burnMsg; + (burnMsg, encodedCctpMessage, cctpAttestation) = _craftCctpTokenBurnMessage(amount); + + //craft the associated VAA + (, encodedVaa) = wormhole.craftVaa( + foreignChain, + foreignSender, + foreignSequence++, + WormholeCctpMessages.encodeDeposit( + burnMsg.burnToken, + amount, + burnMsg.header.sourceDomain, + burnMsg.header.destinationDomain, + burnMsg.header.nonce, + foreignCaller, + burnMsg.mintRecipient, + payload + ) + ); + } + + function _craftCctpTokenBurnMessage( + uint256 amount + ) internal returns ( + CctpTokenBurnMessage memory burnMsg, + bytes memory encodedCctpMessage, + bytes memory cctpAttestation + ) { + //compose the cctp burn msg + burnMsg.header.sourceDomain = FOREIGN_DOMAIN; + burnMsg.header.destinationDomain = messageTransmitter.localDomain(); + burnMsg.header.nonce = foreignNonce++; + burnMsg.header.sender = FOREIGN_TOKEN_MESSENGER; + burnMsg.header.recipient = address(tokenMessenger).toUniversalAddress(); + burnMsg.header.destinationCaller = destinationCaller.toUniversalAddress(); + burnMsg.burnToken = FOREIGN_USDC; + burnMsg.mintRecipient = mintRecipient.toUniversalAddress(); + burnMsg.amount = amount; + burnMsg.messageSender = foreignSender; + + encodedCctpMessage = burnMsg.encode(); + cctpAttestation = messageTransmitter.sign(burnMsg); + } +} diff --git a/src/testing/WormholeOverride.sol b/src/testing/WormholeOverride.sol new file mode 100644 index 0000000..99cdc8a --- /dev/null +++ b/src/testing/WormholeOverride.sol @@ -0,0 +1,214 @@ +// SPDX-License-Identifier: Apache 2 +pragma solidity ^0.8.19; + +import {Vm} from "forge-std/Vm.sol"; + +import {IWormhole} from "wormhole-sdk/interfaces/IWormhole.sol"; +import {BytesParsing} from "wormhole-sdk/libraries/BytesParsing.sol"; +import {toUniversalAddress} from "wormhole-sdk/Utils.sol"; + +import {VM_ADDRESS, DEVNET_GUARDIAN_PRIVATE_KEY} from "./Constants.sol"; +import "./LogUtils.sol"; + +struct PublishedMessage { + uint32 timestamp; + uint16 emitterChainId; + bytes32 emitterAddress; + uint64 sequence; + uint32 nonce; + uint8 consistencyLevel; + bytes payload; +} + +//create fake VAAs for forge tests +library WormholeOverride { + using { toUniversalAddress } for address; + using BytesParsing for bytes; + using LogUtils for Vm.Log[]; + + Vm constant vm = Vm(VM_ADDRESS); + + // keccak256("devnetGuardianPrivateKey") - 1 + bytes32 private constant _DEVNET_GUARDIAN_PK_SLOT = + 0x4c7087e9f1bf599f9f9fff4deb3ecae99b29adaab34a0f53d9fa9d61aeaecb63; + + uint32 constant DEFAULT_NONCE = 0xBBBBBBBB; + uint8 constant DEFAULT_CONSISTENCY_LEVEL = 1; + uint8 constant WORMHOLE_VAA_VERSION = 1; + uint16 constant GOVERNANCE_CHAIN_ID = 1; + bytes32 constant GOVERNANCE_CONTRACT = bytes32(uint256(4)); + + function setUpOverride(IWormhole wormhole) internal { + setUpOverride(wormhole, DEVNET_GUARDIAN_PRIVATE_KEY); + } + + function setUpOverride(IWormhole wormhole, uint256 signer) internal { unchecked { + if (guardianPrivateKey(wormhole) == signer) + return; + + require(guardianPrivateKey(wormhole) == 0, "WormholeOverride: already set up"); + + address devnetGuardian = vm.addr(signer); + + // Get slot for Guardian Set at the current index + uint32 guardianSetIndex = wormhole.getCurrentGuardianSetIndex(); + 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; ++i) + vm.store( + address(wormhole), + bytes32(uint256(keccak256(abi.encodePacked(guardianSetSlot))) + i), + 0 + ); + + // Now overwrite the first guardian key with the devnet key specified + // in the function argument. + vm.store( + address(wormhole), + bytes32(uint256(keccak256(abi.encodePacked(guardianSetSlot))) + 0), //just explicit w/ index 0 + devnetGuardian.toUniversalAddress() + ); + + // Change the length to 1 guardian + vm.store( + address(wormhole), + guardianSetSlot, + bytes32(uint256(1)) // length == 1 + ); + + // Confirm guardian set override + address[] memory guardians = wormhole.getGuardianSet(guardianSetIndex).keys; + assert(guardians.length == 1 && guardians[0] == devnetGuardian); + + // Now do something crazy. Save the private key in a specific slot of Wormhole's storage for + // retrieval later. + vm.store(address(wormhole), _DEVNET_GUARDIAN_PK_SLOT, bytes32(signer)); + }} + + function guardianPrivateKey(IWormhole wormhole) internal view returns (uint256 pk) { + pk = uint256(vm.load(address(wormhole), _DEVNET_GUARDIAN_PK_SLOT)); + } + + function fetchPublishedMessages( + IWormhole wormhole, + Vm.Log[] memory logs + ) internal view returns (PublishedMessage[] memory ret) { unchecked { + Vm.Log[] memory pmLogs = logs.filter( + address(wormhole), + keccak256("LogMessagePublished(address,uint64,uint32,bytes,uint8)") + ); + + ret = new PublishedMessage[](pmLogs.length); + for (uint i; i < pmLogs.length; ++i) { + ret[i].emitterAddress = pmLogs[i].topics[1]; + (ret[i].sequence, ret[i].nonce, ret[i].payload, ret[i].consistencyLevel) = + abi.decode(pmLogs[i].data, (uint64, uint32, bytes, uint8)); + ret[i].timestamp = uint32(block.timestamp); + ret[i].emitterChainId = wormhole.chainId(); + } + }} + + function sign( + IWormhole wormhole, + PublishedMessage memory pm + ) internal view returns (IWormhole.VM memory vaa, bytes memory encoded) { + vaa.version = WORMHOLE_VAA_VERSION; + vaa.timestamp = pm.timestamp; + vaa.nonce = pm.nonce; + vaa.emitterChainId = pm.emitterChainId; + vaa.emitterAddress = pm.emitterAddress; + vaa.sequence = pm.sequence; + vaa.consistencyLevel = pm.consistencyLevel; + vaa.payload = pm.payload; + + bytes memory encodedBody = abi.encodePacked( + pm.timestamp, + pm.nonce, + pm.emitterChainId, + pm.emitterAddress, + pm.sequence, + pm.consistencyLevel, + pm.payload + ); + vaa.hash = keccak256(abi.encodePacked(keccak256(encodedBody))); + + vaa.signatures = new IWormhole.Signature[](1); + (vaa.signatures[0].v, vaa.signatures[0].r, vaa.signatures[0].s) = + vm.sign(guardianPrivateKey(wormhole), vaa.hash); + vaa.signatures[0].v -= 27; + + encoded = abi.encodePacked( + vaa.version, + wormhole.getCurrentGuardianSetIndex(), + uint8(vaa.signatures.length), + vaa.signatures[0].guardianIndex, + vaa.signatures[0].r, + vaa.signatures[0].s, + vaa.signatures[0].v, + encodedBody + ); + } + + function craftVaa( + IWormhole wormhole, + uint16 emitterChain, + bytes32 emitterAddress, + uint64 sequence, + bytes memory payload + ) internal view returns (IWormhole.VM memory vaa, bytes memory encoded) { + PublishedMessage memory pm = PublishedMessage({ + timestamp: uint32(block.timestamp), + nonce: DEFAULT_NONCE, + emitterChainId: emitterChain, + emitterAddress: emitterAddress, + sequence: sequence, + consistencyLevel: DEFAULT_CONSISTENCY_LEVEL, + payload: payload + }); + + (vaa, encoded) = sign(wormhole, pm); + } + + function craftGovernanceVaa( + IWormhole wormhole, + bytes32 module, + uint8 action, + uint16 targetChain, + uint64 sequence, + bytes memory decree + ) internal view returns (IWormhole.VM memory vaa, bytes memory encoded) { + (vaa, encoded) = craftGovernanceVaa( + wormhole, + GOVERNANCE_CHAIN_ID, + GOVERNANCE_CONTRACT, + module, + action, + targetChain, + sequence, + decree + ); + } + + function craftGovernanceVaa( + IWormhole wormhole, + uint16 governanceChain, + bytes32 governanceContract, + bytes32 module, + uint8 action, + uint16 targetChain, + uint64 sequence, + bytes memory decree + ) internal view returns (IWormhole.VM memory vaa, bytes memory encoded) { + (vaa, encoded) = craftVaa( + wormhole, + governanceChain, + governanceContract, + sequence, + abi.encodePacked(module, action, targetChain, decree) + ); + } +} diff --git a/src/testing/WormholeRelayer/MockOffchainRelayer.sol b/src/testing/WormholeRelayer/MockOffchainRelayer.sol index 6b55a3d..01b3236 100644 --- a/src/testing/WormholeRelayer/MockOffchainRelayer.sol +++ b/src/testing/WormholeRelayer/MockOffchainRelayer.sol @@ -11,28 +11,26 @@ import {toUniversalAddress, fromUniversalAddress} from "wormhole-sdk/Utils.sol"; import "wormhole-sdk/libraries/BytesParsing.sol"; import {CCTPMessageLib} from "wormhole-sdk/WormholeRelayer/CCTPBase.sol"; -import {WormholeSimulator} from "../helpers/WormholeSimulator.sol"; -import {CircleMessageTransmitterSimulator} from "../helpers/CircleCCTPSimulator.sol"; +import {VM_ADDRESS} from "../Constants.sol"; +import "../WormholeOverride.sol"; +import "../CctpOverride.sol"; import "./DeliveryInstructionDecoder.sol"; import "./ExecutionParameters.sol"; using BytesParsing for bytes; contract MockOffchainRelayer { + using WormholeOverride for IWormhole; + using CctpOverride for IMessageTransmitter; + using CctpMessages for CctpTokenBurnMessage; + using { toUniversalAddress } for address; + using { fromUniversalAddress } for bytes32; - uint16 chainIdOfWormholeAndGuardianUtilities; - IWormhole relayerWormhole; - WormholeSimulator relayerWormholeSimulator; - CircleMessageTransmitterSimulator relayerCircleSimulator; - - - // 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; - + mapping(uint16 => IWormhole) wormholeContracts; + mapping(uint16 => IMessageTransmitter) messageTransmitterContracts; + mapping(uint16 => IWormholeRelayer) wormholeRelayerContracts; mapping(uint16 => uint256) forks; mapping(uint256 => uint16) chainIdFromFork; @@ -41,11 +39,22 @@ contract MockOffchainRelayer { mapping(bytes32 => bytes) pastEncodedDeliveryVAA; - constructor(address _wormhole, address _wormholeSimulator, address _circleSimulator) { - relayerWormhole = IWormhole(_wormhole); - relayerWormholeSimulator = WormholeSimulator(_wormholeSimulator); - relayerCircleSimulator = CircleMessageTransmitterSimulator(_circleSimulator); - chainIdOfWormholeAndGuardianUtilities = relayerWormhole.chainId(); + function getForkChainId() internal view returns (uint16) { + uint16 chainId = chainIdFromFork[vm.activeFork()]; + require(chainId != 0, "Chain not registered with MockOffchainRelayer"); + return chainId; + } + + function getForkWormhole() internal view returns (IWormhole) { + return wormholeContracts[getForkChainId()]; + } + + function getForkMessageTransmitter() internal view returns (IMessageTransmitter) { + return messageTransmitterContracts[getForkChainId()]; + } + + function getForkWormholeRelayer() internal view returns (IWormholeRelayer) { + return wormholeRelayerContracts[getForkChainId()]; } function getPastEncodedSignedVaas( @@ -70,31 +79,23 @@ contract MockOffchainRelayer { function registerChain( uint16 chainId, - address wormholeRelayerContractAddress, + IWormhole wormholeContractAddress, + IMessageTransmitter messageTransmitterContractAddress, + IWormholeRelayer wormholeRelayerContractAddress, uint256 fork ) public { + wormholeContracts[chainId] = wormholeContractAddress; + messageTransmitterContracts[chainId] = messageTransmitterContractAddress; wormholeRelayerContracts[chainId] = wormholeRelayerContractAddress; forks[chainId] = fork; chainIdFromFork[fork] = chainId; } - function relay() public { - relay(vm.getRecordedLogs()); - } - - function relay(Vm.Log[] memory logs, bool debugLogging) public { - relay(logs, bytes(""), debugLogging); - } - - function relay(Vm.Log[] memory logs) public { - relay(logs, bytes(""), false); - } - function vaaKeyMatchesVAA( VaaKey memory vaaKey, bytes memory signedVaa ) internal view returns (bool) { - IWormhole.VM memory parsedVaa = relayerWormhole.parseVM(signedVaa); + IWormhole.VM memory parsedVaa = getForkWormhole().parseVM(signedVaa); return (vaaKey.chainId == parsedVaa.emitterChainId) && (vaaKey.emitterAddress == parsedVaa.emitterAddress) && @@ -111,89 +112,75 @@ contract MockOffchainRelayer { nonce == cctpKey.nonce && domain == cctpKey.domain; } + function relay(Vm.Log[] memory logs, bool debugLogging) public { + relay(logs, bytes(""), debugLogging); + } + 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" - ); - - vm.selectFork(forks[chainIdOfWormholeAndGuardianUtilities]); - - Vm.Log[] memory entries = relayerWormholeSimulator - .fetchWormholeMessageFromLog(logs); - - if (debugLogging) { - console.log("Found %s wormhole messages in logs", entries.length); - } - - bytes[] memory encodedSignedVaas = new bytes[](entries.length); - for (uint256 i = 0; i < encodedSignedVaas.length; i++) { - encodedSignedVaas[i] = relayerWormholeSimulator.fetchSignedMessageFromLogs( - entries[i], - chainId + IWormhole emitterWormhole = getForkWormhole(); + PublishedMessage[] memory pms = emitterWormhole.fetchPublishedMessages(logs); + if (debugLogging) + console.log( + "Found %s wormhole messages in logs from %s", + pms.length, + address(emitterWormhole) ); - } - - 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); - } + IWormhole.VM[] memory vaas = new IWormhole.VM[](pms.length); + bytes[] memory encodedSignedVaas = new bytes[](pms.length); + for (uint256 i = 0; i < encodedSignedVaas.length; ++i) + (vaas[i], encodedSignedVaas[i]) = emitterWormhole.sign(pms[i]); + + CCTPMessageLib.CCTPMessage[] memory cctpSignedMsgs = new CCTPMessageLib.CCTPMessage[](0); + IMessageTransmitter emitterMessageTransmitter = getForkMessageTransmitter(); + if (address(emitterMessageTransmitter) != address(0)) { + CctpTokenBurnMessage[] memory burnMsgs = + emitterMessageTransmitter.fetchBurnMessages(logs); + if (debugLogging) + console.log( + "Found %s circle messages in logs from %s", + burnMsgs.length, + address(emitterMessageTransmitter) + ); - CCTPMessageLib.CCTPMessage[] memory circleSignedMessages = new CCTPMessageLib.CCTPMessage[](cctpEntries.length); - for (uint256 i = 0; i < cctpEntries.length; i++) { - circleSignedMessages[i] = relayerCircleSimulator.fetchSignedMessageFromLog( - cctpEntries[i] - ); + cctpSignedMsgs = new CCTPMessageLib.CCTPMessage[](burnMsgs.length); + for (uint256 i = 0; i < cctpSignedMsgs.length; ++i) { + cctpSignedMsgs[i].message = burnMsgs[i].encode(); + cctpSignedMsgs[i].signature = emitterMessageTransmitter.sign(burnMsgs[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 < encodedSignedVaas.length; i++) { + for (uint16 i = 0; i < vaas.length; ++i) { if (debugLogging) { console.log( "Found VAA from chain %s emitted from %s", - parsed[i].emitterChainId, - fromUniversalAddress(parsed[i].emitterAddress) + vaas[i].emitterChainId, + vaas[i].emitterAddress.fromUniversalAddress() ); } - if ( - parsed[i].emitterAddress == - toUniversalAddress(wormholeRelayerContracts[chainId]) && - (parsed[i].emitterChainId == chainId) - ) { - if (debugLogging) { - console.log("Relaying VAA to chain %s", chainId); - } - vm.selectFork(forks[chainIdOfWormholeAndGuardianUtilities]); + // if ( + // vaas[i].emitterAddress == + // wormholeRelayerContracts[chainId].toUniversalAddress() && + // (vaas[i].emitterChainId == chainId) + // ) { + // if (debugLogging) { + // console.log("Relaying VAA to chain %s", chainId); + // } + // //vm.selectFork(forks[chainIdOfWormholeAndGuardianUtilities]); genericRelay( encodedSignedVaas[i], encodedSignedVaas, - circleSignedMessages, - parsed[i], + cctpSignedMsgs, + vaas[i], deliveryOverrides ); - } + // } } - - vm.selectFork(forks[chainId]); - } - - function relay(bytes memory deliveryOverrides) public { - relay(vm.getRecordedLogs(), deliveryOverrides, false); } function setInfo( @@ -214,6 +201,8 @@ contract MockOffchainRelayer { IWormhole.VM memory parsedDeliveryVAA, bytes memory deliveryOverrides ) internal { + uint currentFork = vm.activeFork(); + (uint8 payloadId, ) = parsedDeliveryVAA.payload.asUint8Unchecked(0); if (payloadId == 1) { DeliveryInstruction memory instruction = decodeDeliveryInstruction( @@ -265,8 +254,7 @@ contract MockOffchainRelayer { vm.deal(address(this), budget); vm.recordLogs(); - IWormholeRelayerDelivery(wormholeRelayerContracts[targetChain]) - .deliver{value: budget}( + getForkWormholeRelayer().deliver{value: budget}( encodedSignedVaasToBeDelivered, encodedDeliveryVAA, payable(address(this)), @@ -308,18 +296,18 @@ contract MockOffchainRelayer { ); uint16 targetChain = decodeDeliveryInstruction( - relayerWormhole.parseVM(oldEncodedDeliveryVAA).payload + getForkWormhole().parseVM(oldEncodedDeliveryVAA).payload ).targetChain; vm.selectFork(forks[targetChain]); - IWormholeRelayerDelivery(wormholeRelayerContracts[targetChain]) - .deliver{value: budget}( + getForkWormholeRelayer().deliver{value: budget}( oldEncodedSignedVaas, oldEncodedDeliveryVAA, payable(address(this)), encode(deliveryOverride) ); } + vm.selectFork(currentFork); } receive() external payable {} diff --git a/src/testing/WormholeRelayerTest.sol b/src/testing/WormholeRelayerTest.sol index 5ee5701..163522d 100644 --- a/src/testing/WormholeRelayerTest.sol +++ b/src/testing/WormholeRelayerTest.sol @@ -10,8 +10,9 @@ import "wormhole-sdk/interfaces/cctp/IMessageTransmitter.sol"; import "wormhole-sdk/interfaces/cctp/ITokenMessenger.sol"; import "wormhole-sdk/Utils.sol"; -import "./helpers/WormholeSimulator.sol"; -import "./helpers/CircleCCTPSimulator.sol"; +import "./UsdcDealer.sol"; +import "./WormholeOverride.sol"; +import "./CctpOverride.sol"; import "./ERC20Mock.sol"; import "./WormholeRelayer/DeliveryInstructionDecoder.sol"; import "./WormholeRelayer/ExecutionParameters.sol"; @@ -26,18 +27,7 @@ struct ChainInfo { 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); + IUSDC USDC; } struct ActiveFork { @@ -48,15 +38,17 @@ struct ActiveFork { IWormholeRelayer relayer; ITokenBridge tokenBridge; IWormhole wormhole; - WormholeSimulator guardian; // USDC parameters - only non-empty for Ethereum, Avalanche, Optimism, Arbitrum mainnets/testnets - IERC20 USDC; + IUSDC USDC; ITokenMessenger circleTokenMessenger; IMessageTransmitter circleMessageTransmitter; - CircleMessageTransmitterSimulator circleAttester; } abstract contract WormholeRelayerTest is Test { + using WormholeOverride for IWormhole; + using CctpOverride for IMessageTransmitter; + using UsdcDealer for IUSDC; + /** * @dev required override to initialize active forks before each test */ @@ -67,11 +59,6 @@ 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; mapping(uint16 => ChainInfo) public chainInfosMainnet; @@ -109,12 +96,9 @@ abstract contract WormholeRelayerTest is Test { wormhole: chainInfos[i].wormhole, // 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)) + USDC: chainInfos[i].USDC }); } } @@ -128,19 +112,14 @@ abstract contract WormholeRelayerTest is Test { } function _setUp() internal { - // create fork and guardian for each active fork + // create and setup each active fork for (uint256 i = 0; i < activeForksList.length; ++i) { uint16 chainId = activeForksList[i]; ActiveFork storage fork = activeForks[chainId]; fork.fork = vm.createSelectFork(fork.url); - fork.guardian = new WormholeSimulator( - address(fork.wormhole), - DEVNET_GUARDIAN_PK - ); - fork.circleAttester = new CircleMessageTransmitterSimulator( - address(fork.circleMessageTransmitter), - CIRCLE_ATTESTER_PK - ); + fork.wormhole.setUpOverride(); + if (address(fork.circleMessageTransmitter) != address(0)) + fork.circleMessageTransmitter.setUpOverride(); } // run setUp virtual functions for each fork @@ -152,17 +131,15 @@ 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(); // 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.wormhole, + fork.circleMessageTransmitter, + fork.relayer, fork.fork ); } @@ -177,21 +154,20 @@ abstract contract WormholeRelayerTest is Test { } function performDelivery() public { - performDelivery(vm.getRecordedLogs()); + performDelivery(vm.getRecordedLogs(), false); } 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); + function performDelivery(Vm.Log[] memory logs) public { + performDelivery(logs, false); } - function performDelivery(Vm.Log[] memory logs) public { + function performDelivery(Vm.Log[] memory logs, bool debugLogging) public { require(logs.length > 0, "no events recorded"); - mockOffchainRelayer.relay(logs); + mockOffchainRelayer.relay(logs, debugLogging); } function createAndAttestToken( @@ -206,12 +182,8 @@ 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 + (, bytes memory attestation) = home.wormhole.sign( + home.wormhole.fetchPublishedMessages(vm.getRecordedLogs())[0] ); for (uint256 i = 0; i < activeForksList.length; ++i) { @@ -231,10 +203,7 @@ abstract contract WormholeRelayerTest is Test { 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); + current.USDC.deal(addr, amount); vm.selectFork(originalFork); } @@ -273,7 +242,7 @@ abstract contract WormholeRelayerTest is Test { circleTokenMessenger: ITokenMessenger( 0xeb08f243E5d3FCFF26A9E38Ae5520A669f4019d0 ), - USDC: IERC20(0x5425890298aed601595a70AB815c96711a31Bc65) + USDC: IUSDC(0x5425890298aed601595a70AB815c96711a31Bc65) }); chainInfosTestnet[14] = ChainInfo({ chainId: 14, @@ -291,7 +260,7 @@ abstract contract WormholeRelayerTest is Test { wormhole: IWormhole(0x88505117CA88e7dd2eC6EA1E13f0948db2D50D56), circleMessageTransmitter: IMessageTransmitter(address(0)), circleTokenMessenger: ITokenMessenger(address(0)), - USDC: IERC20(address(0)) + USDC: IUSDC(address(0)) }); chainInfosTestnet[4] = ChainInfo({ chainId: 4, @@ -309,7 +278,7 @@ abstract contract WormholeRelayerTest is Test { wormhole: IWormhole(0x68605AD7b15c732a30b1BbC62BE8F2A509D74b4D), circleMessageTransmitter: IMessageTransmitter(address(0)), circleTokenMessenger: ITokenMessenger(address(0)), - USDC: IERC20(address(0)) + USDC: IUSDC(address(0)) }); chainInfosTestnet[16] = ChainInfo({ chainId: 16, @@ -327,7 +296,7 @@ abstract contract WormholeRelayerTest is Test { wormhole: IWormhole(0xa5B7D85a8f27dd7907dc8FdC21FA5657D5E2F901), circleMessageTransmitter: IMessageTransmitter(address(0)), circleTokenMessenger: ITokenMessenger(address(0)), - USDC: IERC20(address(0)) + USDC: IUSDC(address(0)) }); chainInfosMainnet[2] = ChainInfo({ chainId: 2, @@ -349,7 +318,7 @@ abstract contract WormholeRelayerTest is Test { circleTokenMessenger: ITokenMessenger( 0xBd3fa81B58Ba92a82136038B25aDec7066af3155 ), - USDC: IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48) + USDC: IUSDC(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48) }); chainInfosMainnet[4] = ChainInfo({ chainId: 4, @@ -367,7 +336,7 @@ abstract contract WormholeRelayerTest is Test { wormhole: IWormhole(0x98f3c9e6E3fAce36bAAd05FE09d375Ef1464288B), circleMessageTransmitter: IMessageTransmitter(address(0)), circleTokenMessenger: ITokenMessenger(address(0)), - USDC: IERC20(address(0)) + USDC: IUSDC(address(0)) }); chainInfosMainnet[6] = ChainInfo({ chainId: 6, @@ -389,7 +358,7 @@ abstract contract WormholeRelayerTest is Test { circleTokenMessenger: ITokenMessenger( 0x6B25532e1060CE10cc3B0A99e5683b91BFDe6982 ), - USDC: IERC20(0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E) + USDC: IUSDC(0xB97EF9Ef8734C71904D8002F8b6Bc66Dd9c48a6E) }); chainInfosMainnet[10] = ChainInfo({ chainId: 10, @@ -407,7 +376,7 @@ abstract contract WormholeRelayerTest is Test { wormhole: IWormhole(0x126783A6Cb203a3E35344528B26ca3a0489a1485), circleMessageTransmitter: IMessageTransmitter(address(0)), circleTokenMessenger: ITokenMessenger(address(0)), - USDC: IERC20(address(0)) + USDC: IUSDC(address(0)) }); chainInfosMainnet[13] = ChainInfo({ chainId: 13, @@ -425,7 +394,7 @@ abstract contract WormholeRelayerTest is Test { wormhole: IWormhole(0x0C21603c4f3a6387e241c0091A7EA39E43E90bb7), circleMessageTransmitter: IMessageTransmitter(address(0)), circleTokenMessenger: ITokenMessenger(address(0)), - USDC: IERC20(address(0)) + USDC: IUSDC(address(0)) }); chainInfosMainnet[14] = ChainInfo({ chainId: 14, @@ -440,7 +409,7 @@ abstract contract WormholeRelayerTest is Test { wormhole: IWormhole(0xa321448d90d4e5b0A732867c18eA198e75CAC48E), circleMessageTransmitter: IMessageTransmitter(address(0)), circleTokenMessenger: ITokenMessenger(address(0)), - USDC: IERC20(address(0)) + USDC: IUSDC(address(0)) }); chainInfosMainnet[12] = ChainInfo({ chainId: 12, @@ -458,7 +427,7 @@ abstract contract WormholeRelayerTest is Test { wormhole: IWormhole(0xa321448d90d4e5b0A732867c18eA198e75CAC48E), circleMessageTransmitter: IMessageTransmitter(address(0)), circleTokenMessenger: ITokenMessenger(address(0)), - USDC: IERC20(address(0)) + USDC: IUSDC(address(0)) }); chainInfosMainnet[11] = ChainInfo({ chainId: 11, @@ -476,7 +445,7 @@ abstract contract WormholeRelayerTest is Test { wormhole: IWormhole(0xa321448d90d4e5b0A732867c18eA198e75CAC48E), circleMessageTransmitter: IMessageTransmitter(address(0)), circleTokenMessenger: ITokenMessenger(address(0)), - USDC: IERC20(address(0)) + USDC: IUSDC(address(0)) }); chainInfosMainnet[16] = ChainInfo({ chainId: 16, @@ -494,7 +463,7 @@ abstract contract WormholeRelayerTest is Test { wormhole: IWormhole(0xC8e2b0cD52Cf01b0Ce87d389Daa3d414d4cE29f3), circleMessageTransmitter: IMessageTransmitter(address(0)), circleTokenMessenger: ITokenMessenger(address(0)), - USDC: IERC20(address(0)) + USDC: IUSDC(address(0)) }); chainInfosMainnet[23] = ChainInfo({ chainId: 23, @@ -516,7 +485,7 @@ abstract contract WormholeRelayerTest is Test { circleTokenMessenger: ITokenMessenger( 0x19330d10D9Cc8751218eaf51E8885D058642E08A ), - USDC: IERC20(0xaf88d065e77c8cC2239327C5EDb3A432268e5831) + USDC: IUSDC(0xaf88d065e77c8cC2239327C5EDb3A432268e5831) }); chainInfosMainnet[24] = ChainInfo({ chainId: 24, @@ -538,7 +507,7 @@ abstract contract WormholeRelayerTest is Test { circleTokenMessenger: ITokenMessenger( 0x2B4069517957735bE00ceE0fadAE88a26365528f ), - USDC: IERC20(0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85) + USDC: IUSDC(0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85) }); chainInfosMainnet[30] = ChainInfo({ chainId: 30, @@ -557,7 +526,7 @@ abstract contract WormholeRelayerTest is Test { circleTokenMessenger: ITokenMessenger( address(0x1682Ae6375C4E4A97e4B583BC394c861A46D8962) ), - USDC: IERC20(address(0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913)) + USDC: IUSDC(address(0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913)) }); } @@ -602,12 +571,6 @@ abstract contract WormholeRelayerBasicTest is WormholeRelayerTest { ITokenBridge public tokenBridgeTarget; IWormhole public wormholeTarget; - WormholeSimulator public guardianSource; - WormholeSimulator public guardianTarget; - - CircleMessageTransmitterSimulator public circleAttesterSource; - CircleMessageTransmitterSimulator public circleAttesterTarget; - /* * end activeForks aliases */ @@ -621,10 +584,6 @@ abstract contract WormholeRelayerBasicTest is WormholeRelayerTest { targetFork = 1; _setUp(); // 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/BytesLib.sol b/src/testing/helpers/BytesLib.sol deleted file mode 100644 index 4e4718a..0000000 --- a/src/testing/helpers/BytesLib.sol +++ /dev/null @@ -1,476 +0,0 @@ -// SPDX-License-Identifier: Unlicense -/* - * @title Solidity Bytes Arrays Utils - * @author Gonçalo Sá - * - * @dev Bytes tightly packed arrays utility library for ethereum contracts written in Solidity. - * The library lets you concatenate, slice and type cast bytes arrays both in memory and storage. - */ -pragma solidity >=0.8.0 <0.9.0; - -library BytesLib { - function concat(bytes memory _preBytes, bytes memory _postBytes) internal pure returns (bytes memory) { - bytes memory tempBytes; - - assembly { - // Get a location of some free memory and store it in tempBytes as - // Solidity does for memory variables. - tempBytes := mload(0x40) - - // Store the length of the first bytes array at the beginning of - // the memory for tempBytes. - let length := mload(_preBytes) - mstore(tempBytes, length) - - // Maintain a memory counter for the current write location in the - // temp bytes array by adding the 32 bytes for the array length to - // the starting location. - let mc := add(tempBytes, 0x20) - // Stop copying when the memory counter reaches the length of the - // first bytes array. - let end := add(mc, length) - - for { - // Initialize a copy counter to the start of the _preBytes data, - // 32 bytes into its memory. - let cc := add(_preBytes, 0x20) - } lt(mc, end) { - // Increase both counters by 32 bytes each iteration. - mc := add(mc, 0x20) - cc := add(cc, 0x20) - } { - // Write the _preBytes data into the tempBytes memory 32 bytes - // at a time. - mstore(mc, mload(cc)) - } - - // Add the length of _postBytes to the current length of tempBytes - // and store it as the new length in the first 32 bytes of the - // tempBytes memory. - length := mload(_postBytes) - mstore(tempBytes, add(length, mload(tempBytes))) - - // Move the memory counter back from a multiple of 0x20 to the - // actual end of the _preBytes data. - mc := end - // Stop copying when the memory counter reaches the new combined - // length of the arrays. - end := add(mc, length) - - for { let cc := add(_postBytes, 0x20) } lt(mc, end) { - mc := add(mc, 0x20) - cc := add(cc, 0x20) - } { mstore(mc, mload(cc)) } - - // Update the free-memory pointer by padding our last write location - // to 32 bytes: add 31 bytes to the end of tempBytes to move to the - // next 32 byte block, then round down to the nearest multiple of - // 32. If the sum of the length of the two arrays is zero then add - // one before rounding down to leave a blank 32 bytes (the length block with 0). - mstore( - 0x40, - and( - add(add(end, iszero(add(length, mload(_preBytes)))), 31), - not(31) // Round down to the nearest 32 bytes. - ) - ) - } - - return tempBytes; - } - - function concatStorage(bytes storage _preBytes, bytes memory _postBytes) internal { - assembly { - // Read the first 32 bytes of _preBytes storage, which is the length - // of the array. (We don't need to use the offset into the slot - // because arrays use the entire slot.) - let fslot := sload(_preBytes.slot) - // Arrays of 31 bytes or less have an even value in their slot, - // while longer arrays have an odd value. The actual length is - // the slot divided by two for odd values, and the lowest order - // byte divided by two for even values. - // If the slot is even, bitwise and the slot with 255 and divide by - // two to get the length. If the slot is odd, bitwise and the slot - // with -1 and divide by two. - let slength := div(and(fslot, sub(mul(0x100, iszero(and(fslot, 1))), 1)), 2) - let mlength := mload(_postBytes) - let newlength := add(slength, mlength) - // slength can contain both the length and contents of the array - // if length < 32 bytes so let's prepare for that - // v. http://solidity.readthedocs.io/en/latest/miscellaneous.html#layout-of-state-variables-in-storage - switch add(lt(slength, 32), lt(newlength, 32)) - case 2 { - // Since the new array still fits in the slot, we just need to - // update the contents of the slot. - // uint256(bytes_storage) = uint256(bytes_storage) + uint256(bytes_memory) + new_length - sstore( - _preBytes.slot, - // all the modifications to the slot are inside this - // next block - add( - // we can just add to the slot contents because the - // bytes we want to change are the LSBs - fslot, - add( - mul( - div( - // load the bytes from memory - mload(add(_postBytes, 0x20)), - // zero all bytes to the right - exp(0x100, sub(32, mlength)) - ), - // and now shift left the number of bytes to - // leave space for the length in the slot - exp(0x100, sub(32, newlength)) - ), - // increase length by the double of the memory - // bytes length - mul(mlength, 2) - ) - ) - ) - } - case 1 { - // The stored value fits in the slot, but the combined value - // will exceed it. - // get the keccak hash to get the contents of the array - mstore(0x0, _preBytes.slot) - let sc := add(keccak256(0x0, 0x20), div(slength, 32)) - - // save new length - sstore(_preBytes.slot, add(mul(newlength, 2), 1)) - - // The contents of the _postBytes array start 32 bytes into - // the structure. Our first read should obtain the `submod` - // bytes that can fit into the unused space in the last word - // of the stored array. To get this, we read 32 bytes starting - // from `submod`, so the data we read overlaps with the array - // contents by `submod` bytes. Masking the lowest-order - // `submod` bytes allows us to add that value directly to the - // stored value. - - let submod := sub(32, slength) - let mc := add(_postBytes, submod) - let end := add(_postBytes, mlength) - let mask := sub(exp(0x100, submod), 1) - - sstore( - sc, - add( - and(fslot, 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff00), - and(mload(mc), mask) - ) - ) - - for { - mc := add(mc, 0x20) - sc := add(sc, 1) - } lt(mc, end) { - sc := add(sc, 1) - mc := add(mc, 0x20) - } { sstore(sc, mload(mc)) } - - mask := exp(0x100, sub(mc, end)) - - sstore(sc, mul(div(mload(mc), mask), mask)) - } - default { - // get the keccak hash to get the contents of the array - mstore(0x0, _preBytes.slot) - // Start copying to the last used word of the stored array. - let sc := add(keccak256(0x0, 0x20), div(slength, 32)) - - // save new length - sstore(_preBytes.slot, add(mul(newlength, 2), 1)) - - // Copy over the first `submod` bytes of the new data as in - // case 1 above. - let slengthmod := mod(slength, 32) - let mlengthmod := mod(mlength, 32) - let submod := sub(32, slengthmod) - let mc := add(_postBytes, submod) - let end := add(_postBytes, mlength) - let mask := sub(exp(0x100, submod), 1) - - sstore(sc, add(sload(sc), and(mload(mc), mask))) - - for { - sc := add(sc, 1) - mc := add(mc, 0x20) - } lt(mc, end) { - sc := add(sc, 1) - mc := add(mc, 0x20) - } { sstore(sc, mload(mc)) } - - mask := exp(0x100, sub(mc, end)) - - sstore(sc, mul(div(mload(mc), mask), mask)) - } - } - } - - function slice(bytes memory _bytes, uint256 _start, uint256 _length) internal pure returns (bytes memory) { - require(_length + 31 >= _length, "slice_overflow"); - require(_bytes.length >= _start + _length, "slice_outOfBounds"); - - bytes memory tempBytes; - - assembly { - switch iszero(_length) - case 0 { - // Get a location of some free memory and store it in tempBytes as - // Solidity does for memory variables. - tempBytes := mload(0x40) - - // The first word of the slice result is potentially a partial - // word read from the original array. To read it, we calculate - // the length of that partial word and start copying that many - // bytes into the array. The first word we copy will start with - // data we don't care about, but the last `lengthmod` bytes will - // land at the beginning of the contents of the new array. When - // we're done copying, we overwrite the full first word with - // the actual length of the slice. - let lengthmod := and(_length, 31) - - // The multiplication in the next line is necessary - // because when slicing multiples of 32 bytes (lengthmod == 0) - // the following copy loop was copying the origin's length - // and then ending prematurely not copying everything it should. - let mc := add(add(tempBytes, lengthmod), mul(0x20, iszero(lengthmod))) - let end := add(mc, _length) - - for { - // The multiplication in the next line has the same exact purpose - // as the one above. - let cc := add(add(add(_bytes, lengthmod), mul(0x20, iszero(lengthmod))), _start) - } lt(mc, end) { - mc := add(mc, 0x20) - cc := add(cc, 0x20) - } { mstore(mc, mload(cc)) } - - mstore(tempBytes, _length) - - //update free-memory pointer - //allocating the array padded to 32 bytes like the compiler does now - mstore(0x40, and(add(mc, 31), not(31))) - } - //if we want a zero-length slice let's just return a zero-length array - default { - tempBytes := mload(0x40) - //zero out the 32 bytes slice we are about to return - //we need to do it because Solidity does not garbage collect - mstore(tempBytes, 0) - - mstore(0x40, add(tempBytes, 0x20)) - } - } - - return tempBytes; - } - - function toAddress(bytes memory _bytes, uint256 _start) internal pure returns (address) { - require(_bytes.length >= _start + 20, "toAddress_outOfBounds"); - address tempAddress; - - assembly { - tempAddress := div(mload(add(add(_bytes, 0x20), _start)), 0x1000000000000000000000000) - } - - return tempAddress; - } - - function toUint8(bytes memory _bytes, uint256 _start) internal pure returns (uint8) { - require(_bytes.length >= _start + 1, "toUint8_outOfBounds"); - uint8 tempUint; - - assembly { - tempUint := mload(add(add(_bytes, 0x1), _start)) - } - - return tempUint; - } - - function toUint16(bytes memory _bytes, uint256 _start) internal pure returns (uint16) { - require(_bytes.length >= _start + 2, "toUint16_outOfBounds"); - uint16 tempUint; - - assembly { - tempUint := mload(add(add(_bytes, 0x2), _start)) - } - - return tempUint; - } - - function toUint32(bytes memory _bytes, uint256 _start) internal pure returns (uint32) { - require(_bytes.length >= _start + 4, "toUint32_outOfBounds"); - uint32 tempUint; - - assembly { - tempUint := mload(add(add(_bytes, 0x4), _start)) - } - - return tempUint; - } - - function toUint64(bytes memory _bytes, uint256 _start) internal pure returns (uint64) { - require(_bytes.length >= _start + 8, "toUint64_outOfBounds"); - uint64 tempUint; - - assembly { - tempUint := mload(add(add(_bytes, 0x8), _start)) - } - - return tempUint; - } - - function toUint96(bytes memory _bytes, uint256 _start) internal pure returns (uint96) { - require(_bytes.length >= _start + 12, "toUint96_outOfBounds"); - uint96 tempUint; - - assembly { - tempUint := mload(add(add(_bytes, 0xc), _start)) - } - - return tempUint; - } - - function toUint128(bytes memory _bytes, uint256 _start) internal pure returns (uint128) { - require(_bytes.length >= _start + 16, "toUint128_outOfBounds"); - uint128 tempUint; - - assembly { - tempUint := mload(add(add(_bytes, 0x10), _start)) - } - - return tempUint; - } - - function toUint256(bytes memory _bytes, uint256 _start) internal pure returns (uint256) { - require(_bytes.length >= _start + 32, "toUint256_outOfBounds"); - uint256 tempUint; - - assembly { - tempUint := mload(add(add(_bytes, 0x20), _start)) - } - - return tempUint; - } - - function toBytes32(bytes memory _bytes, uint256 _start) internal pure returns (bytes32) { - require(_bytes.length >= _start + 32, "toBytes32_outOfBounds"); - bytes32 tempBytes32; - - assembly { - tempBytes32 := mload(add(add(_bytes, 0x20), _start)) - } - - return tempBytes32; - } - - function equal(bytes memory _preBytes, bytes memory _postBytes) internal pure returns (bool) { - bool success = true; - - assembly { - let length := mload(_preBytes) - - // if lengths don't match the arrays are not equal - switch eq(length, mload(_postBytes)) - case 1 { - // cb is a circuit breaker in the for loop since there's - // no said feature for inline assembly loops - // cb = 1 - don't breaker - // cb = 0 - break - let cb := 1 - - let mc := add(_preBytes, 0x20) - let end := add(mc, length) - - for { let cc := add(_postBytes, 0x20) } - // the next line is the loop condition: - // while(uint256(mc < end) + cb == 2) - eq(add(lt(mc, end), cb), 2) { - mc := add(mc, 0x20) - cc := add(cc, 0x20) - } { - // if any of these checks fails then arrays are not equal - if iszero(eq(mload(mc), mload(cc))) { - // unsuccess: - success := 0 - cb := 0 - } - } - } - default { - // unsuccess: - success := 0 - } - } - - return success; - } - - function equalStorage(bytes storage _preBytes, bytes memory _postBytes) internal view returns (bool) { - bool success = true; - - assembly { - // we know _preBytes_offset is 0 - let fslot := sload(_preBytes.slot) - // Decode the length of the stored array like in concatStorage(). - let slength := div(and(fslot, sub(mul(0x100, iszero(and(fslot, 1))), 1)), 2) - let mlength := mload(_postBytes) - - // if lengths don't match the arrays are not equal - switch eq(slength, mlength) - case 1 { - // slength can contain both the length and contents of the array - // if length < 32 bytes so let's prepare for that - // v. http://solidity.readthedocs.io/en/latest/miscellaneous.html#layout-of-state-variables-in-storage - if iszero(iszero(slength)) { - switch lt(slength, 32) - case 1 { - // blank the last byte which is the length - fslot := mul(div(fslot, 0x100), 0x100) - - if iszero(eq(fslot, mload(add(_postBytes, 0x20)))) { - // unsuccess: - success := 0 - } - } - default { - // cb is a circuit breaker in the for loop since there's - // no said feature for inline assembly loops - // cb = 1 - don't breaker - // cb = 0 - break - let cb := 1 - - // get the keccak hash to get the contents of the array - mstore(0x0, _preBytes.slot) - let sc := keccak256(0x0, 0x20) - - let mc := add(_postBytes, 0x20) - let end := add(mc, mlength) - - // the next line is the loop condition: - // while(uint256(mc < end) + cb == 2) - for {} eq(add(lt(mc, end), cb), 2) { - sc := add(sc, 1) - mc := add(mc, 0x20) - } { - if iszero(eq(sload(sc), mload(mc))) { - // unsuccess: - success := 0 - cb := 0 - } - } - } - } - } - default { - // unsuccess: - success := 0 - } - } - - return success; - } -} diff --git a/src/testing/helpers/CircleCCTPSimulator.sol b/src/testing/helpers/CircleCCTPSimulator.sol deleted file mode 100644 index 9b54ff4..0000000 --- a/src/testing/helpers/CircleCCTPSimulator.sol +++ /dev/null @@ -1,156 +0,0 @@ -// SPDX-License-Identifier: Apache 2 -pragma solidity ^0.8.19; - -import "forge-std/Vm.sol"; - -import {IMessageTransmitter} from "wormhole-sdk/interfaces/cctp/IMessageTransmitter.sol"; -import {CCTPMessageLib} from "wormhole-sdk/WormholeRelayer/CCTPBase.sol"; - -import "./BytesLib.sol"; - -interface MessageTransmitterViewAttesterManager { - function attesterManager() external view returns (address); - - function enableAttester(address newAttester) external; - - function setSignatureThreshold(uint256 newSignatureThreshold) 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; - - bool public valid; - - // 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_; - valid = messageTransmitter_ != address(0); - if(valid) overrideAttester(vm.addr(attesterPrivateKey)); - } - - function overrideAttester(address attesterPublicKey) internal { - { - MessageTransmitterViewAttesterManager attesterManagerInterface = MessageTransmitterViewAttesterManager( - address(messageTransmitter) - ); - address attesterManager = attesterManagerInterface - .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) { - uint256 index = 32; - - // length of payload - uint256 payloadLen = log.data.toUint256(index); - index += 32; - - 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 - * @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("MessageSent(bytes)") - ) { - 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("MessageSent(bytes)") - ) { - 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 - ) public view returns (CCTPMessageLib.CCTPMessage memory) { - // Parse messageTransmitter message from ethereum logs - bytes memory message = parseMessageFromMessageTransmitterLog(log); - - return CCTPMessageLib.CCTPMessage(message, signMessage(message)); - } - - /** - * @notice Signs a simulated messageTransmitter message - * @param message The messageTransmitter message - * @return signedMessage signed messageTransmitter message - */ - - function signMessage( - bytes memory message - ) public view returns (bytes memory signedMessage) { - - 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/WormholeSimulator.sol b/src/testing/helpers/WormholeSimulator.sol deleted file mode 100644 index fafcdb4..0000000 --- a/src/testing/helpers/WormholeSimulator.sol +++ /dev/null @@ -1,281 +0,0 @@ -// SPDX-License-Identifier: Apache 2 -pragma solidity ^0.8.19; - -import "forge-std/Vm.sol"; - -import "wormhole-sdk/interfaces/IWormhole.sol"; - -import "./BytesLib.sol"; - -/** - * @title A Wormhole Guardian Simulator - * @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 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"))))); - Vm public constant vm = Vm(VM_ADDRESS); - - // Allow access to Wormhole - IWormhole public wormhole; - - // Save the guardian PK to sign messages with - uint256 private devnetGuardianPK; - - /** - * @param wormhole_ address of the Wormhole core contract for the mainnet chain being forked - * @param devnetGuardian private key of the devnet Guardian - */ - constructor(address wormhole_, uint256 devnetGuardian) { - wormhole = IWormhole(wormhole_); - devnetGuardianPK = devnetGuardian; - overrideToDevnetGuardian(vm.addr(devnetGuardian)); - } - - function overrideToDevnetGuardian(address devnetGuardian) internal { - { - bytes32 data = vm.load(address(this), bytes32(uint256(2))); - require(data == bytes32(0), "incorrect slot"); - - // Get slot for Guardian Set at the current index - uint32 guardianSetIndex = wormhole.getCurrentGuardianSetIndex(); - 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; ) { - vm.store( - address(wormhole), - bytes32( - uint256(keccak256(abi.encodePacked(guardianSetSlot))) + - i - ), - bytes32(0) - ); - unchecked { - i += 1; - } - } - - // Now overwrite the first guardian key with the devnet key specified - // in the function argument. - vm.store( - address(wormhole), - bytes32( - uint256(keccak256(abi.encodePacked(guardianSetSlot))) + 0 - ), // just explicit w/ index 0 - bytes32(uint256(uint160(devnetGuardian))) - ); - - // Change the length to 1 guardian - vm.store( - address(wormhole), - guardianSetSlot, - bytes32(uint256(1)) // length == 1 - ); - - // Confirm guardian set override - address[] memory guardians = wormhole - .getGuardianSet(guardianSetIndex) - .keys; - require(guardians.length == 1, "guardians.length != 1"); - require( - guardians[0] == devnetGuardian, - "incorrect guardian set override" - ); - } - } - - 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_) { - uint256 index = 0; - - // emitterAddress - vm_.emitterAddress = bytes32(log.topics[1]); - - // sequence - vm_.sequence = log.data.toUint64(index + 32 - 8); - index += 32; - - // nonce - vm_.nonce = log.data.toUint32(index + 32 - 4); - index += 32; - - // skip random bytes - index += 32; - - // consistency level - vm_.consistencyLevel = log.data.toUint8(index + 32 - 1); - 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 wormhole message"); - } - - /** - * @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) { - uint256 count = 0; - for (uint256 i = 0; i < logs.length; i++) { - if ( - logs[i].topics[0] == - keccak256( - "LogMessagePublished(address,uint64,uint32,bytes,uint8)" - ) - ) { - 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( - "LogMessagePublished(address,uint64,uint32,bytes,uint8)" - ) - ) { - published[publishedIndex] = logs[i]; - publishedIndex += 1; - } - } - - return published; - } - - /** - * @notice Encodes Wormhole message body into bytes - * @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) { - encodedObservation = abi.encodePacked( - vm_.timestamp, - vm_.nonce, - vm_.emitterChainId, - vm_.emitterAddress, - vm_.sequence, - vm_.consistencyLevel, - vm_.payload - ); - } - - /** - * @notice Formats and signs a simulated Wormhole message using the emitted log from calling `publishMessage` - * @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) { - // Create message instance - IWormhole.VM memory vm_; - - // Parse wormhole message from ethereum logs - vm_ = parseVMFromLogs(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 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) { - // Compute the hash of the body - bytes memory body = encodeObservation(vm_); - vm_.hash = doubleKeccak256(body); - - // Sign the hash with the devnet guardian private key - IWormhole.Signature[] memory sigs = new IWormhole.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, - wormhole.getCurrentGuardianSetIndex(), - uint8(sigs.length), - sigs[0].guardianIndex, - sigs[0].r, - sigs[0].s, - sigs[0].v - 27, - body - ); - } - - /** - * @notice Sets the wormhole protocol fee - * @param newFee The new wormhole fee - */ - function setMessageFee(uint256 newFee) public { - bytes32 coreModule = 0x00000000000000000000000000000000000000000000000000000000436f7265; - bytes memory message = abi.encodePacked( - coreModule, - uint8(3), - uint16(wormhole.chainId()), - newFee - ); - IWormhole.VM memory preSignedMessage = IWormhole.VM({ - version: 1, - timestamp: uint32(block.timestamp), - nonce: 0, - emitterChainId: wormhole.governanceChainId(), - emitterAddress: wormhole.governanceContract(), - sequence: 0, - consistencyLevel: 200, - payload: message, - guardianSetIndex: 0, - signatures: new IWormhole.Signature[](0), - hash: bytes32("") - }); - - bytes memory signed = encodeAndSignMessage(preSignedMessage); - wormhole.submitSetMessageFee(signed); - } -}