├── images └── architecture.png ├── foundry.toml ├── remappings.txt ├── .solhint.json ├── .gitignore ├── slither.config.json ├── .gitmodules ├── src ├── interfaces │ ├── IUniswapWormholeMessageSender.sol │ └── IUniswapWormholeMessageReceiver.sol ├── Structs.sol ├── UniswapWormholeMessageSender.sol └── UniswapWormholeMessageReceiver.sol ├── .github └── workflows │ └── test.yml ├── test ├── MockGovernanceReceiver.sol └── UniswapWormholeMessageSenderReceiver.t.sol └── README.md /images/architecture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/uniswapfoundation/Uniswap-Wormhole-Bridge/HEAD/images/architecture.png -------------------------------------------------------------------------------- /foundry.toml: -------------------------------------------------------------------------------- 1 | [profile.default] 2 | src = 'src' 3 | out = 'out' 4 | libs = ['lib'] 5 | 6 | # See more config options https://github.com/foundry-rs/foundry/tree/master/config -------------------------------------------------------------------------------- /remappings.txt: -------------------------------------------------------------------------------- 1 | ds-test/=lib/forge-std/lib/ds-test/src/ 2 | forge-std/=lib/forge-std/src/ 3 | wormhole/=lib/wormhole/ethereum/ 4 | @openzeppelin/=lib/openzeppelin-contracts/ -------------------------------------------------------------------------------- /.solhint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "solhint:recommended", 3 | "rules": { 4 | "compiler-version": ["error", "^0.8.0"], 5 | "func-visibility": ["warn", { "ignoreConstructors": true }] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiler files 2 | cache/ 3 | out/ 4 | 5 | # Ignores development broadcast logs 6 | !/broadcast 7 | /broadcast/*/31337/ 8 | /broadcast/**/dry-run/ 9 | 10 | # Docs 11 | docs/ 12 | 13 | # Dotenv file 14 | .env 15 | -------------------------------------------------------------------------------- /slither.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude_informational": true, 3 | "exclude_low": true, 4 | "exclude_medium": false, 5 | "exclude_high": false, 6 | "disable_color": false, 7 | "filter_paths": "lib/|mocks/|test/", 8 | "legacy_ast": false, 9 | "detectors_to_exclude": "low-level-calls,missing-inheritance,incorrect-versions-of-solidity,unused-return,calls-inside-a-loop,block-timestamp" 10 | } 11 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/forge-std"] 2 | path = lib/forge-std 3 | url = https://github.com/foundry-rs/forge-std 4 | branch = v1.5.0 5 | [submodule "lib/wormhole"] 6 | path = lib/wormhole 7 | url = https://github.com/wormhole-foundation/wormhole/ 8 | [submodule "lib/openzeppelin-contracts"] 9 | path = lib/openzeppelin-contracts 10 | url = https://github.com/openzeppelin/openzeppelin-contracts 11 | branch = v4.8.2 -------------------------------------------------------------------------------- /src/interfaces/IUniswapWormholeMessageSender.sol: -------------------------------------------------------------------------------- 1 | // contracts/Structs.sol 2 | // SPDX-License-Identifier: Apache 2 3 | 4 | pragma solidity ^0.8.7; 5 | 6 | interface IUniswapWormholeMessageSender { 7 | function sendMessage( 8 | address[] memory targets, 9 | uint256[] memory values, 10 | bytes[] memory datas, 11 | address messageReceiver, 12 | uint16 receiverChainId 13 | ) external payable; 14 | function owner() external returns (address); 15 | function setOwner(address newOwner) external; 16 | } 17 | -------------------------------------------------------------------------------- /src/Structs.sol: -------------------------------------------------------------------------------- 1 | // contracts/Structs.sol 2 | // SPDX-License-Identifier: Apache 2 3 | 4 | pragma solidity ^0.8.7; 5 | 6 | interface Structs { 7 | struct Provider { 8 | uint16 chainId; 9 | uint16 governanceChainId; 10 | bytes32 governanceContract; 11 | } 12 | 13 | struct GuardianSet { 14 | address[] keys; 15 | uint32 expirationTime; 16 | } 17 | 18 | struct Signature { 19 | bytes32 r; 20 | bytes32 s; 21 | uint8 v; 22 | uint8 guardianIndex; 23 | } 24 | 25 | struct VM { 26 | uint8 version; 27 | uint32 timestamp; 28 | uint32 nonce; 29 | uint16 emitterChainId; 30 | bytes32 emitterAddress; 31 | uint64 sequence; 32 | uint8 consistencyLevel; 33 | bytes payload; 34 | uint32 guardianSetIndex; 35 | Signature[] signatures; 36 | bytes32 hash; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/interfaces/IUniswapWormholeMessageReceiver.sol: -------------------------------------------------------------------------------- 1 | // contracts/Structs.sol 2 | // SPDX-License-Identifier: Apache 2 3 | 4 | pragma solidity ^0.8.7; 5 | 6 | interface IUniswapWormholeMessageReceiver { 7 | struct Provider { 8 | uint16 chainId; 9 | uint16 governanceChainId; 10 | bytes32 governanceContract; 11 | } 12 | 13 | struct GuardianSet { 14 | address[] keys; 15 | uint32 expirationTime; 16 | } 17 | 18 | struct Signature { 19 | bytes32 r; 20 | bytes32 s; 21 | uint8 v; 22 | uint8 guardianIndex; 23 | } 24 | 25 | struct VM { 26 | uint8 version; 27 | uint32 timestamp; 28 | uint32 nonce; 29 | uint16 emitterChainId; 30 | bytes32 emitterAddress; 31 | uint64 sequence; 32 | uint8 consistencyLevel; 33 | bytes payload; 34 | uint32 guardianSetIndex; 35 | Signature[] signatures; 36 | bytes32 hash; 37 | } 38 | 39 | function receiveMessage(bytes memory whMessage) external payable; 40 | } 41 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | workflow_dispatch: 5 | pull_request: 6 | push: 7 | branches: 8 | - main 9 | 10 | env: 11 | FOUNDRY_PROFILE: ci 12 | 13 | jobs: 14 | tests: 15 | strategy: 16 | fail-fast: true 17 | name: forge tests 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v3 21 | with: 22 | submodules: recursive 23 | - name: Install Foundry 24 | uses: foundry-rs/foundry-toolchain@v1 25 | with: 26 | version: nightly 27 | 28 | - name: Run Forge build 29 | run: | 30 | forge --version 31 | forge build --sizes 32 | id: build 33 | 34 | - name: Run Forge tests 35 | run: | 36 | forge test -vvv 37 | id: test 38 | solhint: 39 | name: solhint linter 40 | runs-on: ubuntu-latest 41 | steps: 42 | - uses: actions/checkout@v2 43 | - name: Install solhint 44 | run: | 45 | npm install -g solhint 46 | - name: Run solhint 47 | run: | 48 | solhint 'src/**/*.sol' -------------------------------------------------------------------------------- /test/MockGovernanceReceiver.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | contract MockGovernanceReceiver { 5 | address trustedCaller; 6 | 7 | // governance action values 8 | uint256 public constant governanceValueOne = 1e13; 9 | uint256 public constant governanceValueTwo = 1e17; 10 | 11 | mapping(bytes32 => bool) public consumedActions; 12 | 13 | constructor(address _trustedCaller) { 14 | require(_trustedCaller != address(0), "invalid caller address"); 15 | 16 | trustedCaller = _trustedCaller; 17 | } 18 | 19 | function receiveGovernanceMessageOne(bytes32 governanceAction, uint8 governanceType) public payable { 20 | require(governanceType == 1, "invalid governance type"); 21 | require(msg.sender == trustedCaller, "unknown caller"); 22 | require(!consumedActions[governanceAction], "action already consumed"); 23 | require(msg.value == governanceValueOne, "not enough value"); 24 | 25 | consumedActions[governanceAction] = true; 26 | } 27 | 28 | function receiveGovernanceMessageTwo(bytes32 governanceAction, uint8 governanceType) public payable { 29 | require(governanceType == 2, "invalid governance type"); 30 | require(msg.sender == trustedCaller, "unknown caller"); 31 | require(!consumedActions[governanceAction], "action already consumed"); 32 | require(msg.value == governanceValueTwo, "not enough value"); 33 | 34 | consumedActions[governanceAction] = true; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Uniswap-Wormhole-Bridge 2 | 3 | This repository defines the Uniswap Wormhole Sender and Uniswap Wormhole Receiver smart contracts that are leveraged by the Uniswap Timelock contract to perform governance on Uniswap V3 Deployments on remote blockchains. 4 | 5 | ## Basic Architecture 6 | 7 | The following defines the basic architecture by which the Uniswap Wormhole Sender and Uniswap Wormhole Receiver smart contracts are utilized. 8 | 9 | The intent here is to provide an effective transport layer by which the Uniswap Timelock contract can conduct governance on Uniswap V3 deployments on remote blockchains via Wormhole. 10 | 11 | ![Architecture](./images/architecture.png) 12 | 13 | ## Uniswap Wormhole Sender 14 | 15 | The Uniswap Wormhole Sender contract is designed to be deployed on Ethereum by which the Uniswap timelock contract can enact governance actions on EVM-based deployments outside Ethereum. 16 | 17 | This contract is a single contract, which can then send messages to one or more remote deployments. The Uniswap Wormhole Sender contract must give the owner rights to the Uniswap Timelock contract for this message sending to be effective. 18 | 19 | ## Uniswap Wormhole Receiver 20 | 21 | The Uniswap Wormhole Receiver contract is designed to be deployed on remote EVM-based Uniswap V3 deployments and should only accept messages, which are initiated from the sender and are destined for the deployed blockchain they are on. 22 | 23 | This contract should also invalidate the Wormhole VAAs past a predefined validity window and additionally enforce sequencing such that sequency numbers are only ever monotonically increasing to be valid. So if there are two valid VAAs (3 and 4), if 4 is consumed first, 3 will no longer be valid. 24 | 25 | ## Deployment 26 | 27 | If a sender contract is already deployed, you should not need to deploy a new one, you can re-use the sender contract to send governance messages to multiple remote deployments. If the sender is not yet deployed, you need deploy it on chain, and then call setOwner with the Uniswap Timelock contract to transfer ownership to Uniswap Governance. 28 | 29 | During the initial deployment, you will need to additionally provide the Wormhole Core Bridge contract address from Ethereum as an argument. The Wormhole Core Bridge contract address information can be found [here](https://book.wormhole.com/reference/contracts.html). 30 | 31 | If you are deploying the receiver to a new chain, you will also need to supply a construction argument to define which chain ID you are deploying on. You can find specific information on Wormhole chain IDs [here](https://github.com/wormhole-foundation/wormhole/blob/main/sdk/js/src/utils/consts.ts#L1). Additionally, at construction time, you will need to supply both the Wormhole Core Bridge contract address for the chain you are deploying on and the current contract address for the sender you want to trust. 32 | 33 | If you are redeploying the receiver to an existing chain, you will want to deploy the latest copy of the receiver contract for that chain with any changes desired. Similarly, the construction addresses for the Wormhole Core Contracts and sender will need to be supplied. Additionally, you will need to use the current receiver implementation to send a governance message to the Uniswap V3 deployment on that chain to update it's trusted receiver to point to the new implementation. 34 | 35 | ## Unit Tests 36 | 37 | Unit tests are written using [forge](https://github.com/foundry-rs/foundry). To run the tests, [install forge](https://getfoundry.sh/) and then: 38 | 39 | ```forge test``` 40 | 41 | To check the test coverage: 42 | 43 | ```forge coverage``` -------------------------------------------------------------------------------- /src/UniswapWormholeMessageSender.sol: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright Uniswap Foundation 2023 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with 5 | * the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on 10 | * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the 11 | * specific language governing permissions and limitations under the License. 12 | * 13 | * SPDX-License-Identifier: Apache-2.0 14 | */ 15 | pragma solidity ^0.8.7; 16 | 17 | interface IWormhole { 18 | function publishMessage(uint32 nonce, bytes memory payload, uint8 consistencyLevel) 19 | external 20 | payable 21 | returns (uint64 sequence); 22 | function messageFee() external view returns (uint256); 23 | } 24 | 25 | bytes32 constant messagePayloadVersion = keccak256( 26 | abi.encode( 27 | "UniswapWormholeMessageSenderV1 (bytes32 receivedMessagePayloadVersion, address[] memory targets, uint256[] memory values, bytes[] memory datas, address messageReceiver, uint16 receiverChainId)" 28 | ) 29 | ); 30 | 31 | function generateMessagePayload( 32 | address[] memory _targets, 33 | uint256[] memory _values, 34 | bytes[] memory _calldatas, 35 | address _messageReceiver, 36 | uint16 _receiverChainId 37 | ) pure returns (bytes memory) { 38 | // SECURITY: Anytime this format is changed, messagePayloadVersion should be updated. 39 | return abi.encode(messagePayloadVersion, _targets, _values, _calldatas, _messageReceiver, _receiverChainId); 40 | } 41 | 42 | contract UniswapWormholeMessageSender { 43 | string public constant NAME = "Uniswap Wormhole Message Sender"; 44 | 45 | // address of the permissioned message sender 46 | address public owner; 47 | 48 | // `nonce` in Wormhole is a misnomer and can be safely set to a constant value. 49 | uint32 public constant NONCE = 0; 50 | 51 | /** 52 | * consistencyLevel = 1 means finalized on Ethereum, see https://book.wormhole.com/wormhole/3_coreLayerContracts.html#consistency-levels 53 | * 54 | * WARNING: Be mindful that if the sender is ever adapted to support multiple consistency levels, the sequence number 55 | * enforcement in the receiver could result in delivery of a message with a higher sequence number first and thus 56 | * invalidate the lower sequence number message from being processable on the receiver. As long as CONSISTENCY_LEVEL 57 | * remains a constant this is a non-issue. If this changes, changes to the receiver may be required to address messages 58 | * of variable consistency. 59 | */ 60 | uint8 public constant CONSISTENCY_LEVEL = 1; 61 | 62 | /** 63 | * @notice This event is emitted when a Wormhole message is published. 64 | * @param payload Encoded payload emitted by the Wormhole core contract. 65 | * @param messageReceiver Recipient contract of the emitted Wormhole message. 66 | */ 67 | event MessageSent(bytes payload, address indexed messageReceiver); 68 | 69 | // Wormhole core contract interface 70 | IWormhole private immutable wormhole; 71 | 72 | /** 73 | * @param wormholeAddress Address of Wormhole core messaging contract on this chain. 74 | */ 75 | constructor(address wormholeAddress) { 76 | // sanity check constructor args 77 | require(wormholeAddress != address(0), "Invalid wormhole address"); 78 | 79 | wormhole = IWormhole(wormholeAddress); 80 | owner = msg.sender; 81 | } 82 | 83 | /** 84 | * @param targets array of target addresses 85 | * @param values array of values 86 | * @param calldatas array of calldatas 87 | * @param messageReceiver address of the receiver contract 88 | * @param receiverChainId chain id of the receiver chain 89 | */ 90 | function sendMessage( 91 | address[] memory targets, 92 | uint256[] memory values, 93 | bytes[] memory calldatas, 94 | address messageReceiver, 95 | uint16 receiverChainId 96 | ) external payable onlyOwner { 97 | // cache wormhole instance and verify that the caller sent enough value to cover the Wormhole message fee 98 | IWormhole _wormhole = wormhole; 99 | uint256 messageFee = _wormhole.messageFee(); 100 | 101 | require(msg.value == messageFee, "invalid message fee"); 102 | require(receiverChainId != 2, "invalid receiverChainID Ethereum"); 103 | require(receiverChainId != 0, "invalid receiverChainID Unset"); 104 | 105 | // format the message payload 106 | bytes memory payload = generateMessagePayload(targets, values, calldatas, messageReceiver, receiverChainId); 107 | 108 | // send the payload by invoking the Wormhole core contract 109 | _wormhole.publishMessage{value: messageFee}(NONCE, payload, CONSISTENCY_LEVEL); 110 | 111 | emit MessageSent(payload, messageReceiver); 112 | } 113 | 114 | /** 115 | * @notice Transfers ownership to `newOwner`. 116 | * @param newOwner Address of the `newOwner`. 117 | */ 118 | function setOwner(address newOwner) external onlyOwner { 119 | require(newOwner != address(0), "newOwner cannot equal address(0)"); 120 | 121 | owner = newOwner; 122 | } 123 | 124 | modifier onlyOwner() { 125 | require(msg.sender == owner, "sender not owner"); 126 | _; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/UniswapWormholeMessageReceiver.sol: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright Uniswap Foundation 2023 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with 5 | * the License. You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on 10 | * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the 11 | * specific language governing permissions and limitations under the License. 12 | * 13 | * SPDX-License-Identifier: Apache-2.0 14 | */ 15 | pragma solidity ^0.8.7; 16 | 17 | // solhint-disable-next-line no-global-import 18 | import "./Structs.sol"; 19 | 20 | interface IWormhole { 21 | function parseAndVerifyVM(bytes calldata encodedVM) 22 | external 23 | view 24 | returns (Structs.VM memory vm, bool valid, string memory reason); 25 | } 26 | 27 | /** 28 | * @title Uniswap Wormhole Message Receiver 29 | * @dev This contract receives and executes Uniswap governance proposals that were sent from the UniswapWormholeMessageSender 30 | * contract on Ethereum via Wormhole. 31 | * 32 | * It enforces that proposals are executed in order, but it does not guarantee that all proposals are executed. 33 | * i.e. The message sequence number of proposals must be strictly monotonically increasing, but need not be consecutive 34 | * The maximum number of proposals that can be received is therefore UINT64_MAX. 35 | * For example, if there are proposals 1, 2 and 3, then the following are valid executions (not exhaustive): 36 | * - 1,2,3 37 | * - 1,3 38 | * But the following are impossible (not exhaustive): 39 | * - 1,3,2 40 | */ 41 | contract UniswapWormholeMessageReceiver { 42 | string public constant NAME = "Uniswap Wormhole Message Receiver"; 43 | bytes32 public constant EXPECTED_MESSAGE_PAYLOAD_VERSION = keccak256( 44 | abi.encode( 45 | "UniswapWormholeMessageSenderV1 (bytes32 receivedMessagePayloadVersion, address[] memory targets, uint256[] memory values, bytes[] memory datas, address messageReceiver, uint16 receiverChainId)" 46 | ) 47 | ); 48 | 49 | // Address of the UniswapWormholeMessageSender contract on ethereum in Wormhole format, 50 | // i.e. 12 zero bytes followed by a 20-byte Ethereum address. 51 | bytes32 public immutable messageSender; 52 | 53 | // A uint16 definining the destination chain of a VAA this contract will trust 54 | uint16 public immutable chainId; 55 | 56 | // A uint16 definining the source chain of a VAA this contract will trust 57 | uint16 public constant ETHEREUM_CHAIN_ID = 2; 58 | 59 | IWormhole private immutable wormhole; 60 | 61 | // the next message must have at least this sequence number 62 | uint64 public nextMinimumSequence = 0; 63 | 64 | /** 65 | * Message timeout in seconds: Time out needs to account for: 66 | * - Finality time on source chain 67 | * - Time for Wormhole validators to sign and make VAA available to relayers 68 | * - Time to relay VAA to the target chain 69 | * - Congestion on target chain leading to delayed inclusion of transaction in target chain 70 | * 71 | * Note that there is no way to alter this hard coded value. Including such a feature 72 | * would require some governance structure and some minumum and maximum values. 73 | */ 74 | uint256 public constant MESSAGE_TIME_OUT_SECONDS = 2 days; 75 | 76 | /** 77 | * @param wormholeAddress Address of Wormhole core messaging contract on this chain. 78 | * @param _messageSender Address of the UniswapWormholeMessageSender contract on ethereum in Wormhole format, 79 | * i.e. 12 zero bytes followed by a 20-byte Ethereum address. 80 | */ 81 | constructor(address wormholeAddress, bytes32 _messageSender, uint16 _chainId) { 82 | // sanity check constructor args 83 | require(wormholeAddress != address(0), "Invalid wormhole address"); 84 | require(_messageSender != bytes32(0) && bytes12(_messageSender) == 0, "Invalid sender contract"); 85 | require(_chainId != ETHEREUM_CHAIN_ID, "Invalid chainId Ethereum"); 86 | 87 | wormhole = IWormhole(wormholeAddress); 88 | messageSender = _messageSender; 89 | chainId = _chainId; 90 | } 91 | 92 | /** 93 | * @param whMessage Wormhole message relayed from a source chain. 94 | */ 95 | function receiveMessage(bytes calldata whMessage) external payable { 96 | (Structs.VM memory vm, bool valid, string memory reason) = wormhole.parseAndVerifyVM(whMessage); 97 | 98 | // validate 99 | require(valid, reason); 100 | 101 | // ensure the emitterAddress of this VAA is the Uniswap message sender 102 | require(messageSender == vm.emitterAddress, "Invalid Emitter Address!"); 103 | 104 | // ensure the emitterChainId is Ethereum to prevent impersonation 105 | require(vm.emitterChainId == ETHEREUM_CHAIN_ID, "Invalid Emitter Chain"); 106 | 107 | /** 108 | * Ensure that the sequence field in the VAA is strictly monotonically increasing. This also acts as 109 | * a replay protection mechanism to ensure that already executed messages don't execute again. 110 | * 111 | * WARNING: Be mindful that if the sender is ever adapted to support multiple consistency levels, the sequence number 112 | * enforcement in the receiver could result in delivery of a message with a higher sequence number first and thus 113 | * invalidate the lower sequence number message from being processable on the receiver. As long as CONSISTENCY_LEVEL 114 | * remains a constant this is a non-issue. If this changes, changes to the receiver may be required to address messages 115 | * of variable consistency. 116 | */ 117 | require(vm.sequence >= nextMinimumSequence, "Invalid Sequence number"); 118 | // increase nextMinimumSequence 119 | nextMinimumSequence = vm.sequence + 1; 120 | 121 | // check if the message is still valid as defined by the validity period 122 | // solhint-disable-next-line not-rely-on-time 123 | require(vm.timestamp + MESSAGE_TIME_OUT_SECONDS >= block.timestamp, "Message no longer valid"); 124 | 125 | // verify destination 126 | ( 127 | bytes32 receivedMessagePayloadVersion, 128 | address[] memory targets, 129 | uint256[] memory values, 130 | bytes[] memory calldatas, 131 | address messageReceiver, 132 | uint16 receiverChainId 133 | ) = abi.decode(vm.payload, (bytes32, address[], uint256[], bytes[], address, uint16)); 134 | require(EXPECTED_MESSAGE_PAYLOAD_VERSION == receivedMessagePayloadVersion, "Wrong payload version"); 135 | require(messageReceiver == address(this), "Message not for this dest"); 136 | require(receiverChainId == chainId, "Message not for this chain"); 137 | 138 | // cache target length and verify that each argument has the same length 139 | uint256 targetsLength = targets.length; 140 | require(targetsLength == calldatas.length && targetsLength == values.length, "Inconsistent argument lengths"); 141 | 142 | // verify that the caller sent enough value to make each target call 143 | require(verifyTargetValues(values), "Incorrect value"); 144 | 145 | // execute each message 146 | for (uint256 i = 0; i < targetsLength;) { 147 | (bool success,) = targets[i].call{value: values[i]}(calldatas[i]); 148 | require(success, "Sub-call failed"); 149 | 150 | unchecked { 151 | i += 1; 152 | } 153 | } 154 | } 155 | 156 | function verifyTargetValues(uint256[] memory values) internal view returns (bool) { 157 | uint256 valuesSum; 158 | 159 | uint256 valuesLength = values.length; 160 | for (uint256 i = 0; i < valuesLength;) { 161 | valuesSum += values[i]; 162 | 163 | unchecked { 164 | i += 1; 165 | } 166 | } 167 | 168 | return valuesSum == msg.value; 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /test/UniswapWormholeMessageSenderReceiver.t.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: UNLICENSED 2 | pragma solidity ^0.8.13; 3 | 4 | import "forge-std/Test.sol"; 5 | import {UniswapWormholeMessageReceiver} from "../src/UniswapWormholeMessageReceiver.sol"; 6 | import {IUniswapWormholeMessageReceiver} from "../src/interfaces/IUniswapWormholeMessageReceiver.sol"; 7 | import {UniswapWormholeMessageSender, generateMessagePayload} from "../src/UniswapWormholeMessageSender.sol"; 8 | import {IUniswapWormholeMessageSender} from "../src/interfaces/IUniswapWormholeMessageSender.sol"; 9 | import {Messages} from "wormhole/contracts/Messages.sol"; 10 | import {IWormhole} from "wormhole/contracts/interfaces/IWormhole.sol"; 11 | import "wormhole/contracts/Implementation.sol"; 12 | import "wormhole/contracts/Setup.sol"; 13 | import {Wormhole} from "wormhole/contracts/Wormhole.sol"; 14 | import {MockGovernanceReceiver} from "./MockGovernanceReceiver.sol"; 15 | 16 | interface IMockGovernanceReceiver { 17 | function governanceValueOne() external returns (uint256); 18 | function governanceValueTwo() external returns (uint256); 19 | function consumedActions(bytes32 action) external returns (bool); 20 | } 21 | 22 | contract UniswapWormholeMessageSenderReceiverTest is Test { 23 | IWormhole public wormhole; 24 | IUniswapWormholeMessageReceiver public uniReceiver; 25 | IUniswapWormholeMessageSender public uniSender; 26 | IMockGovernanceReceiver public mock; 27 | 28 | // Mock governance actions 29 | bytes32 constant governanceActionOne = 0x0000000000000000000000000000000000000000000000000000000000000069; 30 | bytes32 constant governanceActionTwo = 0x000000000000000000000000000000000000000000000000000000000000beef; 31 | 32 | // Test setup variables 33 | bytes32 constant msgSender = 0x0000000000000000000000000000000000000000000000000000000000000012; 34 | uint256 constant numGuardians = 19; 35 | uint256 constant quorumGuardians = 13; 36 | uint256 timestamp = 1641070800; 37 | uint16 ethereum_chain_id = 2; 38 | uint16 bsc_chain_id = 4; 39 | uint16 unset_chain_id = 0; 40 | 41 | address[] targets; 42 | uint256[] values; 43 | bytes[] datas; 44 | address[] incorrectLengthTargets; 45 | 46 | function setUp() public { 47 | // set up wormhole contracts 48 | wormhole = IWormhole(setupWormhole()); 49 | 50 | // set up uniswap wormhole message receiver contract 51 | address uniReceiverAddress = address(new UniswapWormholeMessageReceiver(address(wormhole), msgSender, bsc_chain_id)); 52 | uniReceiver = IUniswapWormholeMessageReceiver(uniReceiverAddress); 53 | 54 | // deploy the mock governance contract 55 | address mockReceiver = address(new MockGovernanceReceiver(uniReceiverAddress)); 56 | mock = IMockGovernanceReceiver(mockReceiver); 57 | 58 | // set up uniswap wormhole message sender contract 59 | address uniSenderAddress = address(new UniswapWormholeMessageSender(address(wormhole))); 60 | uniSender = IUniswapWormholeMessageSender(uniSenderAddress); 61 | 62 | // create calldata for the first mock governance action 63 | bytes memory encodedGovernanceActionOne = 64 | abi.encodeWithSignature("receiveGovernanceMessageOne(bytes32,uint8)", governanceActionOne, 1); 65 | 66 | targets.push(mockReceiver); 67 | values.push(mock.governanceValueOne()); 68 | datas.push(encodedGovernanceActionOne); 69 | } 70 | 71 | function setupWormhole() public returns (address) { 72 | Implementation wormholeImpl = new Implementation(); 73 | Setup wormholeSetup = new Setup(); 74 | 75 | Wormhole wormholeAddress = new Wormhole(address(wormholeSetup), new bytes(0)); 76 | 77 | address[] memory initSigners = new address[](numGuardians); 78 | 79 | for (uint256 i = 0; i < numGuardians; ++i) { 80 | initSigners[i] = vm.addr(i + 1); // i+1 is the private key for the i-th signer. 81 | } 82 | 83 | // These values are the default values used in our tilt test environment 84 | // and are not important. 85 | Setup(address(wormholeAddress)).setup( 86 | address(wormholeImpl), 87 | initSigners, 88 | bsc_chain_id, // BSC chain ID 89 | 1, // Governance source chain ID (1 = solana) 90 | 0x0000000000000000000000000000000000000000000000000000000000000004, // Governance source address 91 | block.chainid // evm chain Id 92 | ); 93 | return address(wormholeAddress); 94 | } 95 | 96 | function expectRevertWithValue( 97 | address contractAddress, 98 | bytes memory encodedSignature, 99 | string memory expectedRevert, 100 | uint256 value_ 101 | ) internal { 102 | (bool success, bytes memory result) = contractAddress.call{value: value_}(encodedSignature); 103 | require(!success, "call did not revert"); 104 | 105 | // fetch the revert string bytes 106 | bytes memory newResult; 107 | for (uint256 i = 0; i < result.length; ++i) { 108 | // skip signature 109 | if (i > 3) { 110 | newResult = abi.encodePacked(newResult, result[i]); 111 | } 112 | } 113 | 114 | // compare revert strings 115 | bytes32 expectedRevertHash = keccak256(abi.encode(expectedRevert)); 116 | bytes32 actualRevertHash = keccak256(newResult); 117 | require(expectedRevertHash == actualRevertHash, "call did not revert as expected"); 118 | } 119 | 120 | function simulateSignedVaa(bytes memory body, bytes32 _hash) internal pure returns (bytes memory vaa) { 121 | bytes memory signatures = new bytes(0); 122 | 123 | for (uint256 i = 0; i < quorumGuardians; ++i) { 124 | (uint8 v, bytes32 r, bytes32 s) = vm.sign(i + 1, _hash); 125 | signatures = abi.encodePacked( 126 | signatures, 127 | uint8(i), // Guardian index of the signature 128 | r, 129 | s, 130 | v - 27 // v is either 27 or 28. 27 is added to v in Eth (following BTC) but Wormhole doesn't use it. 131 | ); 132 | } 133 | 134 | vaa = abi.encodePacked( 135 | uint8(1), // Version 136 | uint32(0), // Guardian set index. it is initialized by 0 137 | uint8(quorumGuardians), 138 | signatures, 139 | body 140 | ); 141 | } 142 | 143 | function generateSignedVaa(uint16 emitterChainId, bytes32 emitterAddress, uint64 sequence, bytes memory payload) 144 | public 145 | returns (bytes memory) 146 | { 147 | vm.warp(timestamp); 148 | 149 | // format the message body 150 | bytes memory body = abi.encodePacked( 151 | uint32(block.timestamp), 152 | uint32(0), //nonce is zero 153 | emitterChainId, //emitter chain id for ethereum is 2 154 | emitterAddress, //expected emitter address 155 | sequence, //sequence 156 | uint8(1), //consistency level 157 | payload 158 | ); 159 | 160 | // compute the hash of the body 161 | bytes32 _hash = keccak256(abi.encodePacked(keccak256(body))); 162 | 163 | // return the signed VAA 164 | return simulateSignedVaa(body, _hash); 165 | } 166 | 167 | function updateWormholeMessageFee(uint256 newFee) internal { 168 | bytes32 coreModule = 0x00000000000000000000000000000000000000000000000000000000436f7265; 169 | 170 | // `SetMessageFee` governance payload 171 | bytes memory payload = abi.encodePacked(coreModule, uint8(3), uint16(wormhole.chainId()), newFee); 172 | 173 | // construct the `SetMessageFee` governance VAA 174 | bytes memory body = abi.encodePacked( 175 | uint32(block.timestamp), 176 | uint32(0), //nonce is zero 177 | uint16(1), //governance chain 178 | bytes32(0x0000000000000000000000000000000000000000000000000000000000000004), //governance contract 179 | uint64(0), //sequence 180 | uint8(1), //consistency level 181 | payload 182 | ); 183 | 184 | // compute the hash of the body 185 | bytes32 _hash = keccak256(abi.encodePacked(keccak256(body))); 186 | 187 | // update the message fee 188 | wormhole.submitSetMessageFee(simulateSignedVaa(body, _hash)); 189 | } 190 | 191 | function testReceiverConstructionBadDestinationChainIDEthereum() public { 192 | // set up wormhole contracts 193 | wormhole = IWormhole(setupWormhole()); 194 | 195 | // set up uniswap wormhole message receiver contract, with Ethereum as the destination chain ID 196 | vm.expectRevert('Invalid chainId Ethereum'); 197 | address(new UniswapWormholeMessageReceiver(address(wormhole), msgSender, ethereum_chain_id)); 198 | } 199 | 200 | function testUpdateWormholeMessageFee(uint256 newFee) public { 201 | uint256 currentFee = wormhole.messageFee(); 202 | 203 | vm.assume(currentFee != newFee); 204 | 205 | updateWormholeMessageFee(newFee); 206 | 207 | // verify the state change 208 | currentFee = wormhole.messageFee(); 209 | assertEq(currentFee, newFee); 210 | } 211 | 212 | function testSendMessageFailureZeroMessageFee(uint256 messageFee) public { 213 | vm.assume(messageFee > 0); 214 | 215 | // update the wormhole message fee 216 | updateWormholeMessageFee(messageFee); 217 | 218 | vm.expectRevert("invalid message fee"); 219 | uniSender.sendMessage{value: 0}(targets, values, datas, address(uniReceiver), bsc_chain_id); 220 | } 221 | 222 | function testSendMessageFailureBadReceiverChainEthereum() public { 223 | uint256 currentFee = wormhole.messageFee(); 224 | uint256 newFee = currentFee + 1; 225 | 226 | // update the wormhole message fee 227 | updateWormholeMessageFee(newFee); 228 | 229 | vm.expectRevert('invalid receiverChainID Ethereum'); 230 | uniSender.sendMessage{value: newFee}(targets, values, datas, address(uniReceiver), ethereum_chain_id); 231 | } 232 | 233 | function testSendMessageFailureBadReceiverChainUnset() public { 234 | uint256 currentFee = wormhole.messageFee(); 235 | uint256 newFee = currentFee + 1; 236 | 237 | // update the wormhole message fee 238 | updateWormholeMessageFee(newFee); 239 | 240 | vm.expectRevert('invalid receiverChainID Unset'); 241 | uniSender.sendMessage{value: newFee}(targets, values, datas, address(uniReceiver), unset_chain_id); 242 | } 243 | 244 | function testSendMessageFailureMessageFeeTooLarge() public { 245 | // update the wormhole message fee 246 | uint256 messageFee = 1e6; 247 | updateWormholeMessageFee(messageFee); 248 | 249 | // call `sendMessage` with a fee greater than what is set in the wormhole contract 250 | uint256 invalidFee = 1e18; 251 | 252 | vm.expectRevert("invalid message fee"); 253 | uniSender.sendMessage{value: invalidFee}(targets, values, datas, address(uniReceiver), bsc_chain_id); 254 | } 255 | 256 | function testSendMessageFailureMessageFeeTooLarge2() public { 257 | // call `sendMessage` with a fee greater than what is set in the wormhole contract 258 | uint256 fee = 100; 259 | 260 | vm.expectRevert("invalid message fee"); 261 | uniSender.sendMessage{value: fee}(targets, values, datas, address(uniReceiver), bsc_chain_id); 262 | } 263 | 264 | function testReceiveMessageSuccessWithOneAction() public { 265 | uint64 sequence = 0; 266 | uint16 emitterChainId = 2; 267 | 268 | bytes memory payload = generateMessagePayload(targets, values, datas, address(uniReceiver), bsc_chain_id); 269 | bytes memory whMessage = generateSignedVaa(emitterChainId, msgSender, sequence, payload); 270 | 271 | vm.warp(timestamp + 45 minutes); 272 | uniReceiver.receiveMessage{value: mock.governanceValueOne()}(whMessage); 273 | 274 | // confirm that the mock contract received the governance action 275 | assertEq(mock.consumedActions(governanceActionOne), true); 276 | 277 | // test that it still works with gaps in the sequence numbers 278 | sequence = 100; 279 | 280 | // create second governance action signature 281 | bytes memory encodedGovernanceActionTwo = 282 | abi.encodeWithSignature("receiveGovernanceMessageTwo(bytes32,uint8)", governanceActionTwo, 2); 283 | 284 | targets[0] = address(mock); 285 | values[0] = mock.governanceValueTwo(); 286 | datas[0] = encodedGovernanceActionTwo; 287 | payload = generateMessagePayload(targets, values, datas, address(uniReceiver), bsc_chain_id); 288 | whMessage = generateSignedVaa(emitterChainId, msgSender, sequence, payload); 289 | vm.warp(timestamp + 45 minutes); 290 | uniReceiver.receiveMessage{value: mock.governanceValueTwo()}(whMessage); 291 | } 292 | 293 | function testReceiveMessageSuccessWithTwoActions() public { 294 | // create second governance action signature 295 | bytes memory encodedGovernanceActionTwo = 296 | abi.encodeWithSignature("receiveGovernanceMessageTwo(bytes32,uint8)", governanceActionTwo, 2); 297 | 298 | // create local instance of targets/values/datas arrays 299 | address[] memory _targets = new address[](2); 300 | uint256[] memory _values = new uint256[](2); 301 | bytes[] memory _datas = new bytes[](2); 302 | 303 | // update the local arrays with the first governance action 304 | _targets[0] = targets[0]; 305 | _values[0] = values[0]; 306 | _datas[0] = datas[0]; 307 | 308 | // update the local arrays with the second governance action 309 | _targets[1] = address(mock); 310 | _values[1] = mock.governanceValueTwo(); 311 | _datas[1] = encodedGovernanceActionTwo; 312 | 313 | // other test variables 314 | uint64 sequence = 1; 315 | uint16 emitterChainId = 2; 316 | bytes memory payload = generateMessagePayload(_targets, _values, _datas, address(uniReceiver), bsc_chain_id); 317 | bytes memory whMessage = generateSignedVaa(emitterChainId, msgSender, sequence, payload); 318 | 319 | vm.warp(timestamp + 45 minutes); 320 | uint256 multiActionValue = mock.governanceValueOne() + mock.governanceValueTwo(); 321 | uniReceiver.receiveMessage{value: multiActionValue}(whMessage); 322 | 323 | // confirm that the mock contract received the governance action 324 | assertEq(mock.consumedActions(governanceActionOne), true); 325 | assertEq(mock.consumedActions(governanceActionTwo), true); 326 | } 327 | 328 | function testInvalidSubCall() public { 329 | uint64 sequence = 1; 330 | uint16 emitterChainId = 2; 331 | 332 | // create bad datas array 333 | bytes[] memory badDatas = new bytes[](1); 334 | badDatas[0] = abi.encodeWithSignature("receiveGovernanceMessageOne(bytes32,uint8)", governanceActionOne, 420); // bad action 335 | 336 | bytes memory payload = generateMessagePayload(targets, values, badDatas, address(uniReceiver), bsc_chain_id); 337 | bytes memory whMessage = generateSignedVaa(emitterChainId, msgSender, sequence, payload); 338 | 339 | vm.warp(timestamp + 45 minutes); 340 | 341 | // note Sometimes forge cannot correctly match the revert string from a call. The 342 | // expectRevertWithValue performs the same function as vm.expectRevert. 343 | bytes memory encodedSignature = abi.encodeWithSignature("receiveMessage(bytes)", whMessage); 344 | expectRevertWithValue(address(uniReceiver), encodedSignature, "Sub-call failed", mock.governanceValueOne()); 345 | 346 | // confirm that the mock contract did not receive the governance action 347 | assertEq(mock.consumedActions(governanceActionOne), false); 348 | } 349 | 350 | function testIncorrectValueWithOneAction(uint256 _value) public { 351 | vm.assume(_value != mock.governanceValueOne() && _value < type(uint96).max); 352 | 353 | uint64 sequence = 1; 354 | uint16 emitterChainId = 2; 355 | 356 | bytes memory payload = generateMessagePayload(targets, values, datas, address(uniReceiver), bsc_chain_id); 357 | bytes memory whMessage = generateSignedVaa(emitterChainId, msgSender, sequence, payload); 358 | 359 | vm.warp(timestamp + 45 minutes); 360 | vm.expectRevert("Incorrect value"); 361 | uniReceiver.receiveMessage{value: _value}(whMessage); 362 | } 363 | 364 | function testIncorrectValueWithTwoActions(uint256 _value) public { 365 | vm.assume(_value != mock.governanceValueOne() + mock.governanceValueTwo() && _value < type(uint96).max); 366 | 367 | // create second governance action signature 368 | bytes memory encodedGovernanceActionTwo = 369 | abi.encodeWithSignature("receiveGovernanceMessageTwo(bytes32,uint8)", governanceActionTwo, 2); 370 | 371 | // create local instance of targets/values/datas arrays 372 | address[] memory _targets = new address[](2); 373 | uint256[] memory _values = new uint256[](2); 374 | bytes[] memory _datas = new bytes[](2); 375 | 376 | // update the local arrays with the first governance action 377 | _targets[0] = targets[0]; 378 | _values[0] = values[0]; 379 | _datas[0] = datas[0]; 380 | 381 | // update the local arrays with the second governance action 382 | _targets[1] = address(mock); 383 | _values[1] = mock.governanceValueTwo(); 384 | _datas[1] = encodedGovernanceActionTwo; 385 | 386 | // other test variables 387 | uint64 sequence = 1; 388 | uint16 emitterChainId = 2; 389 | bytes memory payload = generateMessagePayload(_targets, _values, _datas, address(uniReceiver), bsc_chain_id); 390 | bytes memory whMessage = generateSignedVaa(emitterChainId, msgSender, sequence, payload); 391 | 392 | vm.warp(timestamp + 45 minutes); 393 | vm.expectRevert("Incorrect value"); 394 | uniReceiver.receiveMessage{value: _value}(whMessage); 395 | } 396 | 397 | function testInvalidEmitterAddress() public { 398 | uint64 sequence = 1; 399 | 400 | bytes memory payload = generateMessagePayload(targets, values, datas, address(uniReceiver), bsc_chain_id); 401 | bytes memory whMessage = generateSignedVaa(ethereum_chain_id, bytes32(uint256(8)), sequence, payload); 402 | 403 | vm.warp(timestamp + 45 minutes); 404 | vm.expectRevert("Invalid Emitter Address!"); 405 | uniReceiver.receiveMessage(whMessage); 406 | } 407 | 408 | function testInvalidEmitterChainId() public { 409 | uint64 sequence = 1; 410 | 411 | bytes memory payload = generateMessagePayload(targets, values, datas, address(uniReceiver), bsc_chain_id); 412 | bytes memory whMessage = generateSignedVaa(ethereum_chain_id - 1, msgSender, sequence, payload); 413 | 414 | vm.warp(timestamp + 45 minutes); 415 | vm.expectRevert("Invalid Emitter Chain"); 416 | uniReceiver.receiveMessage(whMessage); 417 | } 418 | 419 | function testReplay() public { 420 | uint64 sequence = 1; 421 | 422 | bytes memory payload = generateMessagePayload(targets, values, datas, address(uniReceiver), bsc_chain_id); 423 | bytes memory whMessage = generateSignedVaa(ethereum_chain_id, msgSender, sequence, payload); 424 | 425 | vm.warp(timestamp + 45 minutes); 426 | uniReceiver.receiveMessage{value: mock.governanceValueOne()}(whMessage); 427 | 428 | vm.expectRevert("Invalid Sequence number"); 429 | uniReceiver.receiveMessage(whMessage); 430 | } 431 | 432 | function testInvalidSequence() public { 433 | uint64 sequence = 2; 434 | 435 | bytes memory payload = generateMessagePayload(targets, values, datas, address(uniReceiver), bsc_chain_id); 436 | bytes memory whMessage = generateSignedVaa(ethereum_chain_id, msgSender, sequence, payload); 437 | 438 | vm.warp(timestamp + 45 minutes); 439 | uniReceiver.receiveMessage{value: mock.governanceValueOne()}(whMessage); 440 | 441 | whMessage = generateSignedVaa(ethereum_chain_id, msgSender, sequence - 1, payload); 442 | 443 | vm.expectRevert("Invalid Sequence number"); 444 | uniReceiver.receiveMessage(whMessage); 445 | } 446 | 447 | function testMessageTimeout() public { 448 | uint64 sequence = 2; 449 | 450 | bytes memory payload = generateMessagePayload(targets, values, datas, address(uniReceiver), bsc_chain_id); 451 | bytes memory whMessage = generateSignedVaa(ethereum_chain_id, msgSender, sequence, payload); 452 | 453 | vm.warp(timestamp + 2881 minutes); 454 | vm.expectRevert("Message no longer valid"); 455 | uniReceiver.receiveMessage(whMessage); 456 | } 457 | 458 | function testInconsistentPayload() public { 459 | uint64 sequence = 2; 460 | 461 | bytes memory payload = 462 | generateMessagePayload(incorrectLengthTargets, values, datas, address(uniReceiver), bsc_chain_id); 463 | bytes memory whMessage = generateSignedVaa(ethereum_chain_id, msgSender, sequence, payload); 464 | 465 | vm.warp(timestamp + 45 minutes); 466 | vm.expectRevert("Inconsistent argument lengths"); 467 | uniReceiver.receiveMessage(whMessage); 468 | } 469 | 470 | function testInvalidReceiverAddress() public { 471 | uint64 sequence = 2; 472 | 473 | address invalidReceiver = address(uint160(2023)); 474 | bytes memory payload = generateMessagePayload(targets, values, datas, invalidReceiver, bsc_chain_id); 475 | bytes memory whMessage = generateSignedVaa(ethereum_chain_id, msgSender, sequence, payload); 476 | 477 | vm.warp(timestamp + 45 minutes); 478 | vm.expectRevert("Message not for this dest"); 479 | uniReceiver.receiveMessage(whMessage); 480 | } 481 | 482 | function testInvalidReceiverChain() public { 483 | uint64 sequence = 2; 484 | 485 | bytes memory payload = generateMessagePayload(targets, values, datas, address(uniReceiver), bsc_chain_id - 1); 486 | bytes memory whMessage = generateSignedVaa(ethereum_chain_id, msgSender, sequence, payload); 487 | 488 | vm.warp(timestamp + 45 minutes); 489 | vm.expectRevert("Message not for this chain"); 490 | uniReceiver.receiveMessage(whMessage); 491 | } 492 | 493 | function testFailingSubcall() public { 494 | uint64 sequence = 2; 495 | 496 | address[] memory failingTargets = new address[](1); 497 | failingTargets[0] = 0xb4c79daB8f259C7Aee6E5b2Aa729821864227e84; 498 | 499 | bytes memory payload = 500 | generateMessagePayload(failingTargets, values, datas, address(uniReceiver), bsc_chain_id - 1); 501 | bytes memory whMessage = generateSignedVaa(ethereum_chain_id, msgSender, sequence, payload); 502 | 503 | vm.warp(timestamp + 45 minutes); 504 | vm.expectRevert("Sub-call failed"); 505 | uniReceiver.receiveMessage(whMessage); 506 | } 507 | 508 | // this is a modification of generateMessagePayload() because we need to control _messagePayloadVersion for the following tests. 509 | function generateMessagePayloadWithVersion( 510 | bytes32 _messagePayloadVersion, 511 | address[] memory _targets, 512 | uint256[] memory _values, 513 | bytes[] memory _calldatas, 514 | address _messageReceiver, 515 | uint16 _receiverChainId 516 | ) private pure returns (bytes memory) { 517 | return abi.encode(_messagePayloadVersion, _targets, _values, _calldatas, _messageReceiver, _receiverChainId); 518 | } 519 | 520 | function testInvalidMessageType() public { 521 | bytes32 correctMessagePayloadVersion = keccak256( 522 | abi.encode( 523 | "UniswapWormholeMessageSenderV1 (bytes32 receivedMessagePayloadVersion, address[] memory targets, uint256[] memory values, bytes[] memory datas, address messageReceiver, uint16 receiverChainId)" 524 | ) 525 | ); 526 | bytes32 invalidMessagePayloadVersion = keccak256(abi.encode("invalid")); 527 | 528 | // we are using the locally specified generateMessagePayloadWithVersion() here, so first make sure that it works by testing the happy case. 529 | uint64 sequence = 2; 530 | bytes memory payload = generateMessagePayloadWithVersion( 531 | correctMessagePayloadVersion, targets, values, datas, address(uniReceiver), bsc_chain_id 532 | ); 533 | bytes memory whMessage = generateSignedVaa(ethereum_chain_id, msgSender, sequence, payload); 534 | 535 | vm.warp(timestamp + 45 minutes); 536 | uniReceiver.receiveMessage{value: mock.governanceValueOne()}(whMessage); 537 | 538 | // now make sure that it fails with a wrong message type 539 | sequence = 3; 540 | payload = generateMessagePayloadWithVersion( 541 | invalidMessagePayloadVersion, targets, values, datas, address(uniReceiver), bsc_chain_id 542 | ); 543 | whMessage = generateSignedVaa(ethereum_chain_id, msgSender, sequence, payload); 544 | 545 | vm.warp(timestamp + 45 minutes); 546 | vm.expectRevert("Wrong payload version"); 547 | uniReceiver.receiveMessage(whMessage); 548 | } 549 | 550 | function testSetOwner(address newOwner) public { 551 | vm.assume(newOwner != address(0)); 552 | 553 | // call `setOwner` 554 | uniSender.setOwner(newOwner); 555 | 556 | // confirm state changes 557 | assertEq(uniSender.owner(), newOwner); 558 | } 559 | 560 | function testSetOwnerFailureZeroAddress() public { 561 | address newOwner = address(0); 562 | 563 | // expect the `setOwner` call to revert 564 | vm.expectRevert("newOwner cannot equal address(0)"); 565 | uniSender.setOwner(newOwner); 566 | } 567 | 568 | function testSetOwnerFailureOwnerOnly() public { 569 | address newOwner = address(this); 570 | 571 | // prank the caller's address 572 | vm.prank(makeAddr("notTheOwner")); 573 | 574 | // expect the `setOwner` call to revert 575 | vm.expectRevert("sender not owner"); 576 | uniSender.setOwner(newOwner); 577 | } 578 | } 579 | --------------------------------------------------------------------------------