Browse Source

Relayer: Ethereum folder Changes for Merging into Main (#3038)

* gRelayer: surrounding files

* modification to get compilation

* restore devnet

* remove generic relayer docker

* remove wait for relayer engine

* keep build time 20

* sh -> bash

* sh -> bash

* Remove comment

* bash -> sh

* Revert "bash -> sh"

This reverts commit 5c37e92fa19bbdbefc79c8ee0dbceeb127c53373.

* bash->sh

* gRelayer: ethereum folder changes for generic-relayer-merge

* add eth-devnet

* Adds .github because workflow needs to install forge

* sdk-ci-tests need to install forge

* don't wait for nonexistent relayer engine

* update package.json and package-lock.json

* Remove unnecessary types from package.json

* ts-node

* gRelayer: ethereum folder changes for generic-relayer-merge

* sdk-ci-tests need to install forge

* don't wait for nonexistent relayer engine

* update package.json and package-lock.json

* remove these changes

* Relayer: Natspec documentation in IWormholeRelayer (#3032)

* WIP

* Fixes

* Updated interfaces

* remove bash

* Forward uses same refund chain id and refund address (#3034)

* WIP

* Fixes

* Forward uses same refund chain id and refund address

* Updated interfaces

* Remove forge build warnings

* Add note to interface for resend

* via-ir on unless in Tilt

* Correct IWormholeReceiver interface

* Wormhole message fee now part of quoteDeliveryPrice (#3043)

* Fix to PR 3043

* Remove compiler warning

* Relayer/address drew review (#3060)

* Fix typo in Create2Factory

* Add event for contract upgrades

* Prevent registering contract if it is already registered

* Prevent allowing unset chainId for default delivery provider governance VAA

* memory to calldata for external functions in WormholeRelayerSend

* continue memory to calldata for external functions

* Fix pricing in delivery provider

* Sanity check new default delivery provider isn't 0 address

* Don't save vaaKey as local variable

* cache the length of array rather than iterate every time for vaaKeys

* Replacing memory with calldata in few locations

* Remove stale file DeliveryProviderMessages

* Remove batch VAA sender script

* Remove batch VAA from WormholeSimulator

* Wait for a confirmation in deploy scripts

* remove unnecessary comments

* Fix Delivery Provider Pricing and add a test

* remove console logs

* Revert "continue memory to calldata for external functions"

This reverts commit f322afb6c0bbd09e3d04ab42a90e592ff752f6bf.

* Revert "memory to calldata for external functions in WormholeRelayerSend"

This reverts commit 42fcaad8842d0c81506c9586d8d0fd98f6bb6ae1.

* Revert "Don't save vaaKey as local variable"

This reverts commit a9172379c564fd430a083645c1c42c78e014d68d.

* Revert "cache the length of array rather than iterate every time for vaaKeys"

This reverts commit d61380a9b0c0671e67e3bd5d874ae339e180dd34.

* Revert "Replacing memory with calldata in few locations"

This reverts commit 94e47b6e72eaaa52ac0ba2980c439180401fafd7.

* Revert "Fix typo in Create2Factory"

This reverts commit a9f7bdf461945c8abf020007d16bbc6b4301d051.

* Update contract addresses for via-ir

* Slight improvements to delivery provider implementation

* typed errors for delivery provider

* enable VIA-IR in CI and not in Tilt

* correct contract address for via ir

* WormholeRelayerSend and WormholeRelayerDelivery (#3082)
derpy-duck 2 years ago
parent
commit
7885acb9bd
97 changed files with 14579 additions and 233 deletions
  1. 1 1
      Tiltfile
  2. 9 6
      devnet/eth-devnet.yaml
  3. 9 7
      devnet/eth-devnet2.yaml
  4. 4 0
      ethereum/.dockerignore
  5. 2 0
      ethereum/.gitignore
  6. 28 4
      ethereum/Dockerfile
  7. 8 4
      ethereum/Makefile
  8. 6 1
      ethereum/README.md
  9. 42 0
      ethereum/contracts/interfaces/relayer/IDeliveryProvider.sol
  10. 44 0
      ethereum/contracts/interfaces/relayer/IDeliveryProviderTyped.sol
  11. 49 0
      ethereum/contracts/interfaces/relayer/IWormholeReceiver.sol
  12. 709 0
      ethereum/contracts/interfaces/relayer/IWormholeRelayer.sol
  13. 711 0
      ethereum/contracts/interfaces/relayer/IWormholeRelayerTyped.sol
  14. 263 0
      ethereum/contracts/interfaces/relayer/TypedUnits.sol
  15. 1267 0
      ethereum/contracts/libraries/relayer/BytesParsing.sol
  16. 94 0
      ethereum/contracts/libraries/relayer/ExecutionParameters.sol
  17. 66 0
      ethereum/contracts/libraries/relayer/RelayerInternalStructs.sol
  18. 94 0
      ethereum/contracts/libraries/relayer/Utils.sol
  19. 358 0
      ethereum/contracts/mock/relayer/MockRelayerIntegration.sol
  20. 79 0
      ethereum/contracts/relayer/create2Factory/Create2Factory.sol
  21. 188 0
      ethereum/contracts/relayer/deliveryProvider/DeliveryProvider.sol
  22. 65 0
      ethereum/contracts/relayer/deliveryProvider/DeliveryProviderGetters.sol
  23. 329 0
      ethereum/contracts/relayer/deliveryProvider/DeliveryProviderGovernance.sol
  24. 28 0
      ethereum/contracts/relayer/deliveryProvider/DeliveryProviderImplementation.sol
  25. 13 0
      ethereum/contracts/relayer/deliveryProvider/DeliveryProviderProxy.sol
  26. 77 0
      ethereum/contracts/relayer/deliveryProvider/DeliveryProviderSetters.sol
  27. 33 0
      ethereum/contracts/relayer/deliveryProvider/DeliveryProviderSetup.sol
  28. 54 0
      ethereum/contracts/relayer/deliveryProvider/DeliveryProviderState.sol
  29. 135 0
      ethereum/contracts/relayer/deliveryProvider/DeliveryProviderStructs.sol
  30. 34 0
      ethereum/contracts/relayer/wormholeRelayer/WormholeRelayer.sol
  31. 157 0
      ethereum/contracts/relayer/wormholeRelayer/WormholeRelayerBase.sol
  32. 688 0
      ethereum/contracts/relayer/wormholeRelayer/WormholeRelayerDelivery.sol
  33. 248 0
      ethereum/contracts/relayer/wormholeRelayer/WormholeRelayerGovernance.sol
  34. 571 0
      ethereum/contracts/relayer/wormholeRelayer/WormholeRelayerSend.sol
  35. 239 0
      ethereum/contracts/relayer/wormholeRelayer/WormholeRelayerSerde.sol
  36. 101 0
      ethereum/contracts/relayer/wormholeRelayer/WormholeRelayerStorage.sol
  37. 76 0
      ethereum/forge-test/relayer/AttackForwardIntegration.sol
  38. 38 0
      ethereum/forge-test/relayer/BigRevertBufferIntegration.sol
  39. 117 0
      ethereum/forge-test/relayer/ForwardTester.sol
  40. 206 0
      ethereum/forge-test/relayer/MockGenericRelayer.sol
  41. 289 0
      ethereum/forge-test/relayer/MockWormhole.sol
  42. 375 0
      ethereum/forge-test/relayer/RelayProvider.t.sol
  43. 134 0
      ethereum/forge-test/relayer/TestHelpers.sol
  44. 121 0
      ethereum/forge-test/relayer/TypedUnits.t.sol
  45. 2433 0
      ethereum/forge-test/relayer/WormholeRelayer.t.sol
  46. 202 0
      ethereum/forge-test/relayer/WormholeRelayerGovernance.t.sol
  47. 349 0
      ethereum/forge-test/relayer/WormholeSimulator.sol
  48. 15 8
      ethereum/foundry.toml
  49. 701 151
      ethereum/package-lock.json
  50. 26 22
      ethereum/package.json
  51. 5 0
      ethereum/remappings.txt
  52. 8 8
      ethereum/test/bridge.js
  53. 8 8
      ethereum/test/nft.js
  54. 12 12
      ethereum/test/wormhole.js
  55. 2 1
      ethereum/truffle-config.js
  56. 3 0
      ethereum/ts-scripts/relayer/.env.ci
  57. 5 0
      ethereum/ts-scripts/relayer/.env.kubernetes
  58. 2 0
      ethereum/ts-scripts/relayer/.env.tilt
  59. 1 0
      ethereum/ts-scripts/relayer/.gitignore
  60. 39 0
      ethereum/ts-scripts/relayer/README.md
  61. 52 0
      ethereum/ts-scripts/relayer/config/checkNetworks.ts
  62. 20 0
      ethereum/ts-scripts/relayer/config/ci/chains.json
  63. 44 0
      ethereum/ts-scripts/relayer/config/ci/contracts.json
  64. 41 0
      ethereum/ts-scripts/relayer/config/ci/scriptConfigs/configureDeliveryProvider.json
  65. 20 0
      ethereum/ts-scripts/relayer/config/kubernetes/chains.json
  66. 54 0
      ethereum/ts-scripts/relayer/config/kubernetes/contracts.json
  67. 41 0
      ethereum/ts-scripts/relayer/config/kubernetes/scriptConfigs/configureDeliveryProvider.json
  68. 71 0
      ethereum/ts-scripts/relayer/config/syncContractsJson.ts
  69. 78 0
      ethereum/ts-scripts/relayer/config/testnet/chains.json
  70. 92 0
      ethereum/ts-scripts/relayer/config/testnet/contracts.json
  71. 91 0
      ethereum/ts-scripts/relayer/config/testnet/scriptConfigs/configureDeliveryProvider.json
  72. 20 0
      ethereum/ts-scripts/relayer/config/tilt/chains.json
  73. 54 0
      ethereum/ts-scripts/relayer/config/tilt/contracts.json
  74. 37 0
      ethereum/ts-scripts/relayer/config/tilt/scriptConfigs/configureDeliveryProvider.json
  75. 22 0
      ethereum/ts-scripts/relayer/create2Factory/deployCreate2Factory.ts
  76. 108 0
      ethereum/ts-scripts/relayer/deliveryProvider/configureDeliveryProvider.ts
  77. 49 0
      ethereum/ts-scripts/relayer/deliveryProvider/deployDeliveryProvider.ts
  78. 180 0
      ethereum/ts-scripts/relayer/deliveryProvider/readDeliveryProviderContractState.ts
  79. 41 0
      ethereum/ts-scripts/relayer/deliveryProvider/upgradeDeliveryProvider.ts
  80. 45 0
      ethereum/ts-scripts/relayer/eraseTypes.ts
  81. 181 0
      ethereum/ts-scripts/relayer/helpers/deployments.ts
  82. 442 0
      ethereum/ts-scripts/relayer/helpers/env.ts
  83. 5 0
      ethereum/ts-scripts/relayer/helpers/utils.ts
  84. 155 0
      ethereum/ts-scripts/relayer/helpers/vaa.ts
  85. 57 0
      ethereum/ts-scripts/relayer/mockIntegration/deployMockIntegration.ts
  86. 76 0
      ethereum/ts-scripts/relayer/mockIntegration/messageTest.ts
  87. 150 0
      ethereum/ts-scripts/relayer/mockIntegration/messageUtils.ts
  88. 103 0
      ethereum/ts-scripts/relayer/mockIntegration/readMockIntegrationState.ts
  89. 11 0
      ethereum/ts-scripts/relayer/shell/deployConfigureTest.sh
  90. 7 0
      ethereum/ts-scripts/relayer/shell/deployInContainer.sh
  91. 1 0
      ethereum/ts-scripts/relayer/shell/readContractStatus.sh
  92. 1 0
      ethereum/ts-scripts/relayer/shell/tiltMessageTest.sh
  93. 43 0
      ethereum/ts-scripts/relayer/wormholeRelayer/deployWormholeRelayer.ts
  94. 112 0
      ethereum/ts-scripts/relayer/wormholeRelayer/readWormholeRelayerState.ts
  95. 39 0
      ethereum/ts-scripts/relayer/wormholeRelayer/registerChainsWormholeRelayerSelfSign.ts
  96. 52 0
      ethereum/ts-scripts/relayer/wormholeRelayer/upgradeWormholeRelayerSelfSign.ts
  97. 14 0
      ethereum/tsconfig.json

+ 1 - 1
Tiltfile

@@ -453,7 +453,7 @@ docker_build(
 
     # ignore local node_modules (in case they're present)
     ignore = ["./ethereum/node_modules"],
-    build_args = {"num_guardians": str(num_guardians)},
+    build_args = {"num_guardians": str(num_guardians), "dev": str(not ci)},
     # sync external scripts for incremental development
     # (everything else needs to be restarted from scratch for determinism)
     #

+ 9 - 6
devnet/eth-devnet.yaml

@@ -36,12 +36,14 @@ spec:
           command:
             - npx
             - ganache-cli
-            - -q
-            - -e 10000
-            - --deterministic
-            - --time="1970-01-01T00:00:00+00:00"
+            - --logging.quiet
+            - --wallet.defaultBalance=10000
+            - --wallet.deterministic
+            - --chain.time="1970-01-01T00:00:00+00:00"
             - --host=0.0.0.0
-            - --accounts=11
+            - --wallet.totalAccounts=11
+            - --chain.chainId=1
+            - --chain.asyncRequestProcessing=false
           ports:
             - containerPort: 8545
               name: rpc
@@ -55,10 +57,11 @@ spec:
           command:
             - /bin/sh
             - -c
-            - "npm run migrate && npx truffle exec scripts/deploy_test_token.js && npm run deploy-batched-vaa-sender && npx truffle exec scripts/register_all_chains.js && nc -lkp 2000 0.0.0.0"
+            - "npm run migrate && npx truffle exec scripts/deploy_test_token.js && npx truffle exec scripts/register_all_chains.js && npm run deploy-relayers-evm1 && nc -lkn 2000"
           readinessProbe:
             periodSeconds: 1
             failureThreshold: 300
+            initialDelaySeconds: 90
             tcpSocket:
               port: 2000
         - name: mine

+ 9 - 7
devnet/eth-devnet2.yaml

@@ -37,13 +37,14 @@ spec:
           command:
             - npx
             - ganache-cli
-            - -q
-            - -e 10000
-            - --deterministic
-            - --time="1970-01-01T00:00:00+00:00"
+            - --logging.quiet
+            - --wallet.defaultBalance=10000
+            - --wallet.deterministic
+            - --chain.time="1970-01-01T00:00:00+00:00"
             - --host=0.0.0.0
-            - --accounts=11
-            - --chainId=1397
+            - --wallet.totalAccounts=11
+            - --chain.chainId=1397
+            - --chain.asyncRequestProcessing=false
           ports:
             - containerPort: 8545
               name: rpc
@@ -57,10 +58,11 @@ spec:
           command:
             - /bin/sh
             - -c
-            - "sed -i 's/CHAIN_ID=0x2/CHAIN_ID=0x4/g;s/EVM_CHAIN_ID=1/EVM_CHAIN_ID=1397/g' .env && npm run migrate && npx truffle exec scripts/deploy_test_token.js && npx truffle exec scripts/register_all_chains.js && nc -lkp 2000 0.0.0.0"
+            - "sed -i 's/CHAIN_ID=0x2/CHAIN_ID=0x4/g;s/EVM_CHAIN_ID=1/EVM_CHAIN_ID=1397/g' .env && npm run migrate && npx truffle exec scripts/deploy_test_token.js && npx truffle exec scripts/register_all_chains.js && npm run deploy-relayers-evm2 && nc -lkn 2000"
           readinessProbe:
             periodSeconds: 1
             failureThreshold: 300
+            initialDelaySeconds: 90
             tcpSocket:
               port: 2000
         - name: mine

+ 4 - 0
ethereum/.dockerignore

@@ -1,2 +1,6 @@
 node_modules
 flattened
+ts-scripts/relayer/output
+build
+cache
+out

+ 2 - 0
ethereum/.gitignore

@@ -2,3 +2,5 @@ ganache.log
 lib/*
 !lib/README.md
 flattened
+ethers-contracts
+build-forge/

+ 28 - 4
ethereum/Dockerfile

@@ -1,14 +1,26 @@
 # syntax=docker.io/docker/dockerfile:1.3@sha256:42399d4635eddd7a9b8a24be879d2f9a930d0ed040a61324cfdf59ef1357b3b2
 FROM const-gen AS const-export
-FROM node:lts-alpine@sha256:2ae9624a39ce437e7f58931a5747fdc60224c6e40f8980db90728de58e22af7c
+FROM node:19.6.1-slim@sha256:a1ba21bf0c92931d02a8416f0a54daad66cb36a85d2b73af9d73b044f5f57cfc
 
 # npm wants to clone random Git repositories - lovely.
-RUN apk add git python make build-base
+# RUN apk add git python make build-base
+# RUN apk update && apk add bash
+RUN apt-get update && apt-get -y install \
+  git python make curl netcat
+
+RUN npm i typescript -g
+RUN curl -L https://foundry.paradigm.xyz | bash 
+RUN $HOME/.foundry/bin/foundryup
+RUN ls $HOME/.foundry/bin
 
 # Run as user, otherwise, npx explodes.
+RUN mv /root/.foundry/bin/forge /bin/forge
 USER 1000
+
+
 RUN mkdir -p /home/node/app
 RUN mkdir -p /home/node/.npm
+
 WORKDIR /home/node/app
 
 # Fix git ssh error
@@ -24,20 +36,32 @@ RUN if [ -e /certs/cert.pem ]; then npm config set cafile /certs/cert.pem; fi
 # git
 RUN if [ -e /certs/cert.pem ]; then git config --global http.sslCAInfo /certs/cert.pem; fi
 
+
+WORKDIR /home/node/app
+
 # Only invalidate the npm install step if package.json changed
 COPY --chown=node:node package.json .
 COPY --chown=node:node package-lock.json .
 COPY --from=const-export --chown=node:node .env.0x .env
 
+
 # We want to cache node_modules *and* incorporate it into the final image.
 RUN --mount=type=cache,uid=1000,gid=1000,target=/home/node/.npm \
   --mount=type=cache,uid=1000,gid=1000,target=node_modules \
-  npm ci && \
-  cp -r node_modules node_modules_cache
+  npm ci && \                                                 
+  cp -R node_modules node_modules_cache
+  
 
 # Amusingly, Debian's coreutils version has a bug where mv believes that
 # the target is on a different fs and does a full recursive copy for what
 # could be a renameat syscall. Alpine does not have this bug.
 RUN rm -rf node_modules && mv node_modules_cache node_modules
 
+ARG dev
+ENV DEV=$dev
+
 COPY --chown=node:node . .
+RUN make build
+
+ARG num_guardians
+ENV NUM_GUARDIANS=$num_guardians

+ 8 - 4
ethereum/Makefile

@@ -32,17 +32,21 @@ node_modules: package-lock.json
 # When adding a new dependency, make sure to specify the exact commit hash, and
 # the --no-git and --no-commit flags (see lib/forge-std below)
 .PHONY: forge_dependencies
-forge_dependencies: lib/forge-std
+forge_dependencies: lib/forge-std lib/openzeppelin-contracts
 
 lib/forge-std:
 	forge install foundry-rs/forge-std@v1.5.5 --no-git --no-commit
 
+lib/openzeppelin-contracts:
+	forge install openzeppelin/openzeppelin-contracts@0457042d93d9dfd760dbaa06a4d2f1216fdbe297 --no-git --no-commit
+
 dependencies: node_modules forge_dependencies
 
-build: node_modules ${SOURCE_FILES}
+build: forge_dependencies node_modules ${SOURCE_FILES}
 	mkdir -p build
 	touch -m build
 	npm run build
+	
 
 flattened/%.sol: contracts/%.sol node_modules
 	@mkdir -p $(dir $@)
@@ -62,7 +66,7 @@ test: test-forge test-ganache
 .PHONY: test-ganache
 test-ganache: build .env dependencies
 	@if pgrep ganache-cli; then echo "Error: ganache-cli already running. Stop it before running tests"; exit 1; fi
-	npx ganache-cli -e 10000 --deterministic --time="1970-01-01T00:00:00+00:00" > ganache.log &
+	. ./.env && npx ganache-cli --chain.vmErrorsOnRPCResponse --chain.chainId $$INIT_EVM_CHAIN_ID --wallet.defaultBalance 10000 --wallet.deterministic --chain.time="1970-01-01T00:00:00+00:00" --chain.asyncRequestProcessing=false > ganache.log &
 	sleep 5
 	npm test || (pkill ganache-cli && exit 1)
 	pkill ganache-cli || true
@@ -79,4 +83,4 @@ test-forge: dependencies
 	forge test
 
 clean:
-	rm -rf ganache.log .env node_modules build flattened
+	rm -rf ganache.log .env node_modules build flattened build-forge ethers-contracts

+ 6 - 1
ethereum/README.md

@@ -8,6 +8,11 @@ to transfer tokens in or change configuration settings.
 The `WrappedAsset` is a ERC-20 token contract that holds metadata about a wormhole asset on ETH. Wormhole assets are all
 wrapped non-ETH assets that are currently held on ETH.
 
+### Building
+
+To build the contracts:
+`make build`
+
 ### Deploying
 
 To deploy the bridge on Ethereum you first need to compile all smart contracts:
@@ -26,7 +31,7 @@ assets.
 
 For each test run:
 
-Run `npx ganache-cli --deterministic --time "1970-01-01T00:00:00+00:00"` to start a chain.
+Run `npx ganache-cli --wallet.deterministic --chain.time "1970-01-01T00:00:00+00:00"` to start a chain.
 
 Run the all ethereum tests using `make test`
 

+ 42 - 0
ethereum/contracts/interfaces/relayer/IDeliveryProvider.sol

@@ -0,0 +1,42 @@
+// SPDX-License-Identifier: Apache 2
+
+pragma solidity ^0.8.0;
+
+interface IDeliveryProvider {
+    function quoteDeliveryPrice(
+        uint16 targetChain,
+        uint256 receiverValue,
+        bytes memory encodedExecutionParams
+    ) external view returns (uint256 nativePriceQuote, bytes memory encodedExecutionInfo);
+
+    function quoteAssetConversion(
+        uint16 targetChain,
+        uint256 currentChainAmount
+    ) external view returns (uint256 targetChainAmount);
+
+    /**
+     * @notice This function should return a payable address on this (source) chain where all awards
+     *     should be sent for the relay provider.
+     *
+     */
+    function getRewardAddress() external view returns (address payable rewardAddress);
+
+    /**
+     * @notice This function determines whether a relay provider supports deliveries to a given chain
+     *     or not.
+     *
+     * @param targetChain - The chain which is being delivered to.
+     */
+    function isChainSupported(uint16 targetChain) external view returns (bool supported);
+
+    /**
+     * @notice If a DeliveryProvider supports a given chain, this function should provide the contract
+     *      address (in wormhole format) of the relay provider on that chain.
+     *
+     * @param targetChain - The chain which is being delivered to.
+     */
+    function getTargetChainAddress(uint16 targetChain)
+        external
+        view
+        returns (bytes32 deliveryProviderAddress);
+}

+ 44 - 0
ethereum/contracts/interfaces/relayer/IDeliveryProviderTyped.sol

@@ -0,0 +1,44 @@
+// SPDX-License-Identifier: Apache 2
+
+pragma solidity ^0.8.0;
+
+import "./TypedUnits.sol";
+
+interface IDeliveryProvider {
+    function quoteDeliveryPrice(
+        uint16 targetChain,
+        TargetNative receiverValue,
+        bytes memory encodedExecutionParams
+    ) external view returns (LocalNative nativePriceQuote, bytes memory encodedExecutionInfo);
+
+    function quoteAssetConversion(
+        uint16 targetChain,
+        LocalNative currentChainAmount
+    ) external view returns (TargetNative targetChainAmount);
+
+    /**
+     * @notice This function should return a payable address on this (source) chain where all awards
+     *     should be sent for the relay provider.
+     *
+     */
+    function getRewardAddress() external view returns (address payable rewardAddress);
+
+    /**
+     * @notice This function determines whether a relay provider supports deliveries to a given chain
+     *     or not.
+     *
+     * @param targetChain - The chain which is being delivered to.
+     */
+    function isChainSupported(uint16 targetChain) external view returns (bool supported);
+
+    /**
+     * @notice If a DeliveryProvider supports a given chain, this function should provide the contract
+     *      address (in wormhole format) of the relay provider on that chain.
+     *
+     * @param targetChain - The chain which is being delivered to.
+     */
+    function getTargetChainAddress(uint16 targetChain)
+        external
+        view
+        returns (bytes32 deliveryProviderAddress);
+}

+ 49 - 0
ethereum/contracts/interfaces/relayer/IWormholeReceiver.sol

@@ -0,0 +1,49 @@
+// SPDX-License-Identifier: Apache 2
+
+pragma solidity ^0.8.0;
+
+/**
+ * @notice Interface for a contract which can receive Wormhole messages.
+ */
+interface IWormholeReceiver {
+    /**
+     * @notice When a `send` is performed with this contract as the target, this function will be
+     *     invoked by the WormholeRelayer contract
+     *
+     * NOTE: This function should be restricted such that only the Wormhole Relayer contract can call it.
+     *
+     * We also recommend that this function:
+     *   - Stores all received `deliveryHash`s in a mapping `(bytes32 => bool)`, and
+     *       on every call, checks that deliveryHash has not already been stored in the
+     *       map (This is to prevent other users maliciously trying to relay the same message)
+     *   - Checks that `sourceChain` and `sourceAddress` are indeed who
+     *       you expect to have requested the calling of `send` or `forward` on the source chain
+     *
+     * The invocation of this function corresponding to the `send` request will have msg.value equal
+     *   to the receiverValue specified in the send request.
+     *
+     * If the invocation of this function reverts or exceeds the gas limit 
+     *   specified by the send requester, this delivery will result in a `ReceiverFailure`.
+     *
+     * @param payload - an arbitrary message which was included in the delivery by the
+     *     requester.
+     * @param additionalVaas - Additional VAAs which were requested to be included in this delivery.
+     *   They are guaranteed to all be included and in the same order as was specified in the
+     *     delivery request.
+     * @param sourceAddress - the (wormhole format) address on the sending chain which requested
+     *     this delivery.
+     * @param sourceChain - the wormhole chain ID where this delivery was requested.
+     * @param deliveryHash - the VAA hash of the deliveryVAA.
+     *
+     * NOTE: These signedVaas are NOT verified by the Wormhole core contract prior to being provided
+     *     to this call. Always make sure `parseAndVerify()` is called on the Wormhole core contract
+     *     before trusting the content of a raw VAA, otherwise the VAA may be invalid or malicious.
+     */
+    function receiveWormholeMessages(
+        bytes memory payload,
+        bytes[] memory additionalVaas,
+        bytes32 sourceAddress,
+        uint16 sourceChain,
+        bytes32 deliveryHash
+    ) external payable;
+}

+ 709 - 0
ethereum/contracts/interfaces/relayer/IWormholeRelayer.sol

@@ -0,0 +1,709 @@
+// SPDX-License-Identifier: Apache 2
+
+pragma solidity ^0.8.0;
+
+/**
+ * @title WormholeRelayer
+ * @author 
+ * @notice This project allows developers to build cross-chain applications powered by Wormhole without needing to 
+ * write and run their own relaying infrastructure
+ * 
+ * We implement the IWormholeRelayer interface that allows users to request a delivery provider to relay a payload (and/or additional VAAs) 
+ * to a chain and address of their choice.
+ */
+
+/**
+ * @notice VaaKey identifies a wormhole message
+ *
+ * @custom:member chainId Wormhole chain ID of the chain where this VAA was emitted from
+ * @custom:member emitterAddress Address of the emitter of the VAA, in Wormhole bytes32 format
+ * @custom:member sequence Sequence number of the VAA
+ */
+struct VaaKey {
+    uint16 chainId;
+    bytes32 emitterAddress;
+    uint64 sequence;
+}
+
+interface IWormholeRelayerBase {
+    event SendEvent(
+        uint64 indexed sequence, uint256 deliveryQuote, uint256 paymentForExtraReceiverValue
+    );
+
+    function getRegisteredWormholeRelayerContract(uint16 chainId) external view returns (bytes32);
+}
+
+/**
+ * @title IWormholeRelayerSend
+ * @notice The interface to request deliveries
+ */
+interface IWormholeRelayerSend is IWormholeRelayerBase {
+
+    /**
+     * @notice Publishes an instruction for the default delivery provider
+     * to relay a payload to the address `targetAddress` on chain `targetChain` 
+     * with gas limit `gasLimit` and `msg.value` equal to `receiverValue`
+     * 
+     * `targetAddress` must implement the IWormholeReceiver interface
+     * 
+     * This function must be called with `msg.value` equal to `quoteEVMDeliveryPrice(targetChain, receiverValue, gasLimit)`
+     * 
+     * Any refunds (from leftover gas) will be paid to the delivery provider. In order to receive the refunds, use the `sendPayloadToEvm` function 
+     * with `refundChain` and `refundAddress` as parameters
+     * 
+     * @param targetChain in Wormhole Chain ID format
+     * @param targetAddress address to call on targetChain (that implements IWormholeReceiver) 
+     * @param payload arbitrary bytes to pass in as parameter in call to `targetAddress`
+     * @param receiverValue msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units)
+     * @param gasLimit gas limit with which to call `targetAddress`.
+     * @return sequence sequence number of published VAA containing delivery instructions
+     */
+    function sendPayloadToEvm(
+        uint16 targetChain,
+        address targetAddress,
+        bytes memory payload,
+        uint256 receiverValue,
+        uint256 gasLimit
+    ) external payable returns (uint64 sequence);
+
+    /**
+     * @notice Publishes an instruction for the default delivery provider
+     * to relay a payload to the address `targetAddress` on chain `targetChain` 
+     * with gas limit `gasLimit` and `msg.value` equal to `receiverValue`
+     * 
+     * Any refunds (from leftover gas) will be sent to `refundAddress` on chain `refundChain`
+     * `targetAddress` must implement the IWormholeReceiver interface
+     * 
+     * This function must be called with `msg.value` equal to `quoteEVMDeliveryPrice(targetChain, receiverValue, gasLimit)`
+     * 
+     * @param targetChain in Wormhole Chain ID format
+     * @param targetAddress address to call on targetChain (that implements IWormholeReceiver) 
+     * @param payload arbitrary bytes to pass in as parameter in call to `targetAddress`
+     * @param receiverValue msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units)
+     * @param gasLimit gas limit with which to call `targetAddress`. Any units of gas unused will be refunded according to the
+     *        `targetChainRefundPerGasUnused` rate quoted by the delivery provider
+     * @param refundChain The chain to deliver any refund to, in Wormhole Chain ID format
+     * @param refundAddress The address on `refundChain` to deliver any refund to
+     * @return sequence sequence number of published VAA containing delivery instructions
+     */
+    function sendPayloadToEvm(
+        uint16 targetChain,
+        address targetAddress,
+        bytes memory payload,
+        uint256 receiverValue,
+        uint256 gasLimit,
+        uint16 refundChain,
+        address refundAddress
+    ) external payable returns (uint64 sequence);
+
+    /**
+     * @notice Publishes an instruction for the default delivery provider
+     * to relay a payload and VAAs specified by `vaaKeys` to the address `targetAddress` on chain `targetChain` 
+     * with gas limit `gasLimit` and `msg.value` equal to `receiverValue`
+     * 
+     * `targetAddress` must implement the IWormholeReceiver interface
+     * 
+     * This function must be called with `msg.value` equal to `quoteEVMDeliveryPrice(targetChain, receiverValue, gasLimit)`
+     * 
+     * Any refunds (from leftover gas) will be paid to the delivery provider. In order to receive the refunds, use the `sendVaasToEvm` function 
+     * with `refundChain` and `refundAddress` as parameters
+     * 
+     * @param targetChain in Wormhole Chain ID format
+     * @param targetAddress address to call on targetChain (that implements IWormholeReceiver) 
+     * @param payload arbitrary bytes to pass in as parameter in call to `targetAddress`
+     * @param receiverValue msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units)
+     * @param gasLimit gas limit with which to call `targetAddress`. 
+     * @param vaaKeys Additional VAAs to pass in as parameter in call to `targetAddress`
+     * @return sequence sequence number of published VAA containing delivery instructions
+     */
+    function sendVaasToEvm(
+        uint16 targetChain,
+        address targetAddress,
+        bytes memory payload,
+        uint256 receiverValue,
+        uint256 gasLimit,
+        VaaKey[] memory vaaKeys
+    ) external payable returns (uint64 sequence);
+
+    /**
+     * @notice Publishes an instruction for the default delivery provider
+     * to relay a payload and VAAs specified by `vaaKeys` to the address `targetAddress` on chain `targetChain` 
+     * with gas limit `gasLimit` and `msg.value` equal to `receiverValue`
+     * 
+     * Any refunds (from leftover gas) will be sent to `refundAddress` on chain `refundChain`
+     * `targetAddress` must implement the IWormholeReceiver interface
+     * 
+     * This function must be called with `msg.value` equal to `quoteEVMDeliveryPrice(targetChain, receiverValue, gasLimit)`
+     * 
+     * @param targetChain in Wormhole Chain ID format
+     * @param targetAddress address to call on targetChain (that implements IWormholeReceiver) 
+     * @param payload arbitrary bytes to pass in as parameter in call to `targetAddress`
+     * @param receiverValue msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units)
+     * @param gasLimit gas limit with which to call `targetAddress`. Any units of gas unused will be refunded according to the 
+     *        `targetChainRefundPerGasUnused` rate quoted by the delivery provider
+     * @param vaaKeys Additional VAAs to pass in as parameter in call to `targetAddress`
+     * @param refundChain The chain to deliver any refund to, in Wormhole Chain ID format
+     * @param refundAddress The address on `refundChain` to deliver any refund to
+     * @return sequence sequence number of published VAA containing delivery instructions
+     */
+    function sendVaasToEvm(
+        uint16 targetChain,
+        address targetAddress,
+        bytes memory payload,
+        uint256 receiverValue,
+        uint256 gasLimit,
+        VaaKey[] memory vaaKeys,
+        uint16 refundChain,
+        address refundAddress
+    ) external payable returns (uint64 sequence);
+
+    /**
+     * @notice Publishes an instruction for the delivery provider at `deliveryProviderAddress` 
+     * to relay a payload and VAAs specified by `vaaKeys` to the address `targetAddress` on chain `targetChain` 
+     * with gas limit `gasLimit` and `msg.value` equal to 
+     * receiverValue + (arbitrary amount that is paid for by paymentForExtraReceiverValue of this chain's wei) in targetChain wei.
+     * 
+     * Any refunds (from leftover gas) will be sent to `refundAddress` on chain `refundChain`
+     * `targetAddress` must implement the IWormholeReceiver interface
+     * 
+     * This function must be called with `msg.value` equal to 
+     * quoteEVMDeliveryPrice(targetChain, receiverValue, gasLimit, deliveryProviderAddress) + paymentForExtraReceiverValue
+     * 
+     * @param targetChain in Wormhole Chain ID format
+     * @param targetAddress address to call on targetChain (that implements IWormholeReceiver) 
+     * @param payload arbitrary bytes to pass in as parameter in call to `targetAddress`
+     * @param receiverValue msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units)
+     * @param paymentForExtraReceiverValue amount (in current chain currency units) to spend on extra receiverValue 
+     *        (in addition to the `receiverValue` specified)
+     * @param gasLimit gas limit with which to call `targetAddress`. Any units of gas unused will be refunded according to the  
+     *        `targetChainRefundPerGasUnused` rate quoted by the delivery provider
+     * @param refundChain The chain to deliver any refund to, in Wormhole Chain ID format
+     * @param refundAddress The address on `refundChain` to deliver any refund to
+     * @param deliveryProviderAddress The address of the desired delivery provider's implementation of IDeliveryProvider
+     * @param vaaKeys Additional VAAs to pass in as parameter in call to `targetAddress`
+     * @param consistencyLevel Consistency level with which to publish the delivery instructions - see 
+     *        https://book.wormhole.com/wormhole/3_coreLayerContracts.html?highlight=consistency#consistency-levels
+     * @return sequence sequence number of published VAA containing delivery instructions
+     */
+    function sendToEvm(
+        uint16 targetChain,
+        address targetAddress,
+        bytes memory payload,
+        uint256 receiverValue,
+        uint256 paymentForExtraReceiverValue,
+        uint256 gasLimit,
+        uint16 refundChain,
+        address refundAddress,
+        address deliveryProviderAddress,
+        VaaKey[] memory vaaKeys,
+        uint8 consistencyLevel
+    ) external payable returns (uint64 sequence);
+    
+    /**
+     * @notice Publishes an instruction for the delivery provider at `deliveryProviderAddress` 
+     * to relay a payload and VAAs specified by `vaaKeys` to the address `targetAddress` on chain `targetChain` 
+     * with `msg.value` equal to 
+     * receiverValue + (arbitrary amount that is paid for by paymentForExtraReceiverValue of this chain's wei) in targetChain wei.
+     * 
+     * Any refunds (from leftover gas) will be sent to `refundAddress` on chain `refundChain`
+     * `targetAddress` must implement the IWormholeReceiver interface
+     * 
+     * This function must be called with `msg.value` equal to 
+     * quoteDeliveryPrice(targetChain, receiverValue, encodedExecutionParameters, deliveryProviderAddress) + paymentForExtraReceiverValue  
+     * 
+     * @param targetChain in Wormhole Chain ID format
+     * @param targetAddress address to call on targetChain (that implements IWormholeReceiver), in Wormhole bytes32 format
+     * @param payload arbitrary bytes to pass in as parameter in call to `targetAddress`
+     * @param receiverValue msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units)
+     * @param paymentForExtraReceiverValue amount (in current chain currency units) to spend on extra receiverValue 
+     *        (in addition to the `receiverValue` specified)
+     * @param encodedExecutionParameters encoded information on how to execute delivery that may impact pricing
+     *        e.g. for version EVM_V1, this is a struct that encodes the `gasLimit` with which to call `targetAddress`
+     * @param refundChain The chain to deliver any refund to, in Wormhole Chain ID format
+     * @param refundAddress The address on `refundChain` to deliver any refund to, in Wormhole bytes32 format
+     * @param deliveryProviderAddress The address of the desired delivery provider's implementation of IDeliveryProvider
+     * @param vaaKeys Additional VAAs to pass in as parameter in call to `targetAddress`
+     * @param consistencyLevel Consistency level with which to publish the delivery instructions - see 
+     *        https://book.wormhole.com/wormhole/3_coreLayerContracts.html?highlight=consistency#consistency-levels
+     * @return sequence sequence number of published VAA containing delivery instructions
+     */
+    function send(
+        uint16 targetChain,
+        bytes32 targetAddress,
+        bytes memory payload,
+        uint256 receiverValue,
+        uint256 paymentForExtraReceiverValue,
+        bytes memory encodedExecutionParameters,
+        uint16 refundChain,
+        bytes32 refundAddress,
+        address deliveryProviderAddress,
+        VaaKey[] memory vaaKeys,
+        uint8 consistencyLevel
+    ) external payable returns (uint64 sequence);
+
+    /**
+     * @notice Performs the same function as a `send`, except:
+     * 1)  Can only be used during a delivery (i.e. in execution of `receiveWormholeMessages`)
+     * 2)  Is paid for (along with any other calls to forward) by (any msg.value passed in) + (refund leftover from current delivery)
+     * 3)  Only executes after `receiveWormholeMessages` is completed (and thus does not return a sequence number)
+     * 
+     * The refund from the delivery currently in progress will not be sent to the user; it will instead
+     * be paid to the delivery provider to perform the instruction specified here
+     * 
+     * Publishes an instruction for the same delivery provider (or default, if the same one doesn't support the new target chain)
+     * to relay a payload to the address `targetAddress` on chain `targetChain` 
+     * with gas limit `gasLimit` and with `msg.value` equal to `receiverValue`
+     * 
+     * The following equation must be satisfied (sum_f indicates summing over all forwards requested in `receiveWormholeMessages`):
+     * (refund amount from current execution of receiveWormholeMessages) + sum_f [msg.value_f]
+     * >= sum_f [quoteEVMDeliveryPrice(targetChain_f, receiverValue_f, gasLimit_f)]
+     * 
+     * The difference between the two sides of the above inequality will be added to `paymentForExtraReceiverValue` of the first forward requested
+     * 
+     * Any refunds (from leftover gas) from this forward will be paid to the same refundChain and refundAddress specified for the current delivery.
+     * 
+     * @param targetChain in Wormhole Chain ID format
+     * @param targetAddress address to call on targetChain (that implements IWormholeReceiver), in Wormhole bytes32 format
+     * @param payload arbitrary bytes to pass in as parameter in call to `targetAddress`
+     * @param receiverValue msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units)
+     * @param gasLimit gas limit with which to call `targetAddress`.
+     */
+    function forwardPayloadToEvm(
+        uint16 targetChain,
+        address targetAddress,
+        bytes memory payload,
+        uint256 receiverValue,
+        uint256 gasLimit
+    ) external payable;
+
+    /**
+     * @notice Performs the same function as a `send`, except:
+     * 1)  Can only be used during a delivery (i.e. in execution of `receiveWormholeMessages`)
+     * 2)  Is paid for (along with any other calls to forward) by (any msg.value passed in) + (refund leftover from current delivery)
+     * 3)  Only executes after `receiveWormholeMessages` is completed (and thus does not return a sequence number)
+     * 
+     * The refund from the delivery currently in progress will not be sent to the user; it will instead
+     * be paid to the delivery provider to perform the instruction specified here
+     * 
+     * Publishes an instruction for the same delivery provider (or default, if the same one doesn't support the new target chain)
+     * to relay a payload and VAAs specified by `vaaKeys` to the address `targetAddress` on chain `targetChain` 
+     * with gas limit `gasLimit` and with `msg.value` equal to `receiverValue`
+     * 
+     * The following equation must be satisfied (sum_f indicates summing over all forwards requested in `receiveWormholeMessages`):
+     * (refund amount from current execution of receiveWormholeMessages) + sum_f [msg.value_f]
+     * >= sum_f [quoteEVMDeliveryPrice(targetChain_f, receiverValue_f, gasLimit_f)]
+     * 
+     * The difference between the two sides of the above inequality will be added to `paymentForExtraReceiverValue` of the first forward requested
+     * 
+     * Any refunds (from leftover gas) from this forward will be paid to the same refundChain and refundAddress specified for the current delivery.
+     * 
+     * @param targetChain in Wormhole Chain ID format
+     * @param targetAddress address to call on targetChain (that implements IWormholeReceiver), in Wormhole bytes32 format
+     * @param payload arbitrary bytes to pass in as parameter in call to `targetAddress`
+     * @param receiverValue msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units)
+     * @param gasLimit gas limit with which to call `targetAddress`. 
+     * @param vaaKeys Additional VAAs to pass in as parameter in call to `targetAddress`
+     */
+    function forwardVaasToEvm(
+        uint16 targetChain,
+        address targetAddress,
+        bytes memory payload,
+        uint256 receiverValue,
+        uint256 gasLimit,
+        VaaKey[] memory vaaKeys
+    ) external payable;
+
+    /**
+     * @notice Performs the same function as a `send`, except:
+     * 1)  Can only be used during a delivery (i.e. in execution of `receiveWormholeMessages`)
+     * 2)  Is paid for (along with any other calls to forward) by (any msg.value passed in) + (refund leftover from current delivery)
+     * 3)  Only executes after `receiveWormholeMessages` is completed (and thus does not return a sequence number)
+     * 
+     * The refund from the delivery currently in progress will not be sent to the user; it will instead
+     * be paid to the delivery provider to perform the instruction specified here
+     * 
+     * Publishes an instruction for the delivery provider at `deliveryProviderAddress` 
+     * to relay a payload and VAAs specified by `vaaKeys` to the address `targetAddress` on chain `targetChain` 
+     * with gas limit `gasLimit` and with `msg.value` equal to 
+     * receiverValue + (arbitrary amount that is paid for by paymentForExtraReceiverValue of this chain's wei) in targetChain wei.
+     * 
+     * Any refunds (from leftover gas) will be sent to `refundAddress` on chain `refundChain`
+     * `targetAddress` must implement the IWormholeReceiver interface
+     * 
+     * The following equation must be satisfied (sum_f indicates summing over all forwards requested in `receiveWormholeMessages`):
+     * (refund amount from current execution of receiveWormholeMessages) + sum_f [msg.value_f]
+     * >= sum_f [quoteEVMDeliveryPrice(targetChain_f, receiverValue_f, gasLimit_f, deliveryProviderAddress_f) + paymentForExtraReceiverValue_f]
+     * 
+     * The difference between the two sides of the above inequality will be added to `paymentForExtraReceiverValue` of the first forward requested
+     * 
+     * @param targetChain in Wormhole Chain ID format
+     * @param targetAddress address to call on targetChain (that implements IWormholeReceiver), in Wormhole bytes32 format
+     * @param payload arbitrary bytes to pass in as parameter in call to `targetAddress`
+     * @param receiverValue msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units)
+     * @param paymentForExtraReceiverValue amount (in current chain currency units) to spend on extra receiverValue 
+     *        (in addition to the `receiverValue` specified)
+     * @param gasLimit gas limit with which to call `targetAddress`. Any units of gas unused will be refunded according to the  
+     *        `targetChainRefundPerGasUnused` rate quoted by the delivery provider
+     * @param refundChain The chain to deliver any refund to, in Wormhole Chain ID format
+     * @param refundAddress The address on `refundChain` to deliver any refund to, in Wormhole bytes32 format
+     * @param deliveryProviderAddress The address of the desired delivery provider's implementation of IDeliveryProvider
+     * @param vaaKeys Additional VAAs to pass in as parameter in call to `targetAddress`
+     * @param consistencyLevel Consistency level with which to publish the delivery instructions - see 
+     *        https://book.wormhole.com/wormhole/3_coreLayerContracts.html?highlight=consistency#consistency-levels
+     */
+    function forwardToEvm(
+        uint16 targetChain,
+        address targetAddress,
+        bytes memory payload,
+        uint256 receiverValue,
+        uint256 paymentForExtraReceiverValue,
+        uint256 gasLimit,
+        uint16 refundChain,
+        address refundAddress,
+        address deliveryProviderAddress,
+        VaaKey[] memory vaaKeys,
+        uint8 consistencyLevel
+    ) external payable;
+
+    /**
+     * @notice Performs the same function as a `send`, except:
+     * 1)  Can only be used during a delivery (i.e. in execution of `receiveWormholeMessages`)
+     * 2)  Is paid for (along with any other calls to forward) by (any msg.value passed in) + (refund leftover from current delivery)
+     * 3)  Only executes after `receiveWormholeMessages` is completed (and thus does not return a sequence number)
+     * 
+     * The refund from the delivery currently in progress will not be sent to the user; it will instead
+     * be paid to the delivery provider to perform the instruction specified here
+     * 
+     * Publishes an instruction for the delivery provider at `deliveryProviderAddress` 
+     * to relay a payload and VAAs specified by `vaaKeys` to the address `targetAddress` on chain `targetChain` 
+     * with `msg.value` equal to 
+     * receiverValue + (arbitrary amount that is paid for by paymentForExtraReceiverValue of this chain's wei) in targetChain wei.
+     * 
+     * Any refunds (from leftover gas) will be sent to `refundAddress` on chain `refundChain`
+     * `targetAddress` must implement the IWormholeReceiver interface
+     * 
+     * The following equation must be satisfied (sum_f indicates summing over all forwards requested in `receiveWormholeMessages`):
+     * (refund amount from current execution of receiveWormholeMessages) + sum_f [msg.value_f]
+     * >= sum_f [quoteDeliveryPrice(targetChain_f, receiverValue_f, encodedExecutionParameters_f, deliveryProviderAddress_f) + paymentForExtraReceiverValue_f]
+     * 
+     * The difference between the two sides of the above inequality will be added to `paymentForExtraReceiverValue` of the first forward requested
+     * 
+     * @param targetChain in Wormhole Chain ID format
+     * @param targetAddress address to call on targetChain (that implements IWormholeReceiver), in Wormhole bytes32 format
+     * @param payload arbitrary bytes to pass in as parameter in call to `targetAddress`
+     * @param receiverValue msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units)
+     * @param paymentForExtraReceiverValue amount (in current chain currency units) to spend on extra receiverValue 
+     *        (in addition to the `receiverValue` specified)
+     * @param encodedExecutionParameters encoded information on how to execute delivery that may impact pricing
+     *        e.g. for version EVM_V1, this is a struct that encodes the `gasLimit` with which to call `targetAddress`
+     * @param refundChain The chain to deliver any refund to, in Wormhole Chain ID format
+     * @param refundAddress The address on `refundChain` to deliver any refund to, in Wormhole bytes32 format
+     * @param deliveryProviderAddress The address of the desired delivery provider's implementation of IDeliveryProvider
+     * @param vaaKeys Additional VAAs to pass in as parameter in call to `targetAddress`
+     * @param consistencyLevel Consistency level with which to publish the delivery instructions - see 
+     *        https://book.wormhole.com/wormhole/3_coreLayerContracts.html?highlight=consistency#consistency-levels
+     */
+    function forward(
+        uint16 targetChain,
+        bytes32 targetAddress,
+        bytes memory payload,
+        uint256 receiverValue,
+        uint256 paymentForExtraReceiverValue,
+        bytes memory encodedExecutionParameters,
+        uint16 refundChain,
+        bytes32 refundAddress,
+        address deliveryProviderAddress,
+        VaaKey[] memory vaaKeys,
+        uint8 consistencyLevel
+    ) external payable;
+
+    /**
+     * @notice Requests a previously published delivery instruction to be redelivered 
+     * (e.g. with a different delivery provider)
+     *
+     * This function must be called with `msg.value` equal to 
+     * quoteEVMDeliveryPrice(targetChain, newReceiverValue, newGasLimit, newDeliveryProviderAddress)
+     * 
+     *  @notice *** This will only be able to succeed if the following is true **
+     *         - newGasLimit >= gas limit of the old instruction
+     *         - newReceiverValue >= receiver value of the old instruction
+     *         - newDeliveryProvider's `targetChainRefundPerGasUnused` >= old relay provider's `targetChainRefundPerGasUnused`
+     * 
+     * @param deliveryVaaKey VaaKey identifying the wormhole message containing the 
+     *        previously published delivery instructions
+     * @param targetChain The target chain that the original delivery targeted. Must match targetChain from original delivery instructions
+     * @param newReceiverValue new msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units)
+     * @param newGasLimit gas limit with which to call `targetAddress`. Any units of gas unused will be refunded according to the  
+     *        `targetChainRefundPerGasUnused` rate quoted by the delivery provider, to the refund chain and address specified in the original request
+     * @param newDeliveryProviderAddress The address of the desired delivery provider's implementation of IDeliveryProvider
+     * @return sequence sequence number of published VAA containing redelivery instructions
+     *
+     * @notice *** This will only be able to succeed if the following is true **
+     *         - newGasLimit >= gas limit of the old instruction
+     *         - newReceiverValue >= receiver value of the old instruction
+     *         - newDeliveryProvider's `targetChainRefundPerGasUnused` >= old relay provider's `targetChainRefundPerGasUnused`
+     */
+    function resendToEvm(
+        VaaKey memory deliveryVaaKey,
+        uint16 targetChain,
+        uint256 newReceiverValue,
+        uint256 newGasLimit,
+        address newDeliveryProviderAddress
+    ) external payable returns (uint64 sequence);
+
+    /**
+     * @notice Requests a previously published delivery instruction to be redelivered 
+     * 
+     *
+     * This function must be called with `msg.value` equal to 
+     * quoteDeliveryPrice(targetChain, newReceiverValue, newEncodedExecutionParameters, newDeliveryProviderAddress)
+     * 
+     * @param deliveryVaaKey VaaKey identifying the wormhole message containing the 
+     *        previously published delivery instructions
+     * @param targetChain The target chain that the original delivery targeted. Must match targetChain from original delivery instructions
+     * @param newReceiverValue new msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units)
+     * @param newEncodedExecutionParameters new encoded information on how to execute delivery that may impact pricing
+     *        e.g. for version EVM_V1, this is a struct that encodes the `gasLimit` with which to call `targetAddress`
+     * @param newDeliveryProviderAddress The address of the desired delivery provider's implementation of IDeliveryProvider
+     * @return sequence sequence number of published VAA containing redelivery instructions
+     * 
+     *  @notice *** This will only be able to succeed if the following is true **
+     *         - (For EVM_V1) newGasLimit >= gas limit of the old instruction
+     *         - newReceiverValue >= receiver value of the old instruction
+     *         - (For EVM_V1) newDeliveryProvider's `targetChainRefundPerGasUnused` >= old relay provider's `targetChainRefundPerGasUnused`
+     */
+    function resend(
+        VaaKey memory deliveryVaaKey,
+        uint16 targetChain,
+        uint256 newReceiverValue,
+        bytes memory newEncodedExecutionParameters,
+        address newDeliveryProviderAddress
+    ) external payable returns (uint64 sequence);
+
+    /**
+     * @notice Returns the price to request a relay to chain `targetChain`, using the default delivery provider
+     * 
+     * @param targetChain in Wormhole Chain ID format
+     * @param receiverValue msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units)
+     * @param gasLimit gas limit with which to call `targetAddress`. 
+     * @return nativePriceQuote Price, in units of current chain currency, that the delivery provider charges to perform the relay
+     * @return targetChainRefundPerGasUnused amount of target chain currency that will be refunded per unit of gas unused, 
+     *         if a refundAddress is specified
+     */
+    function quoteEVMDeliveryPrice(
+        uint16 targetChain,
+        uint256 receiverValue,
+        uint256 gasLimit
+    ) external view returns (uint256 nativePriceQuote, uint256 targetChainRefundPerGasUnused);
+
+    /**
+     * @notice Returns the price to request a relay to chain `targetChain`, using delivery provider `deliveryProviderAddress`
+     * 
+     * @param targetChain in Wormhole Chain ID format
+     * @param receiverValue msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units)
+     * @param gasLimit gas limit with which to call `targetAddress`. 
+     * @param deliveryProviderAddress The address of the desired delivery provider's implementation of IDeliveryProvider
+     * @return nativePriceQuote Price, in units of current chain currency, that the delivery provider charges to perform the relay
+     * @return targetChainRefundPerGasUnused amount of target chain currency that will be refunded per unit of gas unused, 
+     *         if a refundAddress is specified
+     */
+    function quoteEVMDeliveryPrice(
+        uint16 targetChain,
+        uint256 receiverValue,
+        uint256 gasLimit,
+        address deliveryProviderAddress
+    ) external view returns (uint256 nativePriceQuote, uint256 targetChainRefundPerGasUnused);
+
+    /**
+     * @notice Returns the price to request a relay to chain `targetChain`, using delivery provider `deliveryProviderAddress`
+     * 
+     * @param targetChain in Wormhole Chain ID format
+     * @param receiverValue msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units)
+     * @param encodedExecutionParameters encoded information on how to execute delivery that may impact pricing
+     *        e.g. for version EVM_V1, this is a struct that encodes the `gasLimit` with which to call `targetAddress`
+     * @param deliveryProviderAddress The address of the desired delivery provider's implementation of IDeliveryProvider
+     * @return nativePriceQuote Price, in units of current chain currency, that the delivery provider charges to perform the relay
+     * @return encodedExecutionInfo encoded information on how the delivery will be executed
+     *        e.g. for version EVM_V1, this is a struct that encodes the `gasLimit` and `targetChainRefundPerGasUnused`
+     *             (which is the amount of target chain currency that will be refunded per unit of gas unused, 
+     *              if a refundAddress is specified)
+     */
+    function quoteDeliveryPrice(
+        uint16 targetChain,
+        uint256 receiverValue,
+        bytes memory encodedExecutionParameters,
+        address deliveryProviderAddress
+    ) external view returns (uint256 nativePriceQuote, bytes memory encodedExecutionInfo);
+
+    /**
+     * @notice Returns the (extra) amount of target chain currency that `targetAddress`
+     * will be called with, if the `paymentForExtraReceiverValue` field is set to `currentChainAmount`
+     * 
+     * @param targetChain in Wormhole Chain ID format
+     * @param currentChainAmount The value that `paymentForExtraReceiverValue` will be set to
+     * @param deliveryProviderAddress The address of the desired delivery provider's implementation of IDeliveryProvider
+     * @return targetChainAmount The amount such that if `targetAddress` will be called with `msg.value` equal to
+     *         receiverValue + targetChainAmount
+     */
+    function quoteNativeForChain(
+        uint16 targetChain,
+        uint256 currentChainAmount,
+        address deliveryProviderAddress
+    ) external view returns (uint256 targetChainAmount);
+
+    /**
+     * @notice Returns the address of the current default delivery provider
+     * @return deliveryProvider The address of (the default delivery provider)'s contract on this source
+     *   chain. This must be a contract that implements IDeliveryProvider.
+     */
+    function getDefaultDeliveryProvider() external view returns (address deliveryProvider);
+}
+
+/**
+ * @title IWormholeRelayerDelivery
+ * @notice The interface to execute deliveries. Only relevant for Delivery Providers 
+ */
+interface IWormholeRelayerDelivery is IWormholeRelayerBase {
+    enum DeliveryStatus {
+        SUCCESS,
+        RECEIVER_FAILURE,
+        FORWARD_REQUEST_FAILURE,
+        FORWARD_REQUEST_SUCCESS
+    }
+
+    enum RefundStatus {
+        REFUND_SENT,
+        REFUND_FAIL,
+        CROSS_CHAIN_REFUND_SENT,
+        CROSS_CHAIN_REFUND_FAIL_PROVIDER_NOT_SUPPORTED,
+        CROSS_CHAIN_REFUND_FAIL_NOT_ENOUGH
+    }
+
+    /**
+     * @custom:member recipientContract - The target contract address
+     * @custom:member sourceChain - The chain which this delivery was requested from (in wormhole
+     *     ChainID format)
+     * @custom:member sequence - The wormhole sequence number of the delivery VAA on the source chain
+     *     corresponding to this delivery request
+     * @custom:member deliveryVaaHash - The hash of the delivery VAA corresponding to this delivery
+     *     request
+     * @custom:member gasUsed - The amount of gas that was used to call your target contract 
+     * @custom:member status:
+     *   - RECEIVER_FAILURE, if the target contract reverts
+     *   - SUCCESS, if the target contract doesn't revert and no forwards were requested
+     *   - FORWARD_REQUEST_FAILURE, if the target contract doesn't revert, forwards were requested,
+     *       but provided/leftover funds were not sufficient to cover them all
+     *   - FORWARD_REQUEST_SUCCESS, if the target contract doesn't revert and all forwards are covered
+     * @custom:member additionalStatusInfo:
+     *   - If status is SUCCESS or FORWARD_REQUEST_SUCCESS, then this is empty.
+     *   - If status is RECEIVER_FAILURE, this is `RETURNDATA_TRUNCATION_THRESHOLD` bytes of the
+     *       return data (i.e. potentially truncated revert reason information).
+     *   - If status is FORWARD_REQUEST_FAILURE, this is also the revert data - the reason the forward failed.
+     *     This will be either an encoded Cancelled, DeliveryProviderReverted, or DeliveryProviderPaymentFailed error
+     * @custom:member refundStatus - Result of the refund. REFUND_SUCCESS or REFUND_FAIL are for
+     *     refunds where targetChain=refundChain; the others are for targetChain!=refundChain,
+     *     where a cross chain refund is necessary
+     * @custom:member overridesInfo:
+     *   - If not an override: empty bytes array
+     *   - Otherwise: An encoded `DeliveryOverride`
+     */
+    event Delivery(
+        address indexed recipientContract,
+        uint16 indexed sourceChain,
+        uint64 indexed sequence,
+        bytes32 deliveryVaaHash,
+        DeliveryStatus status,
+        uint256 gasUsed,
+        RefundStatus refundStatus,
+        bytes additionalStatusInfo,
+        bytes overridesInfo
+    );
+
+    /**
+     * @notice The delivery provider calls `deliver` to relay messages as described by one delivery instruction
+     * 
+     * The delivery provider must pass in the specified (by VaaKeys[]) signed wormhole messages (VAAs) from the source chain
+     * as well as the signed wormhole message with the delivery instructions (the delivery VAA)
+     *
+     * The messages will be relayed to the target address (with the specified gas limit and receiver value) iff the following checks are met:
+     * - the delivery VAA has a valid signature
+     * - the delivery VAA's emitter is one of these WormholeRelayer contracts
+     * - the delivery provider passed in at least enough of this chain's currency as msg.value (enough meaning the maximum possible refund)     
+     * - the instruction's target chain is this chain
+     * - the relayed signed VAAs match the descriptions in container.messages (the VAA hashes match, or the emitter address, sequence number pair matches, depending on the description given)
+     *
+     * @param encodedVMs - An array of signed wormhole messages (all from the same source chain
+     *     transaction)
+     * @param encodedDeliveryVAA - Signed wormhole message from the source chain's WormholeRelayer
+     *     contract with payload being the encoded delivery instruction container
+     * @param relayerRefundAddress - The address to which any refunds to the delivery provider
+     *     should be sent
+     * @param deliveryOverrides - Optional overrides field which must be either an empty bytes array or
+     *     an encoded DeliveryOverride struct
+     */
+    function deliver(
+        bytes[] memory encodedVMs,
+        bytes memory encodedDeliveryVAA,
+        address payable relayerRefundAddress,
+        bytes memory deliveryOverrides
+    ) external payable;
+}
+
+interface IWormholeRelayer is IWormholeRelayerDelivery, IWormholeRelayerSend {}
+
+/*
+ *  Errors thrown by IWormholeRelayer contract
+ */
+
+// Bound chosen by the following formula: `memoryWord * 4 + selectorSize`.
+// This means that an error identifier plus four fixed size arguments should be available to developers.
+// In the case of a `require` revert with error message, this should provide 2 memory word's worth of data.
+uint256 constant RETURNDATA_TRUNCATION_THRESHOLD = 132;
+
+//When msg.value was not equal to `delivery provider's quoted delivery price` + `paymentForExtraReceiverValue`
+error InvalidMsgValue(uint256 msgValue, uint256 totalFee);
+
+error RequestedGasLimitTooLow();
+
+error DeliveryProviderDoesNotSupportTargetChain(address relayer, uint16 chainId);
+error DeliveryProviderCannotReceivePayment();
+
+//When calling `forward()` on the WormholeRelayer if no delivery is in progress
+error NoDeliveryInProgress();
+//When calling `delivery()` a second time even though a delivery is already in progress
+error ReentrantDelivery(address msgSender, address lockedBy);
+//When any other contract but the delivery target calls `forward()` on the WormholeRelayer while a
+//  delivery is in progress
+error ForwardRequestFromWrongAddress(address msgSender, address deliveryTarget);
+
+error InvalidPayloadId(uint8 parsed, uint8 expected);
+error InvalidPayloadLength(uint256 received, uint256 expected);
+error InvalidVaaKeyType(uint8 parsed);
+
+error InvalidDeliveryVaa(string reason);
+//When the delivery VAA (signed wormhole message with delivery instructions) was not emitted by the
+//  registered WormholeRelayer contract
+error InvalidEmitter(bytes32 emitter, bytes32 registered, uint16 chainId);
+error VaaKeysLengthDoesNotMatchVaasLength(uint256 keys, uint256 vaas);
+error VaaKeysDoNotMatchVaas(uint8 index);
+//When someone tries to call an external function of the WormholeRelayer that is only intended to be
+//  called by the WormholeRelayer itself (to allow retroactive reverts for atomicity)
+error RequesterNotWormholeRelayer();
+
+//When trying to relay a `DeliveryInstruction` to any other chain but the one it was specified for
+error TargetChainIsNotThisChain(uint16 targetChain);
+error ForwardNotSufficientlyFunded(uint256 amountOfFunds, uint256 amountOfFundsNeeded);
+//When a `DeliveryOverride` contains a gas limit that's less than the original
+error InvalidOverrideGasLimit();
+//When a `DeliveryOverride` contains a receiver value that's less than the original
+error InvalidOverrideReceiverValue();
+//When a `DeliveryOverride` contains a 'refund per unit of gas unused' that's less than the original
+error InvalidOverrideRefundPerGasUnused();
+
+//When the delivery provider doesn't pass in sufficient funds (i.e. msg.value does not cover the
+// maximum possible refund to the user)
+error InsufficientRelayerFunds(uint256 msgValue, uint256 minimum);
+
+//When a bytes32 field can't be converted into a 20 byte EVM address, because the 12 padding bytes
+//  are non-zero (duplicated from Utils.sol)
+error NotAnEvmAddress(bytes32);

+ 711 - 0
ethereum/contracts/interfaces/relayer/IWormholeRelayerTyped.sol

@@ -0,0 +1,711 @@
+// SPDX-License-Identifier: Apache 2
+
+pragma solidity ^0.8.19;
+
+import "./TypedUnits.sol";
+
+/**
+ * @title WormholeRelayer
+ * @author 
+ * @notice This project allows developers to build cross-chain applications powered by Wormhole without needing to 
+ * write and run their own relaying infrastructure
+ * 
+ * We implement the IWormholeRelayer interface that allows users to request a delivery provider to relay a payload (and/or additional VAAs) 
+ * to a chain and address of their choice.
+ */
+
+/**
+ * @notice VaaKey identifies a wormhole message
+ *
+ * @custom:member chainId Wormhole chain ID of the chain where this VAA was emitted from
+ * @custom:member emitterAddress Address of the emitter of the VAA, in Wormhole bytes32 format
+ * @custom:member sequence Sequence number of the VAA
+ */
+struct VaaKey {
+    uint16 chainId;
+    bytes32 emitterAddress;
+    uint64 sequence;
+}
+
+interface IWormholeRelayerBase {
+    event SendEvent(
+        uint64 indexed sequence, LocalNative deliveryQuote, LocalNative paymentForExtraReceiverValue
+    );
+
+    function getRegisteredWormholeRelayerContract(uint16 chainId) external view returns (bytes32);
+}
+
+/**
+ * @title IWormholeRelayerSend
+ * @notice The interface to request deliveries
+ */
+interface IWormholeRelayerSend is IWormholeRelayerBase {
+
+    /**
+     * @notice Publishes an instruction for the default delivery provider
+     * to relay a payload to the address `targetAddress` on chain `targetChain` 
+     * with gas limit `gasLimit` and `msg.value` equal to `receiverValue`
+     * 
+     * `targetAddress` must implement the IWormholeReceiver interface
+     * 
+     * This function must be called with `msg.value` equal to `quoteEVMDeliveryPrice(targetChain, receiverValue, gasLimit)`
+     * 
+     * Any refunds (from leftover gas) will be paid to the delivery provider. In order to receive the refunds, use the `sendPayloadToEvm` function 
+     * with `refundChain` and `refundAddress` as parameters
+     * 
+     * @param targetChain in Wormhole Chain ID format
+     * @param targetAddress address to call on targetChain (that implements IWormholeReceiver) 
+     * @param payload arbitrary bytes to pass in as parameter in call to `targetAddress`
+     * @param receiverValue msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units)
+     * @param gasLimit gas limit with which to call `targetAddress`.
+     * @return sequence sequence number of published VAA containing delivery instructions
+     */
+    function sendPayloadToEvm(
+        uint16 targetChain,
+        address targetAddress,
+        bytes memory payload,
+        TargetNative receiverValue,
+        Gas gasLimit
+    ) external payable returns (uint64 sequence);
+
+    /**
+     * @notice Publishes an instruction for the default delivery provider
+     * to relay a payload to the address `targetAddress` on chain `targetChain` 
+     * with gas limit `gasLimit` and `msg.value` equal to `receiverValue`
+     * 
+     * Any refunds (from leftover gas) will be sent to `refundAddress` on chain `refundChain`
+     * `targetAddress` must implement the IWormholeReceiver interface
+     * 
+     * This function must be called with `msg.value` equal to `quoteEVMDeliveryPrice(targetChain, receiverValue, gasLimit)`
+     * 
+     * @param targetChain in Wormhole Chain ID format
+     * @param targetAddress address to call on targetChain (that implements IWormholeReceiver) 
+     * @param payload arbitrary bytes to pass in as parameter in call to `targetAddress`
+     * @param receiverValue msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units)
+     * @param gasLimit gas limit with which to call `targetAddress`. Any units of gas unused will be refunded according to the
+     *        `targetChainRefundPerGasUnused` rate quoted by the delivery provider
+     * @param refundChain The chain to deliver any refund to, in Wormhole Chain ID format
+     * @param refundAddress The address on `refundChain` to deliver any refund to
+     * @return sequence sequence number of published VAA containing delivery instructions
+     */
+    function sendPayloadToEvm(
+        uint16 targetChain,
+        address targetAddress,
+        bytes memory payload,
+        TargetNative receiverValue,
+        Gas gasLimit,
+        uint16 refundChain,
+        address refundAddress
+    ) external payable returns (uint64 sequence);
+
+    /**
+     * @notice Publishes an instruction for the default delivery provider
+     * to relay a payload and VAAs specified by `vaaKeys` to the address `targetAddress` on chain `targetChain` 
+     * with gas limit `gasLimit` and `msg.value` equal to `receiverValue`
+     * 
+     * `targetAddress` must implement the IWormholeReceiver interface
+     * 
+     * This function must be called with `msg.value` equal to `quoteEVMDeliveryPrice(targetChain, receiverValue, gasLimit)`
+     * 
+     * Any refunds (from leftover gas) will be paid to the delivery provider. In order to receive the refunds, use the `sendVaasToEvm` function 
+     * with `refundChain` and `refundAddress` as parameters
+     * 
+     * @param targetChain in Wormhole Chain ID format
+     * @param targetAddress address to call on targetChain (that implements IWormholeReceiver) 
+     * @param payload arbitrary bytes to pass in as parameter in call to `targetAddress`
+     * @param receiverValue msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units)
+     * @param gasLimit gas limit with which to call `targetAddress`. 
+     * @param vaaKeys Additional VAAs to pass in as parameter in call to `targetAddress`
+     * @return sequence sequence number of published VAA containing delivery instructions
+     */
+    function sendVaasToEvm(
+        uint16 targetChain,
+        address targetAddress,
+        bytes memory payload,
+        TargetNative receiverValue,
+        Gas gasLimit,
+        VaaKey[] memory vaaKeys
+    ) external payable returns (uint64 sequence);
+
+    /**
+     * @notice Publishes an instruction for the default delivery provider
+     * to relay a payload and VAAs specified by `vaaKeys` to the address `targetAddress` on chain `targetChain` 
+     * with gas limit `gasLimit` and `msg.value` equal to `receiverValue`
+     * 
+     * Any refunds (from leftover gas) will be sent to `refundAddress` on chain `refundChain`
+     * `targetAddress` must implement the IWormholeReceiver interface
+     * 
+     * This function must be called with `msg.value` equal to `quoteEVMDeliveryPrice(targetChain, receiverValue, gasLimit)`
+     * 
+     * @param targetChain in Wormhole Chain ID format
+     * @param targetAddress address to call on targetChain (that implements IWormholeReceiver) 
+     * @param payload arbitrary bytes to pass in as parameter in call to `targetAddress`
+     * @param receiverValue msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units)
+     * @param gasLimit gas limit with which to call `targetAddress`. Any units of gas unused will be refunded according to the 
+     *        `targetChainRefundPerGasUnused` rate quoted by the delivery provider
+     * @param vaaKeys Additional VAAs to pass in as parameter in call to `targetAddress`
+     * @param refundChain The chain to deliver any refund to, in Wormhole Chain ID format
+     * @param refundAddress The address on `refundChain` to deliver any refund to
+     * @return sequence sequence number of published VAA containing delivery instructions
+     */
+    function sendVaasToEvm(
+        uint16 targetChain,
+        address targetAddress,
+        bytes memory payload,
+        TargetNative receiverValue,
+        Gas gasLimit,
+        VaaKey[] memory vaaKeys,
+        uint16 refundChain,
+        address refundAddress
+    ) external payable returns (uint64 sequence);
+
+    /**
+     * @notice Publishes an instruction for the delivery provider at `deliveryProviderAddress` 
+     * to relay a payload and VAAs specified by `vaaKeys` to the address `targetAddress` on chain `targetChain` 
+     * with gas limit `gasLimit` and `msg.value` equal to 
+     * receiverValue + (arbitrary amount that is paid for by paymentForExtraReceiverValue of this chain's wei) in targetChain wei.
+     * 
+     * Any refunds (from leftover gas) will be sent to `refundAddress` on chain `refundChain`
+     * `targetAddress` must implement the IWormholeReceiver interface
+     * 
+     * This function must be called with `msg.value` equal to 
+     * quoteEVMDeliveryPrice(targetChain, receiverValue, gasLimit, deliveryProviderAddress) + paymentForExtraReceiverValue
+     * 
+     * @param targetChain in Wormhole Chain ID format
+     * @param targetAddress address to call on targetChain (that implements IWormholeReceiver) 
+     * @param payload arbitrary bytes to pass in as parameter in call to `targetAddress`
+     * @param receiverValue msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units)
+     * @param paymentForExtraReceiverValue amount (in current chain currency units) to spend on extra receiverValue 
+     *        (in addition to the `receiverValue` specified)
+     * @param gasLimit gas limit with which to call `targetAddress`. Any units of gas unused will be refunded according to the  
+     *        `targetChainRefundPerGasUnused` rate quoted by the delivery provider
+     * @param refundChain The chain to deliver any refund to, in Wormhole Chain ID format
+     * @param refundAddress The address on `refundChain` to deliver any refund to
+     * @param deliveryProviderAddress The address of the desired delivery provider's implementation of IDeliveryProvider
+     * @param vaaKeys Additional VAAs to pass in as parameter in call to `targetAddress`
+     * @param consistencyLevel Consistency level with which to publish the delivery instructions - see 
+     *        https://book.wormhole.com/wormhole/3_coreLayerContracts.html?highlight=consistency#consistency-levels
+     * @return sequence sequence number of published VAA containing delivery instructions
+     */
+    function sendToEvm(
+        uint16 targetChain,
+        address targetAddress,
+        bytes memory payload,
+        TargetNative receiverValue,
+        LocalNative paymentForExtraReceiverValue,
+        Gas gasLimit,
+        uint16 refundChain,
+        address refundAddress,
+        address deliveryProviderAddress,
+        VaaKey[] memory vaaKeys,
+        uint8 consistencyLevel
+    ) external payable returns (uint64 sequence);
+    
+    /**
+     * @notice Publishes an instruction for the delivery provider at `deliveryProviderAddress` 
+     * to relay a payload and VAAs specified by `vaaKeys` to the address `targetAddress` on chain `targetChain` 
+     * with `msg.value` equal to 
+     * receiverValue + (arbitrary amount that is paid for by paymentForExtraReceiverValue of this chain's wei) in targetChain wei.
+     * 
+     * Any refunds (from leftover gas) will be sent to `refundAddress` on chain `refundChain`
+     * `targetAddress` must implement the IWormholeReceiver interface
+     * 
+     * This function must be called with `msg.value` equal to 
+     * quoteDeliveryPrice(targetChain, receiverValue, encodedExecutionParameters, deliveryProviderAddress) + paymentForExtraReceiverValue  
+     * 
+     * @param targetChain in Wormhole Chain ID format
+     * @param targetAddress address to call on targetChain (that implements IWormholeReceiver), in Wormhole bytes32 format
+     * @param payload arbitrary bytes to pass in as parameter in call to `targetAddress`
+     * @param receiverValue msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units)
+     * @param paymentForExtraReceiverValue amount (in current chain currency units) to spend on extra receiverValue 
+     *        (in addition to the `receiverValue` specified)
+     * @param encodedExecutionParameters encoded information on how to execute delivery that may impact pricing
+     *        e.g. for version EVM_V1, this is a struct that encodes the `gasLimit` with which to call `targetAddress`
+     * @param refundChain The chain to deliver any refund to, in Wormhole Chain ID format
+     * @param refundAddress The address on `refundChain` to deliver any refund to, in Wormhole bytes32 format
+     * @param deliveryProviderAddress The address of the desired delivery provider's implementation of IDeliveryProvider
+     * @param vaaKeys Additional VAAs to pass in as parameter in call to `targetAddress`
+     * @param consistencyLevel Consistency level with which to publish the delivery instructions - see 
+     *        https://book.wormhole.com/wormhole/3_coreLayerContracts.html?highlight=consistency#consistency-levels
+     * @return sequence sequence number of published VAA containing delivery instructions
+     */
+    function send(
+        uint16 targetChain,
+        bytes32 targetAddress,
+        bytes memory payload,
+        TargetNative receiverValue,
+        LocalNative paymentForExtraReceiverValue,
+        bytes memory encodedExecutionParameters,
+        uint16 refundChain,
+        bytes32 refundAddress,
+        address deliveryProviderAddress,
+        VaaKey[] memory vaaKeys,
+        uint8 consistencyLevel
+    ) external payable returns (uint64 sequence);
+
+    /**
+     * @notice Performs the same function as a `send`, except:
+     * 1)  Can only be used during a delivery (i.e. in execution of `receiveWormholeMessages`)
+     * 2)  Is paid for (along with any other calls to forward) by (any msg.value passed in) + (refund leftover from current delivery)
+     * 3)  Only executes after `receiveWormholeMessages` is completed (and thus does not return a sequence number)
+     * 
+     * The refund from the delivery currently in progress will not be sent to the user; it will instead
+     * be paid to the delivery provider to perform the instruction specified here
+     * 
+     * Publishes an instruction for the same delivery provider (or default, if the same one doesn't support the new target chain)
+     * to relay a payload to the address `targetAddress` on chain `targetChain` 
+     * with gas limit `gasLimit` and with `msg.value` equal to `receiverValue`
+     * 
+     * The following equation must be satisfied (sum_f indicates summing over all forwards requested in `receiveWormholeMessages`):
+     * (refund amount from current execution of receiveWormholeMessages) + sum_f [msg.value_f]
+     * >= sum_f [quoteEVMDeliveryPrice(targetChain_f, receiverValue_f, gasLimit_f)]
+     * 
+     * The difference between the two sides of the above inequality will be added to `paymentForExtraReceiverValue` of the first forward requested
+     * 
+     * Any refunds (from leftover gas) from this forward will be paid to the same refundChain and refundAddress specified for the current delivery.
+     * 
+     * @param targetChain in Wormhole Chain ID format
+     * @param targetAddress address to call on targetChain (that implements IWormholeReceiver), in Wormhole bytes32 format
+     * @param payload arbitrary bytes to pass in as parameter in call to `targetAddress`
+     * @param receiverValue msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units)
+     * @param gasLimit gas limit with which to call `targetAddress`.
+     */
+    function forwardPayloadToEvm(
+        uint16 targetChain,
+        address targetAddress,
+        bytes memory payload,
+        TargetNative receiverValue,
+        Gas gasLimit
+    ) external payable;
+
+    /**
+     * @notice Performs the same function as a `send`, except:
+     * 1)  Can only be used during a delivery (i.e. in execution of `receiveWormholeMessages`)
+     * 2)  Is paid for (along with any other calls to forward) by (any msg.value passed in) + (refund leftover from current delivery)
+     * 3)  Only executes after `receiveWormholeMessages` is completed (and thus does not return a sequence number)
+     * 
+     * The refund from the delivery currently in progress will not be sent to the user; it will instead
+     * be paid to the delivery provider to perform the instruction specified here
+     * 
+     * Publishes an instruction for the same delivery provider (or default, if the same one doesn't support the new target chain)
+     * to relay a payload and VAAs specified by `vaaKeys` to the address `targetAddress` on chain `targetChain` 
+     * with gas limit `gasLimit` and with `msg.value` equal to `receiverValue`
+     * 
+     * The following equation must be satisfied (sum_f indicates summing over all forwards requested in `receiveWormholeMessages`):
+     * (refund amount from current execution of receiveWormholeMessages) + sum_f [msg.value_f]
+     * >= sum_f [quoteEVMDeliveryPrice(targetChain_f, receiverValue_f, gasLimit_f)]
+     * 
+     * The difference between the two sides of the above inequality will be added to `paymentForExtraReceiverValue` of the first forward requested
+     * 
+     * Any refunds (from leftover gas) from this forward will be paid to the same refundChain and refundAddress specified for the current delivery.
+     * 
+     * @param targetChain in Wormhole Chain ID format
+     * @param targetAddress address to call on targetChain (that implements IWormholeReceiver), in Wormhole bytes32 format
+     * @param payload arbitrary bytes to pass in as parameter in call to `targetAddress`
+     * @param receiverValue msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units)
+     * @param gasLimit gas limit with which to call `targetAddress`. 
+     * @param vaaKeys Additional VAAs to pass in as parameter in call to `targetAddress`
+     */
+    function forwardVaasToEvm(
+        uint16 targetChain,
+        address targetAddress,
+        bytes memory payload,
+        TargetNative receiverValue,
+        Gas gasLimit,
+        VaaKey[] memory vaaKeys
+    ) external payable;
+
+    /**
+     * @notice Performs the same function as a `send`, except:
+     * 1)  Can only be used during a delivery (i.e. in execution of `receiveWormholeMessages`)
+     * 2)  Is paid for (along with any other calls to forward) by (any msg.value passed in) + (refund leftover from current delivery)
+     * 3)  Only executes after `receiveWormholeMessages` is completed (and thus does not return a sequence number)
+     * 
+     * The refund from the delivery currently in progress will not be sent to the user; it will instead
+     * be paid to the delivery provider to perform the instruction specified here
+     * 
+     * Publishes an instruction for the delivery provider at `deliveryProviderAddress` 
+     * to relay a payload and VAAs specified by `vaaKeys` to the address `targetAddress` on chain `targetChain` 
+     * with gas limit `gasLimit` and with `msg.value` equal to 
+     * receiverValue + (arbitrary amount that is paid for by paymentForExtraReceiverValue of this chain's wei) in targetChain wei.
+     * 
+     * Any refunds (from leftover gas) will be sent to `refundAddress` on chain `refundChain`
+     * `targetAddress` must implement the IWormholeReceiver interface
+     * 
+     * The following equation must be satisfied (sum_f indicates summing over all forwards requested in `receiveWormholeMessages`):
+     * (refund amount from current execution of receiveWormholeMessages) + sum_f [msg.value_f]
+     * >= sum_f [quoteEVMDeliveryPrice(targetChain_f, receiverValue_f, gasLimit_f, deliveryProviderAddress_f) + paymentForExtraReceiverValue_f]
+     * 
+     * The difference between the two sides of the above inequality will be added to `paymentForExtraReceiverValue` of the first forward requested
+     * 
+     * @param targetChain in Wormhole Chain ID format
+     * @param targetAddress address to call on targetChain (that implements IWormholeReceiver), in Wormhole bytes32 format
+     * @param payload arbitrary bytes to pass in as parameter in call to `targetAddress`
+     * @param receiverValue msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units)
+     * @param paymentForExtraReceiverValue amount (in current chain currency units) to spend on extra receiverValue 
+     *        (in addition to the `receiverValue` specified)
+     * @param gasLimit gas limit with which to call `targetAddress`. Any units of gas unused will be refunded according to the  
+     *        `targetChainRefundPerGasUnused` rate quoted by the delivery provider
+     * @param refundChain The chain to deliver any refund to, in Wormhole Chain ID format
+     * @param refundAddress The address on `refundChain` to deliver any refund to, in Wormhole bytes32 format
+     * @param deliveryProviderAddress The address of the desired delivery provider's implementation of IDeliveryProvider
+     * @param vaaKeys Additional VAAs to pass in as parameter in call to `targetAddress`
+     * @param consistencyLevel Consistency level with which to publish the delivery instructions - see 
+     *        https://book.wormhole.com/wormhole/3_coreLayerContracts.html?highlight=consistency#consistency-levels
+     */
+    function forwardToEvm(
+        uint16 targetChain,
+        address targetAddress,
+        bytes memory payload,
+        TargetNative receiverValue,
+        LocalNative paymentForExtraReceiverValue,
+        Gas gasLimit,
+        uint16 refundChain,
+        address refundAddress,
+        address deliveryProviderAddress,
+        VaaKey[] memory vaaKeys,
+        uint8 consistencyLevel
+    ) external payable;
+
+    /**
+     * @notice Performs the same function as a `send`, except:
+     * 1)  Can only be used during a delivery (i.e. in execution of `receiveWormholeMessages`)
+     * 2)  Is paid for (along with any other calls to forward) by (any msg.value passed in) + (refund leftover from current delivery)
+     * 3)  Only executes after `receiveWormholeMessages` is completed (and thus does not return a sequence number)
+     * 
+     * The refund from the delivery currently in progress will not be sent to the user; it will instead
+     * be paid to the delivery provider to perform the instruction specified here
+     * 
+     * Publishes an instruction for the delivery provider at `deliveryProviderAddress` 
+     * to relay a payload and VAAs specified by `vaaKeys` to the address `targetAddress` on chain `targetChain` 
+     * with `msg.value` equal to 
+     * receiverValue + (arbitrary amount that is paid for by paymentForExtraReceiverValue of this chain's wei) in targetChain wei.
+     * 
+     * Any refunds (from leftover gas) will be sent to `refundAddress` on chain `refundChain`
+     * `targetAddress` must implement the IWormholeReceiver interface
+     * 
+     * The following equation must be satisfied (sum_f indicates summing over all forwards requested in `receiveWormholeMessages`):
+     * (refund amount from current execution of receiveWormholeMessages) + sum_f [msg.value_f]
+     * >= sum_f [quoteDeliveryPrice(targetChain_f, receiverValue_f, encodedExecutionParameters_f, deliveryProviderAddress_f) + paymentForExtraReceiverValue_f]
+     * 
+     * The difference between the two sides of the above inequality will be added to `paymentForExtraReceiverValue` of the first forward requested
+     * 
+     * @param targetChain in Wormhole Chain ID format
+     * @param targetAddress address to call on targetChain (that implements IWormholeReceiver), in Wormhole bytes32 format
+     * @param payload arbitrary bytes to pass in as parameter in call to `targetAddress`
+     * @param receiverValue msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units)
+     * @param paymentForExtraReceiverValue amount (in current chain currency units) to spend on extra receiverValue 
+     *        (in addition to the `receiverValue` specified)
+     * @param encodedExecutionParameters encoded information on how to execute delivery that may impact pricing
+     *        e.g. for version EVM_V1, this is a struct that encodes the `gasLimit` with which to call `targetAddress`
+     * @param refundChain The chain to deliver any refund to, in Wormhole Chain ID format
+     * @param refundAddress The address on `refundChain` to deliver any refund to, in Wormhole bytes32 format
+     * @param deliveryProviderAddress The address of the desired delivery provider's implementation of IDeliveryProvider
+     * @param vaaKeys Additional VAAs to pass in as parameter in call to `targetAddress`
+     * @param consistencyLevel Consistency level with which to publish the delivery instructions - see 
+     *        https://book.wormhole.com/wormhole/3_coreLayerContracts.html?highlight=consistency#consistency-levels
+     */
+    function forward(
+        uint16 targetChain,
+        bytes32 targetAddress,
+        bytes memory payload,
+        TargetNative receiverValue,
+        LocalNative paymentForExtraReceiverValue,
+        bytes memory encodedExecutionParameters,
+        uint16 refundChain,
+        bytes32 refundAddress,
+        address deliveryProviderAddress,
+        VaaKey[] memory vaaKeys,
+        uint8 consistencyLevel
+    ) external payable;
+
+    /**
+     * @notice Requests a previously published delivery instruction to be redelivered 
+     * (e.g. with a different delivery provider)
+     *
+     * This function must be called with `msg.value` equal to 
+     * quoteEVMDeliveryPrice(targetChain, newReceiverValue, newGasLimit, newDeliveryProviderAddress)
+     * 
+     *  @notice *** This will only be able to succeed if the following is true **
+     *         - newGasLimit >= gas limit of the old instruction
+     *         - newReceiverValue >= receiver value of the old instruction
+     *         - newDeliveryProvider's `targetChainRefundPerGasUnused` >= old relay provider's `targetChainRefundPerGasUnused`
+     * 
+     * @param deliveryVaaKey VaaKey identifying the wormhole message containing the 
+     *        previously published delivery instructions
+     * @param targetChain The target chain that the original delivery targeted. Must match targetChain from original delivery instructions
+     * @param newReceiverValue new msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units)
+     * @param newGasLimit gas limit with which to call `targetAddress`. Any units of gas unused will be refunded according to the  
+     *        `targetChainRefundPerGasUnused` rate quoted by the delivery provider, to the refund chain and address specified in the original request
+     * @param newDeliveryProviderAddress The address of the desired delivery provider's implementation of IDeliveryProvider
+     * @return sequence sequence number of published VAA containing redelivery instructions
+     *
+     * @notice *** This will only be able to succeed if the following is true **
+     *         - newGasLimit >= gas limit of the old instruction
+     *         - newReceiverValue >= receiver value of the old instruction
+     *         - newDeliveryProvider's `targetChainRefundPerGasUnused` >= old relay provider's `targetChainRefundPerGasUnused`
+     */
+    function resendToEvm(
+        VaaKey memory deliveryVaaKey,
+        uint16 targetChain,
+        TargetNative newReceiverValue,
+        Gas newGasLimit,
+        address newDeliveryProviderAddress
+    ) external payable returns (uint64 sequence);
+
+    /**
+     * @notice Requests a previously published delivery instruction to be redelivered 
+     * 
+     *
+     * This function must be called with `msg.value` equal to 
+     * quoteDeliveryPrice(targetChain, newReceiverValue, newEncodedExecutionParameters, newDeliveryProviderAddress)
+     * 
+     * @param deliveryVaaKey VaaKey identifying the wormhole message containing the 
+     *        previously published delivery instructions
+     * @param targetChain The target chain that the original delivery targeted. Must match targetChain from original delivery instructions
+     * @param newReceiverValue new msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units)
+     * @param newEncodedExecutionParameters new encoded information on how to execute delivery that may impact pricing
+     *        e.g. for version EVM_V1, this is a struct that encodes the `gasLimit` with which to call `targetAddress`
+     * @param newDeliveryProviderAddress The address of the desired delivery provider's implementation of IDeliveryProvider
+     * @return sequence sequence number of published VAA containing redelivery instructions
+     * 
+     *  @notice *** This will only be able to succeed if the following is true **
+     *         - (For EVM_V1) newGasLimit >= gas limit of the old instruction
+     *         - newReceiverValue >= receiver value of the old instruction
+     *         - (For EVM_V1) newDeliveryProvider's `targetChainRefundPerGasUnused` >= old relay provider's `targetChainRefundPerGasUnused`
+     */
+    function resend(
+        VaaKey memory deliveryVaaKey,
+        uint16 targetChain,
+        TargetNative newReceiverValue,
+        bytes memory newEncodedExecutionParameters,
+        address newDeliveryProviderAddress
+    ) external payable returns (uint64 sequence);
+
+    /**
+     * @notice Returns the price to request a relay to chain `targetChain`, using the default delivery provider
+     * 
+     * @param targetChain in Wormhole Chain ID format
+     * @param receiverValue msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units)
+     * @param gasLimit gas limit with which to call `targetAddress`. 
+     * @return nativePriceQuote Price, in units of current chain currency, that the delivery provider charges to perform the relay
+     * @return targetChainRefundPerGasUnused amount of target chain currency that will be refunded per unit of gas unused, 
+     *         if a refundAddress is specified
+     */
+    function quoteEVMDeliveryPrice(
+        uint16 targetChain,
+        TargetNative receiverValue,
+        Gas gasLimit
+    ) external view returns (LocalNative nativePriceQuote, GasPrice targetChainRefundPerGasUnused);
+
+    /**
+     * @notice Returns the price to request a relay to chain `targetChain`, using delivery provider `deliveryProviderAddress`
+     * 
+     * @param targetChain in Wormhole Chain ID format
+     * @param receiverValue msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units)
+     * @param gasLimit gas limit with which to call `targetAddress`. 
+     * @param deliveryProviderAddress The address of the desired delivery provider's implementation of IDeliveryProvider
+     * @return nativePriceQuote Price, in units of current chain currency, that the delivery provider charges to perform the relay
+     * @return targetChainRefundPerGasUnused amount of target chain currency that will be refunded per unit of gas unused, 
+     *         if a refundAddress is specified
+     */
+    function quoteEVMDeliveryPrice(
+        uint16 targetChain,
+        TargetNative receiverValue,
+        Gas gasLimit,
+        address deliveryProviderAddress
+    ) external view returns (LocalNative nativePriceQuote, GasPrice targetChainRefundPerGasUnused);
+
+    /**
+     * @notice Returns the price to request a relay to chain `targetChain`, using delivery provider `deliveryProviderAddress`
+     * 
+     * @param targetChain in Wormhole Chain ID format
+     * @param receiverValue msg.value that delivery provider should pass in for call to `targetAddress` (in targetChain currency units)
+     * @param encodedExecutionParameters encoded information on how to execute delivery that may impact pricing
+     *        e.g. for version EVM_V1, this is a struct that encodes the `gasLimit` with which to call `targetAddress`
+     * @param deliveryProviderAddress The address of the desired delivery provider's implementation of IDeliveryProvider
+     * @return nativePriceQuote Price, in units of current chain currency, that the delivery provider charges to perform the relay
+     * @return encodedExecutionInfo encoded information on how the delivery will be executed
+     *        e.g. for version EVM_V1, this is a struct that encodes the `gasLimit` and `targetChainRefundPerGasUnused`
+     *             (which is the amount of target chain currency that will be refunded per unit of gas unused, 
+     *              if a refundAddress is specified)
+     */
+    function quoteDeliveryPrice(
+        uint16 targetChain,
+        TargetNative receiverValue,
+        bytes memory encodedExecutionParameters,
+        address deliveryProviderAddress
+    ) external view returns (LocalNative nativePriceQuote, bytes memory encodedExecutionInfo);
+
+    /**
+     * @notice Returns the (extra) amount of target chain currency that `targetAddress`
+     * will be called with, if the `paymentForExtraReceiverValue` field is set to `currentChainAmount`
+     * 
+     * @param targetChain in Wormhole Chain ID format
+     * @param currentChainAmount The value that `paymentForExtraReceiverValue` will be set to
+     * @param deliveryProviderAddress The address of the desired delivery provider's implementation of IDeliveryProvider
+     * @return targetChainAmount The amount such that if `targetAddress` will be called with `msg.value` equal to
+     *         receiverValue + targetChainAmount
+     */
+    function quoteNativeForChain(
+        uint16 targetChain,
+        LocalNative currentChainAmount,
+        address deliveryProviderAddress
+    ) external view returns (TargetNative targetChainAmount);
+
+    /**
+     * @notice Returns the address of the current default delivery provider
+     * @return deliveryProvider The address of (the default delivery provider)'s contract on this source
+     *   chain. This must be a contract that implements IDeliveryProvider.
+     */
+    function getDefaultDeliveryProvider() external view returns (address deliveryProvider);
+}
+
+/**
+ * @title IWormholeRelayerDelivery
+ * @notice The interface to execute deliveries. Only relevant for Delivery Providers 
+ */
+interface IWormholeRelayerDelivery is IWormholeRelayerBase {
+    enum DeliveryStatus {
+        SUCCESS,
+        RECEIVER_FAILURE,
+        FORWARD_REQUEST_FAILURE,
+        FORWARD_REQUEST_SUCCESS
+    }
+
+    enum RefundStatus {
+        REFUND_SENT,
+        REFUND_FAIL,
+        CROSS_CHAIN_REFUND_SENT,
+        CROSS_CHAIN_REFUND_FAIL_PROVIDER_NOT_SUPPORTED,
+        CROSS_CHAIN_REFUND_FAIL_NOT_ENOUGH
+    }
+
+    /**
+     * @custom:member recipientContract - The target contract address
+     * @custom:member sourceChain - The chain which this delivery was requested from (in wormhole
+     *     ChainID format)
+     * @custom:member sequence - The wormhole sequence number of the delivery VAA on the source chain
+     *     corresponding to this delivery request
+     * @custom:member deliveryVaaHash - The hash of the delivery VAA corresponding to this delivery
+     *     request
+     * @custom:member gasUsed - The amount of gas that was used to call your target contract 
+     * @custom:member status:
+     *   - RECEIVER_FAILURE, if the target contract reverts
+     *   - SUCCESS, if the target contract doesn't revert and no forwards were requested
+     *   - FORWARD_REQUEST_FAILURE, if the target contract doesn't revert, forwards were requested,
+     *       but provided/leftover funds were not sufficient to cover them all
+     *   - FORWARD_REQUEST_SUCCESS, if the target contract doesn't revert and all forwards are covered
+     * @custom:member additionalStatusInfo:
+     *   - If status is SUCCESS or FORWARD_REQUEST_SUCCESS, then this is empty.
+     *   - If status is RECEIVER_FAILURE, this is `RETURNDATA_TRUNCATION_THRESHOLD` bytes of the
+     *       return data (i.e. potentially truncated revert reason information).
+     *   - If status is FORWARD_REQUEST_FAILURE, this is also the revert data - the reason the forward failed.
+     *     This will be either an encoded Cancelled, DeliveryProviderReverted, or DeliveryProviderPaymentFailed error
+     * @custom:member refundStatus - Result of the refund. REFUND_SUCCESS or REFUND_FAIL are for
+     *     refunds where targetChain=refundChain; the others are for targetChain!=refundChain,
+     *     where a cross chain refund is necessary
+     * @custom:member overridesInfo:
+     *   - If not an override: empty bytes array
+     *   - Otherwise: An encoded `DeliveryOverride`
+     */
+    event Delivery(
+        address indexed recipientContract,
+        uint16 indexed sourceChain,
+        uint64 indexed sequence,
+        bytes32 deliveryVaaHash,
+        DeliveryStatus status,
+        Gas gasUsed,
+        RefundStatus refundStatus,
+        bytes additionalStatusInfo,
+        bytes overridesInfo
+    );
+
+    /**
+     * @notice The delivery provider calls `deliver` to relay messages as described by one delivery instruction
+     * 
+     * The delivery provider must pass in the specified (by VaaKeys[]) signed wormhole messages (VAAs) from the source chain
+     * as well as the signed wormhole message with the delivery instructions (the delivery VAA)
+     *
+     * The messages will be relayed to the target address (with the specified gas limit and receiver value) iff the following checks are met:
+     * - the delivery VAA has a valid signature
+     * - the delivery VAA's emitter is one of these WormholeRelayer contracts
+     * - the delivery provider passed in at least enough of this chain's currency as msg.value (enough meaning the maximum possible refund)     
+     * - the instruction's target chain is this chain
+     * - the relayed signed VAAs match the descriptions in container.messages (the VAA hashes match, or the emitter address, sequence number pair matches, depending on the description given)
+     *
+     * @param encodedVMs - An array of signed wormhole messages (all from the same source chain
+     *     transaction)
+     * @param encodedDeliveryVAA - Signed wormhole message from the source chain's WormholeRelayer
+     *     contract with payload being the encoded delivery instruction container
+     * @param relayerRefundAddress - The address to which any refunds to the delivery provider
+     *     should be sent
+     * @param deliveryOverrides - Optional overrides field which must be either an empty bytes array or
+     *     an encoded DeliveryOverride struct
+     */
+    function deliver(
+        bytes[] memory encodedVMs,
+        bytes memory encodedDeliveryVAA,
+        address payable relayerRefundAddress,
+        bytes memory deliveryOverrides
+    ) external payable;
+}
+
+interface IWormholeRelayer is IWormholeRelayerDelivery, IWormholeRelayerSend {}
+
+/*
+ *  Errors thrown by IWormholeRelayer contract
+ */
+
+// Bound chosen by the following formula: `memoryWord * 4 + selectorSize`.
+// This means that an error identifier plus four fixed size arguments should be available to developers.
+// In the case of a `require` revert with error message, this should provide 2 memory word's worth of data.
+uint256 constant RETURNDATA_TRUNCATION_THRESHOLD = 132;
+
+//When msg.value was not equal to `delivery provider's quoted delivery price` + `paymentForExtraReceiverValue`
+error InvalidMsgValue(LocalNative msgValue, LocalNative totalFee);
+
+error RequestedGasLimitTooLow();
+
+error DeliveryProviderDoesNotSupportTargetChain(address relayer, uint16 chainId);
+error DeliveryProviderCannotReceivePayment();
+
+//When calling `forward()` on the WormholeRelayer if no delivery is in progress
+error NoDeliveryInProgress();
+//When calling `delivery()` a second time even though a delivery is already in progress
+error ReentrantDelivery(address msgSender, address lockedBy);
+//When any other contract but the delivery target calls `forward()` on the WormholeRelayer while a
+//  delivery is in progress
+error ForwardRequestFromWrongAddress(address msgSender, address deliveryTarget);
+
+error InvalidPayloadId(uint8 parsed, uint8 expected);
+error InvalidPayloadLength(uint256 received, uint256 expected);
+error InvalidVaaKeyType(uint8 parsed);
+
+error InvalidDeliveryVaa(string reason);
+//When the delivery VAA (signed wormhole message with delivery instructions) was not emitted by the
+//  registered WormholeRelayer contract
+error InvalidEmitter(bytes32 emitter, bytes32 registered, uint16 chainId);
+error VaaKeysLengthDoesNotMatchVaasLength(uint256 keys, uint256 vaas);
+error VaaKeysDoNotMatchVaas(uint8 index);
+//When someone tries to call an external function of the WormholeRelayer that is only intended to be
+//  called by the WormholeRelayer itself (to allow retroactive reverts for atomicity)
+error RequesterNotWormholeRelayer();
+
+//When trying to relay a `DeliveryInstruction` to any other chain but the one it was specified for
+error TargetChainIsNotThisChain(uint16 targetChain);
+error ForwardNotSufficientlyFunded(LocalNative amountOfFunds, LocalNative amountOfFundsNeeded);
+//When a `DeliveryOverride` contains a gas limit that's less than the original
+error InvalidOverrideGasLimit();
+//When a `DeliveryOverride` contains a receiver value that's less than the original
+error InvalidOverrideReceiverValue();
+//When a `DeliveryOverride` contains a 'refund per unit of gas unused' that's less than the original
+error InvalidOverrideRefundPerGasUnused();
+
+//When the delivery provider doesn't pass in sufficient funds (i.e. msg.value does not cover the
+// maximum possible refund to the user)
+error InsufficientRelayerFunds(LocalNative msgValue, LocalNative minimum);
+
+//When a bytes32 field can't be converted into a 20 byte EVM address, because the 12 padding bytes
+//  are non-zero (duplicated from Utils.sol)
+error NotAnEvmAddress(bytes32);

+ 263 - 0
ethereum/contracts/interfaces/relayer/TypedUnits.sol

@@ -0,0 +1,263 @@
+// SPDX-License-Identifier: Apache 2
+
+pragma solidity ^0.8.19;
+
+type WeiPrice is uint256;
+
+type GasPrice is uint256;
+
+type Gas is uint256;
+
+type Dollar is uint256;
+
+type Wei is uint256;
+
+type LocalNative is uint256;
+
+type TargetNative is uint256;
+
+using {
+    addWei as +,
+    subWei as -,
+    lteWei as <=,
+    ltWei as <,
+    gtWei as >,
+    eqWei as ==,
+    neqWei as !=
+} for Wei global;
+using {addTargetNative as +, subTargetNative as -} for TargetNative global;
+using {
+    leLocalNative as <,
+    leqLocalNative as <=,
+    neqLocalNative as !=,
+    addLocalNative as +,
+    subLocalNative as -
+} for LocalNative global;
+using {
+    ltGas as <,
+    lteGas as <=,
+    subGas as -
+} for Gas global;
+
+using WeiLib for Wei;
+using GasLib for Gas;
+using DollarLib for Dollar;
+using WeiPriceLib for WeiPrice;
+using GasPriceLib for GasPrice;
+
+function ltWei(Wei a, Wei b) pure returns (bool) {
+    return Wei.unwrap(a) < Wei.unwrap(b);
+}
+
+function eqWei(Wei a, Wei b) pure returns (bool) {
+    return Wei.unwrap(a) == Wei.unwrap(b);
+}
+
+function gtWei(Wei a, Wei b) pure returns (bool) {
+    return Wei.unwrap(a) > Wei.unwrap(b);
+}
+
+function lteWei(Wei a, Wei b) pure returns (bool) {
+    return Wei.unwrap(a) <= Wei.unwrap(b);
+}
+
+function subWei(Wei a, Wei b) pure returns (Wei) {
+    return Wei.wrap(Wei.unwrap(a) - Wei.unwrap(b));
+}
+
+function addWei(Wei a, Wei b) pure returns (Wei) {
+    return Wei.wrap(Wei.unwrap(a) + Wei.unwrap(b));
+}
+
+function neqWei(Wei a, Wei b) pure returns (bool) {
+    return Wei.unwrap(a) != Wei.unwrap(b);
+}
+
+function ltGas(Gas a, Gas b) pure returns (bool) {
+    return Gas.unwrap(a) < Gas.unwrap(b);
+}
+
+function lteGas(Gas a, Gas b) pure returns (bool) {
+    return Gas.unwrap(a) <= Gas.unwrap(b);
+}
+
+function subGas(Gas a, Gas b) pure returns (Gas) {
+    return Gas.wrap(Gas.unwrap(a) - Gas.unwrap(b));
+}
+
+function addTargetNative(TargetNative a, TargetNative b) pure returns (TargetNative) {
+    return TargetNative.wrap(TargetNative.unwrap(a) + TargetNative.unwrap(b));
+}
+
+function subTargetNative(TargetNative a, TargetNative b) pure returns (TargetNative) {
+    return TargetNative.wrap(TargetNative.unwrap(a) - TargetNative.unwrap(b));
+}
+
+function addLocalNative(LocalNative a, LocalNative b) pure returns (LocalNative) {
+    return LocalNative.wrap(LocalNative.unwrap(a) + LocalNative.unwrap(b));
+}
+
+function subLocalNative(LocalNative a, LocalNative b) pure returns (LocalNative) {
+    return LocalNative.wrap(LocalNative.unwrap(a) - LocalNative.unwrap(b));
+}
+
+function neqLocalNative(LocalNative a, LocalNative b) pure returns (bool) {
+    return LocalNative.unwrap(a) != LocalNative.unwrap(b);
+}
+
+function leLocalNative(LocalNative a, LocalNative b) pure returns (bool) {
+    return LocalNative.unwrap(a) < LocalNative.unwrap(b);
+}
+
+function leqLocalNative(LocalNative a, LocalNative b) pure returns (bool) {
+    return LocalNative.unwrap(a) <= LocalNative.unwrap(b);
+}
+
+library WeiLib {
+    using {
+        toDollars,
+        toGas,
+        convertAsset,
+        min,
+        max,
+        scale,
+        unwrap,
+        asGasPrice,
+        asTargetNative,
+        asLocalNative
+    } for Wei;
+
+    function min(Wei x, Wei maxVal) internal pure returns (Wei) {
+        return x > maxVal ? maxVal : x;
+    }
+
+    function max(Wei x, Wei maxVal) internal pure returns (Wei) {
+        return x < maxVal ? maxVal : x;
+    }
+
+    function asTargetNative(Wei w) internal pure returns (TargetNative) {
+        return TargetNative.wrap(Wei.unwrap(w));
+    }
+
+    function asLocalNative(Wei w) internal pure returns (LocalNative) {
+        return LocalNative.wrap(Wei.unwrap(w));
+    }
+
+    function toDollars(Wei w, WeiPrice price) internal pure returns (Dollar) {
+        return Dollar.wrap(Wei.unwrap(w) * WeiPrice.unwrap(price));
+    }
+
+    function toGas(Wei w, GasPrice price) internal pure returns (Gas) {
+        return Gas.wrap(Wei.unwrap(w) / GasPrice.unwrap(price));
+    }
+
+    function scale(Wei w, Gas num, Gas denom) internal pure returns (Wei) {
+        return Wei.wrap(Wei.unwrap(w) * Gas.unwrap(num) / Gas.unwrap(denom));
+    }
+
+    function unwrap(Wei w) internal pure returns (uint256) {
+        return Wei.unwrap(w);
+    }
+
+    function asGasPrice(Wei w) internal pure returns (GasPrice) {
+        return GasPrice.wrap(Wei.unwrap(w));
+    }
+
+    function convertAsset(
+        Wei w,
+        WeiPrice fromPrice,
+        WeiPrice toPrice,
+        uint32 multiplierNum,
+        uint32 multiplierDenom,
+        bool roundUp
+    ) internal pure returns (Wei) {
+        Dollar numerator = w.toDollars(fromPrice).mul(multiplierNum);
+        WeiPrice denom = toPrice.mul(multiplierDenom);
+        Wei res = numerator.toWei(denom, roundUp);
+        return res;
+    }
+}
+
+library GasLib {
+    using {toWei, unwrap} for Gas;
+
+    function min(Gas x, Gas maxVal) internal pure returns (Gas) {
+        return x < maxVal ? x : maxVal;
+    }
+
+    function toWei(Gas w, GasPrice price) internal pure returns (Wei) {
+        return Wei.wrap(w.unwrap() * price.unwrap());
+    }
+
+    function unwrap(Gas w) internal pure returns (uint256) {
+        return Gas.unwrap(w);
+    }
+}
+
+library DollarLib {
+    using {toWei, mul, unwrap} for Dollar;
+
+    function mul(Dollar a, uint256 b) internal pure returns (Dollar) {
+        return Dollar.wrap(a.unwrap() * b);
+    }
+
+    function toWei(Dollar w, WeiPrice price, bool roundUp) internal pure returns (Wei) {
+        return Wei.wrap((w.unwrap() + (roundUp ? price.unwrap() - 1 : 0)) / price.unwrap());
+    }
+
+    function toGas(Dollar w, GasPrice price, WeiPrice weiPrice) internal pure returns (Gas) {
+        return w.toWei(weiPrice, false).toGas(price);
+    }
+
+    function unwrap(Dollar w) internal pure returns (uint256) {
+        return Dollar.unwrap(w);
+    }
+}
+
+library WeiPriceLib {
+    using {mul, unwrap} for WeiPrice;
+
+    function mul(WeiPrice a, uint256 b) internal pure returns (WeiPrice) {
+        return WeiPrice.wrap(a.unwrap() * b);
+    }
+
+    function unwrap(WeiPrice w) internal pure returns (uint256) {
+        return WeiPrice.unwrap(w);
+    }
+}
+
+library GasPriceLib {
+    using {unwrap, priceAsWei} for GasPrice;
+
+    function priceAsWei(GasPrice w) internal pure returns (Wei) {
+        return Wei.wrap(w.unwrap());
+    }
+
+    function unwrap(GasPrice w) internal pure returns (uint256) {
+        return GasPrice.unwrap(w);
+    }
+}
+
+library TargetNativeLib {
+    using {unwrap, asNative} for TargetNative;
+
+    function unwrap(TargetNative w) internal pure returns (uint256) {
+        return TargetNative.unwrap(w);
+    }
+
+    function asNative(TargetNative w) internal pure returns (Wei) {
+        return Wei.wrap(TargetNative.unwrap(w));
+    }
+}
+
+library LocalNativeLib {
+    using {unwrap, asNative} for LocalNative;
+
+    function unwrap(LocalNative w) internal pure returns (uint256) {
+        return LocalNative.unwrap(w);
+    }
+
+    function asNative(LocalNative w) internal pure returns (Wei) {
+        return Wei.wrap(LocalNative.unwrap(w));
+    }
+}

+ 1267 - 0
ethereum/contracts/libraries/relayer/BytesParsing.sol

@@ -0,0 +1,1267 @@
+pragma solidity ^0.8.19;
+
+library BytesParsing {
+  uint256 private constant freeMemoryPtr = 0x40;
+  uint256 private constant wordSize = 32;
+
+  error OutOfBounds(uint256 offset, uint256 length);
+
+  function checkBound(uint offset, uint length) internal pure {
+    if (offset > length)
+      revert OutOfBounds(offset, length);
+  }
+
+  function sliceUnchecked(
+    bytes memory encoded,
+    uint offset,
+    uint length
+  ) internal pure returns (bytes memory ret, uint nextOffset) {
+    //bail early for degenerate case
+    if (length == 0)
+      return (new bytes(0), offset);
+
+    assembly ("memory-safe") {
+      nextOffset := add(offset, length)
+      ret := mload(freeMemoryPtr)
+
+      //Explanation on how we copy data here:
+      //  The bytes type has the following layout in memory:
+      //    [length: 32 bytes, data: length bytes]
+      //  So if we allocate `bytes memory foo = new bytes(1);` then `foo` will be a pointer to 33
+      //    bytes where the first 32 bytes contain the length and the last byte is the actual data.
+      //  Since mload always loads 32 bytes of memory at once, we use our shift variable to align
+      //    our reads so that our last read lines up exactly with the last 32 bytes of `encoded`.
+      //  However this also means that if the length of `encoded` is not a multiple of 32 bytes, our
+      //    first read will necessarily partly contain bytes from `encoded`'s 32 length bytes that
+      //    will be written into the length part of our `ret` slice.
+      //  We remedy this issue by writing the length of our `ret` slice at the end, thus
+      //    overwritting those garbage bytes.
+      let shift := and(length, 31) //equivalent to `mod(length, 32)` but 2 gas cheaper
+      if iszero(shift) {
+        shift := wordSize
+      }
+
+      let dest := add(ret, shift)
+      let end := add(dest, length)
+      for {
+        let src := add(add(encoded, shift), offset)
+      } lt(dest, end) {
+        src := add(src, wordSize)
+        dest := add(dest, wordSize)
+      } {
+        mstore(dest, mload(src))
+      }
+
+      mstore(ret, length)
+      //When compiling with --via-ir then normally allocated memory (i.e. via new) will have 32 byte
+      //  memory alignment and so we enforce the same memory alignment here.
+      mstore(freeMemoryPtr, and(add(dest, 31), not(31)))
+    }
+  }
+
+  function slice(
+    bytes memory encoded,
+    uint offset,
+    uint length
+  ) internal pure returns (bytes memory ret, uint nextOffset) {
+    (ret, nextOffset) = sliceUnchecked(encoded, offset, length);
+    checkBound(nextOffset, encoded.length);
+  }
+
+  function asAddressUnchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (address, uint) {
+    (uint160 ret, uint nextOffset) = asUint160(encoded, offset);
+    return (address(ret), nextOffset);
+  }
+
+  function asAddress(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (address ret, uint nextOffset) {
+    (ret, nextOffset) = asAddressUnchecked(encoded, offset);
+    checkBound(nextOffset, encoded.length);
+  }
+
+  function asBoolUnckecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bool, uint) {
+    (uint8 ret, uint nextOffset) = asUint8(encoded, offset);
+    return (ret != 0, nextOffset);
+  }
+
+  function asBool(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bool ret, uint nextOffset) {
+    (ret, nextOffset) = asBoolUnckecked(encoded, offset);
+    checkBound(nextOffset, encoded.length);
+  }
+
+/* -------------------------------------------------------------------------------------------------
+Remaining library code below was auto-generated by via the following js/node code:
+
+for (let bytes = 1; bytes <= 32; ++bytes) {
+  const bits = bytes*8;
+  console.log(
+`function asUint${bits}Unchecked(
+  bytes memory encoded,
+  uint offset
+) internal pure returns (uint${bits} ret, uint nextOffset) {
+  assembly ("memory-safe") {
+    nextOffset := add(offset, ${bytes})
+    ret := mload(add(encoded, nextOffset))
+  }
+  return (ret, nextOffset);
+}
+
+function asUint${bits}(
+  bytes memory encoded,
+  uint offset
+) internal pure returns (uint${bits} ret, uint nextOffset) {
+  (ret, nextOffset) = asUint${bits}Unchecked(encoded, offset);
+  checkBound(nextOffset, encoded.length);
+}
+
+function asBytes${bytes}Unchecked(
+  bytes memory encoded,
+  uint offset
+) internal pure returns (bytes${bytes}, uint) {
+  (uint${bits} ret, uint nextOffset) = asUint${bits}Unchecked(encoded, offset);
+  return (bytes${bytes}(ret), nextOffset);
+}
+
+function asBytes${bytes}(
+  bytes memory encoded,
+  uint offset
+) internal pure returns (bytes${bytes}, uint) {
+  (uint${bits} ret, uint nextOffset) = asUint${bits}(encoded, offset);
+  return (bytes${bytes}(ret), nextOffset);
+}
+`
+  );
+}
+------------------------------------------------------------------------------------------------- */
+
+  function asUint8Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint8 ret, uint nextOffset) {
+    assembly ("memory-safe") {
+      nextOffset := add(offset, 1)
+      ret := mload(add(encoded, nextOffset))
+    }
+    return (ret, nextOffset);
+  }
+
+  function asUint8(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint8 ret, uint nextOffset) {
+    (ret, nextOffset) = asUint8Unchecked(encoded, offset);
+    checkBound(nextOffset, encoded.length);
+  }
+
+  function asBytes1Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes1, uint) {
+    (uint8 ret, uint nextOffset) = asUint8Unchecked(encoded, offset);
+    return (bytes1(ret), nextOffset);
+  }
+
+  function asBytes1(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes1, uint) {
+    (uint8 ret, uint nextOffset) = asUint8(encoded, offset);
+    return (bytes1(ret), nextOffset);
+  }
+
+  function asUint16Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint16 ret, uint nextOffset) {
+    assembly ("memory-safe") {
+      nextOffset := add(offset, 2)
+      ret := mload(add(encoded, nextOffset))
+    }
+    return (ret, nextOffset);
+  }
+
+  function asUint16(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint16 ret, uint nextOffset) {
+    (ret, nextOffset) = asUint16Unchecked(encoded, offset);
+    checkBound(nextOffset, encoded.length);
+  }
+
+  function asBytes2Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes2, uint) {
+    (uint16 ret, uint nextOffset) = asUint16Unchecked(encoded, offset);
+    return (bytes2(ret), nextOffset);
+  }
+
+  function asBytes2(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes2, uint) {
+    (uint16 ret, uint nextOffset) = asUint16(encoded, offset);
+    return (bytes2(ret), nextOffset);
+  }
+
+  function asUint24Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint24 ret, uint nextOffset) {
+    assembly ("memory-safe") {
+      nextOffset := add(offset, 3)
+      ret := mload(add(encoded, nextOffset))
+    }
+    return (ret, nextOffset);
+  }
+
+  function asUint24(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint24 ret, uint nextOffset) {
+    (ret, nextOffset) = asUint24Unchecked(encoded, offset);
+    checkBound(nextOffset, encoded.length);
+  }
+
+  function asBytes3Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes3, uint) {
+    (uint24 ret, uint nextOffset) = asUint24Unchecked(encoded, offset);
+    return (bytes3(ret), nextOffset);
+  }
+
+  function asBytes3(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes3, uint) {
+    (uint24 ret, uint nextOffset) = asUint24(encoded, offset);
+    return (bytes3(ret), nextOffset);
+  }
+
+  function asUint32Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint32 ret, uint nextOffset) {
+    assembly ("memory-safe") {
+      nextOffset := add(offset, 4)
+      ret := mload(add(encoded, nextOffset))
+    }
+    return (ret, nextOffset);
+  }
+
+  function asUint32(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint32 ret, uint nextOffset) {
+    (ret, nextOffset) = asUint32Unchecked(encoded, offset);
+    checkBound(nextOffset, encoded.length);
+  }
+
+  function asBytes4Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes4, uint) {
+    (uint32 ret, uint nextOffset) = asUint32Unchecked(encoded, offset);
+    return (bytes4(ret), nextOffset);
+  }
+
+  function asBytes4(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes4, uint) {
+    (uint32 ret, uint nextOffset) = asUint32(encoded, offset);
+    return (bytes4(ret), nextOffset);
+  }
+
+  function asUint40Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint40 ret, uint nextOffset) {
+    assembly ("memory-safe") {
+      nextOffset := add(offset, 5)
+      ret := mload(add(encoded, nextOffset))
+    }
+    return (ret, nextOffset);
+  }
+
+  function asUint40(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint40 ret, uint nextOffset) {
+    (ret, nextOffset) = asUint40Unchecked(encoded, offset);
+    checkBound(nextOffset, encoded.length);
+  }
+
+  function asBytes5Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes5, uint) {
+    (uint40 ret, uint nextOffset) = asUint40Unchecked(encoded, offset);
+    return (bytes5(ret), nextOffset);
+  }
+
+  function asBytes5(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes5, uint) {
+    (uint40 ret, uint nextOffset) = asUint40(encoded, offset);
+    return (bytes5(ret), nextOffset);
+  }
+
+  function asUint48Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint48 ret, uint nextOffset) {
+    assembly ("memory-safe") {
+      nextOffset := add(offset, 6)
+      ret := mload(add(encoded, nextOffset))
+    }
+    return (ret, nextOffset);
+  }
+
+  function asUint48(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint48 ret, uint nextOffset) {
+    (ret, nextOffset) = asUint48Unchecked(encoded, offset);
+    checkBound(nextOffset, encoded.length);
+  }
+
+  function asBytes6Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes6, uint) {
+    (uint48 ret, uint nextOffset) = asUint48Unchecked(encoded, offset);
+    return (bytes6(ret), nextOffset);
+  }
+
+  function asBytes6(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes6, uint) {
+    (uint48 ret, uint nextOffset) = asUint48(encoded, offset);
+    return (bytes6(ret), nextOffset);
+  }
+
+  function asUint56Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint56 ret, uint nextOffset) {
+    assembly ("memory-safe") {
+      nextOffset := add(offset, 7)
+      ret := mload(add(encoded, nextOffset))
+    }
+    return (ret, nextOffset);
+  }
+
+  function asUint56(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint56 ret, uint nextOffset) {
+    (ret, nextOffset) = asUint56Unchecked(encoded, offset);
+    checkBound(nextOffset, encoded.length);
+  }
+
+  function asBytes7Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes7, uint) {
+    (uint56 ret, uint nextOffset) = asUint56Unchecked(encoded, offset);
+    return (bytes7(ret), nextOffset);
+  }
+
+  function asBytes7(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes7, uint) {
+    (uint56 ret, uint nextOffset) = asUint56(encoded, offset);
+    return (bytes7(ret), nextOffset);
+  }
+
+  function asUint64Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint64 ret, uint nextOffset) {
+    assembly ("memory-safe") {
+      nextOffset := add(offset, 8)
+      ret := mload(add(encoded, nextOffset))
+    }
+    return (ret, nextOffset);
+  }
+
+  function asUint64(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint64 ret, uint nextOffset) {
+    (ret, nextOffset) = asUint64Unchecked(encoded, offset);
+    checkBound(nextOffset, encoded.length);
+  }
+
+  function asBytes8Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes8, uint) {
+    (uint64 ret, uint nextOffset) = asUint64Unchecked(encoded, offset);
+    return (bytes8(ret), nextOffset);
+  }
+
+  function asBytes8(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes8, uint) {
+    (uint64 ret, uint nextOffset) = asUint64(encoded, offset);
+    return (bytes8(ret), nextOffset);
+  }
+
+  function asUint72Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint72 ret, uint nextOffset) {
+    assembly ("memory-safe") {
+      nextOffset := add(offset, 9)
+      ret := mload(add(encoded, nextOffset))
+    }
+    return (ret, nextOffset);
+  }
+
+  function asUint72(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint72 ret, uint nextOffset) {
+    (ret, nextOffset) = asUint72Unchecked(encoded, offset);
+    checkBound(nextOffset, encoded.length);
+  }
+
+  function asBytes9Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes9, uint) {
+    (uint72 ret, uint nextOffset) = asUint72Unchecked(encoded, offset);
+    return (bytes9(ret), nextOffset);
+  }
+
+  function asBytes9(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes9, uint) {
+    (uint72 ret, uint nextOffset) = asUint72(encoded, offset);
+    return (bytes9(ret), nextOffset);
+  }
+
+  function asUint80Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint80 ret, uint nextOffset) {
+    assembly ("memory-safe") {
+      nextOffset := add(offset, 10)
+      ret := mload(add(encoded, nextOffset))
+    }
+    return (ret, nextOffset);
+  }
+
+  function asUint80(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint80 ret, uint nextOffset) {
+    (ret, nextOffset) = asUint80Unchecked(encoded, offset);
+    checkBound(nextOffset, encoded.length);
+  }
+
+  function asBytes10Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes10, uint) {
+    (uint80 ret, uint nextOffset) = asUint80Unchecked(encoded, offset);
+    return (bytes10(ret), nextOffset);
+  }
+
+  function asBytes10(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes10, uint) {
+    (uint80 ret, uint nextOffset) = asUint80(encoded, offset);
+    return (bytes10(ret), nextOffset);
+  }
+
+  function asUint88Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint88 ret, uint nextOffset) {
+    assembly ("memory-safe") {
+      nextOffset := add(offset, 11)
+      ret := mload(add(encoded, nextOffset))
+    }
+    return (ret, nextOffset);
+  }
+
+  function asUint88(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint88 ret, uint nextOffset) {
+    (ret, nextOffset) = asUint88Unchecked(encoded, offset);
+    checkBound(nextOffset, encoded.length);
+  }
+
+  function asBytes11Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes11, uint) {
+    (uint88 ret, uint nextOffset) = asUint88Unchecked(encoded, offset);
+    return (bytes11(ret), nextOffset);
+  }
+
+  function asBytes11(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes11, uint) {
+    (uint88 ret, uint nextOffset) = asUint88(encoded, offset);
+    return (bytes11(ret), nextOffset);
+  }
+
+  function asUint96Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint96 ret, uint nextOffset) {
+    assembly ("memory-safe") {
+      nextOffset := add(offset, 12)
+      ret := mload(add(encoded, nextOffset))
+    }
+    return (ret, nextOffset);
+  }
+
+  function asUint96(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint96 ret, uint nextOffset) {
+    (ret, nextOffset) = asUint96Unchecked(encoded, offset);
+    checkBound(nextOffset, encoded.length);
+  }
+
+  function asBytes12Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes12, uint) {
+    (uint96 ret, uint nextOffset) = asUint96Unchecked(encoded, offset);
+    return (bytes12(ret), nextOffset);
+  }
+
+  function asBytes12(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes12, uint) {
+    (uint96 ret, uint nextOffset) = asUint96(encoded, offset);
+    return (bytes12(ret), nextOffset);
+  }
+
+  function asUint104Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint104 ret, uint nextOffset) {
+    assembly ("memory-safe") {
+      nextOffset := add(offset, 13)
+      ret := mload(add(encoded, nextOffset))
+    }
+    return (ret, nextOffset);
+  }
+
+  function asUint104(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint104 ret, uint nextOffset) {
+    (ret, nextOffset) = asUint104Unchecked(encoded, offset);
+    checkBound(nextOffset, encoded.length);
+  }
+
+  function asBytes13Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes13, uint) {
+    (uint104 ret, uint nextOffset) = asUint104Unchecked(encoded, offset);
+    return (bytes13(ret), nextOffset);
+  }
+
+  function asBytes13(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes13, uint) {
+    (uint104 ret, uint nextOffset) = asUint104(encoded, offset);
+    return (bytes13(ret), nextOffset);
+  }
+
+  function asUint112Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint112 ret, uint nextOffset) {
+    assembly ("memory-safe") {
+      nextOffset := add(offset, 14)
+      ret := mload(add(encoded, nextOffset))
+    }
+    return (ret, nextOffset);
+  }
+
+  function asUint112(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint112 ret, uint nextOffset) {
+    (ret, nextOffset) = asUint112Unchecked(encoded, offset);
+    checkBound(nextOffset, encoded.length);
+  }
+
+  function asBytes14Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes14, uint) {
+    (uint112 ret, uint nextOffset) = asUint112Unchecked(encoded, offset);
+    return (bytes14(ret), nextOffset);
+  }
+
+  function asBytes14(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes14, uint) {
+    (uint112 ret, uint nextOffset) = asUint112(encoded, offset);
+    return (bytes14(ret), nextOffset);
+  }
+
+  function asUint120Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint120 ret, uint nextOffset) {
+    assembly ("memory-safe") {
+      nextOffset := add(offset, 15)
+      ret := mload(add(encoded, nextOffset))
+    }
+    return (ret, nextOffset);
+  }
+
+  function asUint120(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint120 ret, uint nextOffset) {
+    (ret, nextOffset) = asUint120Unchecked(encoded, offset);
+    checkBound(nextOffset, encoded.length);
+  }
+
+  function asBytes15Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes15, uint) {
+    (uint120 ret, uint nextOffset) = asUint120Unchecked(encoded, offset);
+    return (bytes15(ret), nextOffset);
+  }
+
+  function asBytes15(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes15, uint) {
+    (uint120 ret, uint nextOffset) = asUint120(encoded, offset);
+    return (bytes15(ret), nextOffset);
+  }
+
+  function asUint128Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint128 ret, uint nextOffset) {
+    assembly ("memory-safe") {
+      nextOffset := add(offset, 16)
+      ret := mload(add(encoded, nextOffset))
+    }
+    return (ret, nextOffset);
+  }
+
+  function asUint128(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint128 ret, uint nextOffset) {
+    (ret, nextOffset) = asUint128Unchecked(encoded, offset);
+    checkBound(nextOffset, encoded.length);
+  }
+
+  function asBytes16Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes16, uint) {
+    (uint128 ret, uint nextOffset) = asUint128Unchecked(encoded, offset);
+    return (bytes16(ret), nextOffset);
+  }
+
+  function asBytes16(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes16, uint) {
+    (uint128 ret, uint nextOffset) = asUint128(encoded, offset);
+    return (bytes16(ret), nextOffset);
+  }
+
+  function asUint136Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint136 ret, uint nextOffset) {
+    assembly ("memory-safe") {
+      nextOffset := add(offset, 17)
+      ret := mload(add(encoded, nextOffset))
+    }
+    return (ret, nextOffset);
+  }
+
+  function asUint136(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint136 ret, uint nextOffset) {
+    (ret, nextOffset) = asUint136Unchecked(encoded, offset);
+    checkBound(nextOffset, encoded.length);
+  }
+
+  function asBytes17Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes17, uint) {
+    (uint136 ret, uint nextOffset) = asUint136Unchecked(encoded, offset);
+    return (bytes17(ret), nextOffset);
+  }
+
+  function asBytes17(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes17, uint) {
+    (uint136 ret, uint nextOffset) = asUint136(encoded, offset);
+    return (bytes17(ret), nextOffset);
+  }
+
+  function asUint144Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint144 ret, uint nextOffset) {
+    assembly ("memory-safe") {
+      nextOffset := add(offset, 18)
+      ret := mload(add(encoded, nextOffset))
+    }
+    return (ret, nextOffset);
+  }
+
+  function asUint144(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint144 ret, uint nextOffset) {
+    (ret, nextOffset) = asUint144Unchecked(encoded, offset);
+    checkBound(nextOffset, encoded.length);
+  }
+
+  function asBytes18Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes18, uint) {
+    (uint144 ret, uint nextOffset) = asUint144Unchecked(encoded, offset);
+    return (bytes18(ret), nextOffset);
+  }
+
+  function asBytes18(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes18, uint) {
+    (uint144 ret, uint nextOffset) = asUint144(encoded, offset);
+    return (bytes18(ret), nextOffset);
+  }
+
+  function asUint152Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint152 ret, uint nextOffset) {
+    assembly ("memory-safe") {
+      nextOffset := add(offset, 19)
+      ret := mload(add(encoded, nextOffset))
+    }
+    return (ret, nextOffset);
+  }
+
+  function asUint152(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint152 ret, uint nextOffset) {
+    (ret, nextOffset) = asUint152Unchecked(encoded, offset);
+    checkBound(nextOffset, encoded.length);
+  }
+
+  function asBytes19Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes19, uint) {
+    (uint152 ret, uint nextOffset) = asUint152Unchecked(encoded, offset);
+    return (bytes19(ret), nextOffset);
+  }
+
+  function asBytes19(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes19, uint) {
+    (uint152 ret, uint nextOffset) = asUint152(encoded, offset);
+    return (bytes19(ret), nextOffset);
+  }
+
+  function asUint160Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint160 ret, uint nextOffset) {
+    assembly ("memory-safe") {
+      nextOffset := add(offset, 20)
+      ret := mload(add(encoded, nextOffset))
+    }
+    return (ret, nextOffset);
+  }
+
+  function asUint160(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint160 ret, uint nextOffset) {
+    (ret, nextOffset) = asUint160Unchecked(encoded, offset);
+    checkBound(nextOffset, encoded.length);
+  }
+
+  function asBytes20Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes20, uint) {
+    (uint160 ret, uint nextOffset) = asUint160Unchecked(encoded, offset);
+    return (bytes20(ret), nextOffset);
+  }
+
+  function asBytes20(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes20, uint) {
+    (uint160 ret, uint nextOffset) = asUint160(encoded, offset);
+    return (bytes20(ret), nextOffset);
+  }
+
+  function asUint168Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint168 ret, uint nextOffset) {
+    assembly ("memory-safe") {
+      nextOffset := add(offset, 21)
+      ret := mload(add(encoded, nextOffset))
+    }
+    return (ret, nextOffset);
+  }
+
+  function asUint168(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint168 ret, uint nextOffset) {
+    (ret, nextOffset) = asUint168Unchecked(encoded, offset);
+    checkBound(nextOffset, encoded.length);
+  }
+
+  function asBytes21Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes21, uint) {
+    (uint168 ret, uint nextOffset) = asUint168Unchecked(encoded, offset);
+    return (bytes21(ret), nextOffset);
+  }
+
+  function asBytes21(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes21, uint) {
+    (uint168 ret, uint nextOffset) = asUint168(encoded, offset);
+    return (bytes21(ret), nextOffset);
+  }
+
+  function asUint176Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint176 ret, uint nextOffset) {
+    assembly ("memory-safe") {
+      nextOffset := add(offset, 22)
+      ret := mload(add(encoded, nextOffset))
+    }
+    return (ret, nextOffset);
+  }
+
+  function asUint176(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint176 ret, uint nextOffset) {
+    (ret, nextOffset) = asUint176Unchecked(encoded, offset);
+    checkBound(nextOffset, encoded.length);
+  }
+
+  function asBytes22Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes22, uint) {
+    (uint176 ret, uint nextOffset) = asUint176Unchecked(encoded, offset);
+    return (bytes22(ret), nextOffset);
+  }
+
+  function asBytes22(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes22, uint) {
+    (uint176 ret, uint nextOffset) = asUint176(encoded, offset);
+    return (bytes22(ret), nextOffset);
+  }
+
+  function asUint184Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint184 ret, uint nextOffset) {
+    assembly ("memory-safe") {
+      nextOffset := add(offset, 23)
+      ret := mload(add(encoded, nextOffset))
+    }
+    return (ret, nextOffset);
+  }
+
+  function asUint184(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint184 ret, uint nextOffset) {
+    (ret, nextOffset) = asUint184Unchecked(encoded, offset);
+    checkBound(nextOffset, encoded.length);
+  }
+
+  function asBytes23Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes23, uint) {
+    (uint184 ret, uint nextOffset) = asUint184Unchecked(encoded, offset);
+    return (bytes23(ret), nextOffset);
+  }
+
+  function asBytes23(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes23, uint) {
+    (uint184 ret, uint nextOffset) = asUint184(encoded, offset);
+    return (bytes23(ret), nextOffset);
+  }
+
+  function asUint192Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint192 ret, uint nextOffset) {
+    assembly ("memory-safe") {
+      nextOffset := add(offset, 24)
+      ret := mload(add(encoded, nextOffset))
+    }
+    return (ret, nextOffset);
+  }
+
+  function asUint192(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint192 ret, uint nextOffset) {
+    (ret, nextOffset) = asUint192Unchecked(encoded, offset);
+    checkBound(nextOffset, encoded.length);
+  }
+
+  function asBytes24Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes24, uint) {
+    (uint192 ret, uint nextOffset) = asUint192Unchecked(encoded, offset);
+    return (bytes24(ret), nextOffset);
+  }
+
+  function asBytes24(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes24, uint) {
+    (uint192 ret, uint nextOffset) = asUint192(encoded, offset);
+    return (bytes24(ret), nextOffset);
+  }
+
+  function asUint200Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint200 ret, uint nextOffset) {
+    assembly ("memory-safe") {
+      nextOffset := add(offset, 25)
+      ret := mload(add(encoded, nextOffset))
+    }
+    return (ret, nextOffset);
+  }
+
+  function asUint200(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint200 ret, uint nextOffset) {
+    (ret, nextOffset) = asUint200Unchecked(encoded, offset);
+    checkBound(nextOffset, encoded.length);
+  }
+
+  function asBytes25Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes25, uint) {
+    (uint200 ret, uint nextOffset) = asUint200Unchecked(encoded, offset);
+    return (bytes25(ret), nextOffset);
+  }
+
+  function asBytes25(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes25, uint) {
+    (uint200 ret, uint nextOffset) = asUint200(encoded, offset);
+    return (bytes25(ret), nextOffset);
+  }
+
+  function asUint208Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint208 ret, uint nextOffset) {
+    assembly ("memory-safe") {
+      nextOffset := add(offset, 26)
+      ret := mload(add(encoded, nextOffset))
+    }
+    return (ret, nextOffset);
+  }
+
+  function asUint208(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint208 ret, uint nextOffset) {
+    (ret, nextOffset) = asUint208Unchecked(encoded, offset);
+    checkBound(nextOffset, encoded.length);
+  }
+
+  function asBytes26Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes26, uint) {
+    (uint208 ret, uint nextOffset) = asUint208Unchecked(encoded, offset);
+    return (bytes26(ret), nextOffset);
+  }
+
+  function asBytes26(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes26, uint) {
+    (uint208 ret, uint nextOffset) = asUint208(encoded, offset);
+    return (bytes26(ret), nextOffset);
+  }
+
+  function asUint216Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint216 ret, uint nextOffset) {
+    assembly ("memory-safe") {
+      nextOffset := add(offset, 27)
+      ret := mload(add(encoded, nextOffset))
+    }
+    return (ret, nextOffset);
+  }
+
+  function asUint216(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint216 ret, uint nextOffset) {
+    (ret, nextOffset) = asUint216Unchecked(encoded, offset);
+    checkBound(nextOffset, encoded.length);
+  }
+
+  function asBytes27Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes27, uint) {
+    (uint216 ret, uint nextOffset) = asUint216Unchecked(encoded, offset);
+    return (bytes27(ret), nextOffset);
+  }
+
+  function asBytes27(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes27, uint) {
+    (uint216 ret, uint nextOffset) = asUint216(encoded, offset);
+    return (bytes27(ret), nextOffset);
+  }
+
+  function asUint224Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint224 ret, uint nextOffset) {
+    assembly ("memory-safe") {
+      nextOffset := add(offset, 28)
+      ret := mload(add(encoded, nextOffset))
+    }
+    return (ret, nextOffset);
+  }
+
+  function asUint224(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint224 ret, uint nextOffset) {
+    (ret, nextOffset) = asUint224Unchecked(encoded, offset);
+    checkBound(nextOffset, encoded.length);
+  }
+
+  function asBytes28Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes28, uint) {
+    (uint224 ret, uint nextOffset) = asUint224Unchecked(encoded, offset);
+    return (bytes28(ret), nextOffset);
+  }
+
+  function asBytes28(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes28, uint) {
+    (uint224 ret, uint nextOffset) = asUint224(encoded, offset);
+    return (bytes28(ret), nextOffset);
+  }
+
+  function asUint232Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint232 ret, uint nextOffset) {
+    assembly ("memory-safe") {
+      nextOffset := add(offset, 29)
+      ret := mload(add(encoded, nextOffset))
+    }
+    return (ret, nextOffset);
+  }
+
+  function asUint232(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint232 ret, uint nextOffset) {
+    (ret, nextOffset) = asUint232Unchecked(encoded, offset);
+    checkBound(nextOffset, encoded.length);
+  }
+
+  function asBytes29Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes29, uint) {
+    (uint232 ret, uint nextOffset) = asUint232Unchecked(encoded, offset);
+    return (bytes29(ret), nextOffset);
+  }
+
+  function asBytes29(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes29, uint) {
+    (uint232 ret, uint nextOffset) = asUint232(encoded, offset);
+    return (bytes29(ret), nextOffset);
+  }
+
+  function asUint240Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint240 ret, uint nextOffset) {
+    assembly ("memory-safe") {
+      nextOffset := add(offset, 30)
+      ret := mload(add(encoded, nextOffset))
+    }
+    return (ret, nextOffset);
+  }
+
+  function asUint240(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint240 ret, uint nextOffset) {
+    (ret, nextOffset) = asUint240Unchecked(encoded, offset);
+    checkBound(nextOffset, encoded.length);
+  }
+
+  function asBytes30Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes30, uint) {
+    (uint240 ret, uint nextOffset) = asUint240Unchecked(encoded, offset);
+    return (bytes30(ret), nextOffset);
+  }
+
+  function asBytes30(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes30, uint) {
+    (uint240 ret, uint nextOffset) = asUint240(encoded, offset);
+    return (bytes30(ret), nextOffset);
+  }
+
+  function asUint248Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint248 ret, uint nextOffset) {
+    assembly ("memory-safe") {
+      nextOffset := add(offset, 31)
+      ret := mload(add(encoded, nextOffset))
+    }
+    return (ret, nextOffset);
+  }
+
+  function asUint248(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint248 ret, uint nextOffset) {
+    (ret, nextOffset) = asUint248Unchecked(encoded, offset);
+    checkBound(nextOffset, encoded.length);
+  }
+
+  function asBytes31Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes31, uint) {
+    (uint248 ret, uint nextOffset) = asUint248Unchecked(encoded, offset);
+    return (bytes31(ret), nextOffset);
+  }
+
+  function asBytes31(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes31, uint) {
+    (uint248 ret, uint nextOffset) = asUint248(encoded, offset);
+    return (bytes31(ret), nextOffset);
+  }
+
+  function asUint256Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint256 ret, uint nextOffset) {
+    assembly ("memory-safe") {
+      nextOffset := add(offset, 32)
+      ret := mload(add(encoded, nextOffset))
+    }
+    return (ret, nextOffset);
+  }
+
+  function asUint256(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (uint256 ret, uint nextOffset) {
+    (ret, nextOffset) = asUint256Unchecked(encoded, offset);
+    checkBound(nextOffset, encoded.length);
+  }
+
+  function asBytes32Unchecked(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes32, uint) {
+    (uint256 ret, uint nextOffset) = asUint256Unchecked(encoded, offset);
+    return (bytes32(ret), nextOffset);
+  }
+
+  function asBytes32(
+    bytes memory encoded,
+    uint offset
+  ) internal pure returns (bytes32, uint) {
+    (uint256 ret, uint nextOffset) = asUint256(encoded, offset);
+    return (bytes32(ret), nextOffset);
+  }
+}

+ 94 - 0
ethereum/contracts/libraries/relayer/ExecutionParameters.sol

@@ -0,0 +1,94 @@
+// SPDX-License-Identifier: Apache 2
+
+pragma solidity ^0.8.19;
+
+import "../../interfaces/relayer/TypedUnits.sol";
+import {BytesParsing} from "../../libraries/relayer/BytesParsing.sol";
+
+error UnexpectedExecutionParamsVersion(uint8 version, uint8 expectedVersion);
+error UnsupportedExecutionParamsVersion(uint8 version);
+error TargetChainAndExecutionParamsVersionMismatch(uint16 targetChain, uint8 version);
+error UnexpectedExecutionInfoVersion(uint8 version, uint8 expectedVersion);
+error UnsupportedExecutionInfoVersion(uint8 version);
+error TargetChainAndExecutionInfoVersionMismatch(uint16 targetChain, uint8 version);
+error VersionMismatchOverride(uint8 instructionVersion, uint8 overrideVersion);
+
+using BytesParsing for bytes;
+
+enum ExecutionParamsVersion {EVM_V1}
+
+struct EvmExecutionParamsV1 {
+    Gas gasLimit;
+}
+
+enum ExecutionInfoVersion {EVM_V1}
+
+struct EvmExecutionInfoV1 {
+    Gas gasLimit;
+    GasPrice targetChainRefundPerGasUnused;
+}
+
+function decodeExecutionParamsVersion(bytes memory data)
+    pure
+    returns (ExecutionParamsVersion version)
+{
+    (version) = abi.decode(data, (ExecutionParamsVersion));
+}
+
+function decodeExecutionInfoVersion(bytes memory data)
+    pure
+    returns (ExecutionInfoVersion version)
+{
+    (version) = abi.decode(data, (ExecutionInfoVersion));
+}
+
+function encodeEvmExecutionParamsV1(EvmExecutionParamsV1 memory executionParams)
+    pure
+    returns (bytes memory)
+{
+    return abi.encode(uint8(ExecutionParamsVersion.EVM_V1), executionParams.gasLimit);
+}
+
+function decodeEvmExecutionParamsV1(bytes memory data)
+    pure
+    returns (EvmExecutionParamsV1 memory executionParams)
+{
+    uint8 version;
+    (version, executionParams.gasLimit) = abi.decode(data, (uint8, Gas));
+
+    if (version != uint8(ExecutionParamsVersion.EVM_V1)) {
+        revert UnexpectedExecutionParamsVersion(version, uint8(ExecutionParamsVersion.EVM_V1));
+    }
+}
+
+function encodeEvmExecutionInfoV1(EvmExecutionInfoV1 memory executionInfo)
+    pure
+    returns (bytes memory)
+{
+    return abi.encode(
+        uint8(ExecutionInfoVersion.EVM_V1),
+        executionInfo.gasLimit,
+        executionInfo.targetChainRefundPerGasUnused
+    );
+}
+
+function decodeEvmExecutionInfoV1(bytes memory data)
+    pure
+    returns (EvmExecutionInfoV1 memory executionInfo)
+{
+    uint8 version;
+    (version, executionInfo.gasLimit, executionInfo.targetChainRefundPerGasUnused) =
+        abi.decode(data, (uint8, Gas, GasPrice));
+
+    if (version != uint8(ExecutionInfoVersion.EVM_V1)) {
+        revert UnexpectedExecutionInfoVersion(version, uint8(ExecutionInfoVersion.EVM_V1));
+    }
+}
+
+function getEmptyEvmExecutionParamsV1()
+    pure
+    returns (EvmExecutionParamsV1 memory executionParams)
+{
+    executionParams.gasLimit = Gas.wrap(uint256(0));
+}
+

+ 66 - 0
ethereum/contracts/libraries/relayer/RelayerInternalStructs.sol

@@ -0,0 +1,66 @@
+// SPDX-License-Identifier: Apache 2
+
+pragma solidity ^0.8.19;
+
+import "../../interfaces/relayer/TypedUnits.sol";
+import "../../interfaces/relayer/IWormholeRelayerTyped.sol";
+
+struct DeliveryInstruction {
+    uint16 targetChain;
+    bytes32 targetAddress;
+    bytes payload;
+    TargetNative requestedReceiverValue;
+    TargetNative extraReceiverValue;
+    bytes encodedExecutionInfo;
+    uint16 refundChain;
+    bytes32 refundAddress;
+    bytes32 refundDeliveryProvider;
+    bytes32 sourceDeliveryProvider;
+    bytes32 senderAddress;
+    VaaKey[] vaaKeys;
+}
+
+// Meant to hold all necessary values for `CoreRelayerDelivery::executeInstruction`
+// Nothing more and nothing less.
+struct EvmDeliveryInstruction {
+  uint16 sourceChain;
+  bytes32 targetAddress;
+  bytes payload;
+  Gas gasLimit;
+  TargetNative totalReceiverValue;
+  GasPrice targetChainRefundPerGasUnused;
+  bytes32 senderAddress;
+  bytes32 deliveryHash;
+  bytes[] signedVaas;
+}
+
+struct RedeliveryInstruction {
+    VaaKey deliveryVaaKey;
+    uint16 targetChain;
+    TargetNative newRequestedReceiverValue;
+    bytes newEncodedExecutionInfo;
+    bytes32 newSourceDeliveryProvider;
+    bytes32 newSenderAddress;
+}
+
+/**
+ * @notice When a user requests a `resend()`, a `RedeliveryInstruction` is emitted by the
+ *     WormholeRelayer and in turn converted by the relay provider into an encoded (=serialized)
+ *     `DeliveryOverride` struct which is then passed to `delivery()` to override the parameters of
+ *     a previously failed delivery attempt.
+ *
+ * @custom:member newReceiverValue - must >= than the `receiverValue` specified in the original
+ *     `DeliveryInstruction`
+ * @custom:member newExecutionInfo - for EVM_V1, must contain a gasLimit and targetChainRefundPerGasUnused
+ * such that 
+ * - gasLimit is >= the `gasLimit` specified in the `executionParameters`
+ *     of the original `DeliveryInstruction`
+ * - targetChainRefundPerGasUnused is >=  the `targetChainRefundPerGasUnused` specified in the original
+ *     `DeliveryInstruction`
+ * @custom:member redeliveryHash - the hash of the redelivery which is being performed
+ */
+struct DeliveryOverride {
+    TargetNative newReceiverValue;
+    bytes newExecutionInfo;
+    bytes32 redeliveryHash;
+}

+ 94 - 0
ethereum/contracts/libraries/relayer/Utils.sol

@@ -0,0 +1,94 @@
+// SPDX-License-Identifier: Apache 2
+
+pragma solidity ^0.8.0;
+
+import "../../interfaces/relayer/TypedUnits.sol";
+
+error NotAnEvmAddress(bytes32);
+
+function pay(address payable receiver, LocalNative amount) returns (bool success) {
+  uint256 amount_ = LocalNative.unwrap(amount);
+  if (amount_ != 0)
+    // TODO: we currently ignore the return data. Some users of this function might want to bubble up the return value though.
+    // Specifying a higher limit than 63/64 of the remaining gas caps it at that amount without throwing an exception.
+    (success,) = returnLengthBoundedCall(receiver, new bytes(0), gasleft(), amount_, 0);
+  else
+    success = true;
+}
+
+function min(uint256 a, uint256 b) pure returns (uint256) {
+  return a < b ? a : b;
+}
+
+function min(uint64 a, uint64 b) pure returns (uint64) {
+  return a < b ? a : b;
+}
+
+function max(uint256 a, uint256 b) pure returns (uint256) {
+  return a > b ? a : b;
+}
+
+function toWormholeFormat(address addr) pure returns (bytes32) {
+  return bytes32(uint256(uint160(addr)));
+}
+
+function fromWormholeFormat(bytes32 whFormatAddress) pure returns (address) {
+  if (uint256(whFormatAddress) >> 160 != 0)
+    revert NotAnEvmAddress(whFormatAddress);
+  return address(uint160(uint256(whFormatAddress)));
+}
+
+function fromWormholeFormatUnchecked(bytes32 whFormatAddress) pure returns (address) {
+  return address(uint160(uint256(whFormatAddress)));
+}
+
+
+uint256 constant freeMemoryPtr = 0x40;
+uint256 constant memoryWord = 32;
+uint256 constant maskModulo32 = 0x1f;
+
+/**
+ * Implements call that truncates return data to a specific size to avoid excessive gas consumption for relayers
+ * when a revert or unexpectedly large return value is produced by the call.
+ *
+ * @param returnedData Buffer of returned data truncated to the first `dataLengthBound` bytes.
+ */
+function returnLengthBoundedCall(
+  address payable callee,
+  bytes memory callData,
+  uint256 gasLimit,
+  uint256 value,
+  uint256 dataLengthBound
+) returns (bool success, bytes memory returnedData) {
+  uint256 callDataLength = callData.length;
+  assembly ("memory-safe") {
+    returnedData := mload(freeMemoryPtr)
+    let returnedDataBuffer := add(returnedData, memoryWord)
+    let callDataBuffer := add(callData, memoryWord)
+
+    success := call(gasLimit, callee, value, callDataBuffer, callDataLength, returnedDataBuffer, dataLengthBound)
+    let returnedDataSize := returndatasize()
+    switch lt(dataLengthBound, returnedDataSize)
+    case 1 {
+      returnedDataSize := dataLengthBound
+    } default {}
+    mstore(returnedData, returnedDataSize)
+
+    // Here we update the free memory pointer.
+    // We want to pad `returnedData` to memory word size, i.e. 32 bytes.
+    // Note that negating bitwise `maskModulo32` produces a mask that aligns addressing to 32 bytes.
+    // This allows us to pad the entire `bytes` structure (length + buffer) to 32 bytes at the end.
+    // We add `maskModulo32` to get the next free memory "slot" in case the `returnedDataSize` is not a multiple of the memory word size.
+    //
+    // Rationale:
+    // We do not care about the alignment of the free memory pointer. The solidity compiler documentation does not promise nor require alignment on it.
+    // It does however lightly suggest to pad `bytes` structures to 32 bytes: https://docs.soliditylang.org/en/v0.8.20/assembly.html#example
+    // Searching for "alignment" and "padding" in https://gitter.im/ethereum/solidity-dev
+    // yielded the following at the time of writing – paraphrased:
+    // > It's possible that the compiler cleans that padding in some cases. Users should not rely on the compiler never doing that.
+    // This means that we want to ensure that the free memory pointer points to memory just after this padding for our `returnedData` `bytes` structure.
+    let paddedPastTheEndOffset := and(add(returnedDataSize, maskModulo32), not(maskModulo32))
+    let newFreeMemoryPtr := add(returnedDataBuffer, paddedPastTheEndOffset)
+    mstore(freeMemoryPtr, newFreeMemoryPtr)
+  }
+}

+ 358 - 0
ethereum/contracts/mock/relayer/MockRelayerIntegration.sol

@@ -0,0 +1,358 @@
+// contracts/mock/relayer/MockRelayerIntegration.sol
+// SPDX-License-Identifier: Apache 2
+
+pragma solidity ^0.8.0;
+
+import "../../libraries/external/BytesLib.sol";
+import "../../interfaces/IWormhole.sol";
+import "../../interfaces/relayer/IWormholeRelayerTyped.sol";
+import "../../interfaces/relayer/IWormholeReceiver.sol";
+
+import {toWormholeFormat} from "../../libraries/relayer/Utils.sol";
+
+struct XAddress {
+    uint16 chainId;
+    bytes32 addr;
+}
+
+struct DeliveryData {
+    bytes32 sourceAddress;
+    uint16 sourceChain;
+    bytes32 deliveryHash;
+    bytes payload;
+    bytes[] additionalVaas;
+}
+
+contract MockRelayerIntegration is IWormholeReceiver {
+    using BytesLib for bytes;
+
+    // wormhole instance on this chain
+    IWormhole immutable wormhole;
+
+    // trusted relayer contract on this chain
+    IWormholeRelayer immutable relayer;
+
+    // deployer of this contract
+    address immutable owner;
+
+    // latest delivery data
+    DeliveryData latestDeliveryData;
+
+    // mapping of other MockRelayerIntegration contracts
+    mapping(uint16 => bytes32) registeredContracts;
+
+    bytes[] messageHistory;
+
+    enum Version {
+        SEND,
+        SEND_WITH_ADDITIONAL_VAA,
+        FORWARD,
+        MULTIFORWARD
+    }
+
+    struct Message {
+        Version version;
+        bytes message;
+        bytes forwardMessage;
+    }
+
+    constructor(address _wormholeCore, address _coreRelayer) {
+        wormhole = IWormhole(_wormholeCore);
+        relayer = IWormholeRelayer(_coreRelayer);
+        owner = msg.sender;
+    }
+
+    function sendMessage(
+        bytes memory _message,
+        uint16 targetChain,
+        uint32 gasLimit,
+        uint128 receiverValue
+    ) public payable returns (uint64 sequence) {
+        bytes memory fullMessage = encodeMessage(Message(Version.SEND, _message, bytes("")));
+        return sendToEvm(
+            targetChain,
+            getRegisteredContractAddress(targetChain),
+            gasLimit,
+            targetChain,
+            getRegisteredContractAddress(targetChain),
+            receiverValue,
+            0,
+            fullMessage,
+            new VaaKey[](0)
+        );
+    }
+
+    function sendMessageWithAdditionalVaas(
+        bytes memory _message,
+        uint16 targetChain,
+        uint32 gasLimit,
+        uint128 receiverValue,
+        VaaKey[] memory vaaKeys
+    ) public payable returns (uint64 sequence) {
+        bytes memory fullMessage = encodeMessage(Message(Version.SEND, _message, bytes("")));
+        return sendToEvm(
+            targetChain,
+            getRegisteredContractAddress(targetChain),
+            gasLimit,
+            targetChain,
+            getRegisteredContractAddress(targetChain),
+            receiverValue,
+            0,
+            fullMessage,
+            vaaKeys
+        );
+    }
+
+    function sendMessageWithRefund(
+        bytes memory _message,
+        uint16 targetChain,
+        uint32 gasLimit,
+        uint128 receiverValue,
+        uint16 refundChain,
+        address refundAddress
+    ) public payable returns (uint64 sequence) {
+        bytes memory fullMessage = encodeMessage(Message(Version.SEND, _message, bytes("")));
+        return sendToEvm(
+            targetChain,
+            getRegisteredContractAddress(targetChain),
+            gasLimit,
+            refundChain,
+            refundAddress,
+            receiverValue,
+            0,
+            fullMessage,
+            new VaaKey[](0)
+        );
+    }
+
+    function sendMessageWithForwardedResponse(
+        bytes memory _message,
+        bytes memory _forwardedMessage,
+        uint16 targetChain,
+        uint32 gasLimit,
+        uint128 receiverValue
+    ) public payable returns (uint64 sequence) {
+        bytes memory fullMessage =
+            encodeMessage(Message(Version.FORWARD, _message, _forwardedMessage));
+        return sendToEvm(
+            targetChain,
+            getRegisteredContractAddress(targetChain),
+            gasLimit,
+            targetChain,
+            getRegisteredContractAddress(targetChain),
+            receiverValue,
+            0,
+            fullMessage,
+            new VaaKey[](0)
+        );
+    }
+
+    function sendMessageWithForwardedResponse(
+        bytes memory _message,
+        bytes memory _forwardedMessage,
+        uint16 targetChain,
+        uint32 gasLimit,
+        uint128 receiverValue,
+        uint16 refundChain,
+        address refundAddress
+    ) public payable returns (uint64 sequence) {
+        bytes memory fullMessage =
+            encodeMessage(Message(Version.FORWARD, _message, _forwardedMessage));
+        return sendToEvm(
+            targetChain,
+            getRegisteredContractAddress(targetChain),
+            gasLimit,
+            refundChain,
+            refundAddress,
+            receiverValue,
+            0,
+            fullMessage,
+            new VaaKey[](0)
+        );
+    }
+
+    function sendMessageWithMultiForwardedResponse(
+        bytes memory _message,
+        bytes memory _forwardedMessage,
+        uint16 targetChain,
+        uint32 gasLimit,
+        uint128 receiverValue
+    ) public payable returns (uint64 sequence) {
+        bytes memory fullMessage =
+            encodeMessage(Message(Version.MULTIFORWARD, _message, _forwardedMessage));
+        return sendToEvm(
+            targetChain,
+            getRegisteredContractAddress(targetChain),
+            gasLimit,
+            targetChain,
+            getRegisteredContractAddress(targetChain),
+            receiverValue,
+            0,
+            fullMessage,
+            new VaaKey[](0)
+        );
+    }
+
+    function sendToEvm(
+        uint16 targetChain,
+        address destination,
+        uint32 gasLimit,
+        uint16 refundChain,
+        address refundAddress,
+        uint128 receiverValue,
+        uint256 paymentForExtraReceiverValue,
+        bytes memory payload,
+        VaaKey[] memory vaaKeys
+    ) public payable returns (uint64 sequence) {
+        sequence = relayer.sendToEvm{value: msg.value}(
+            targetChain,
+            destination,
+            payload,
+            TargetNative.wrap(receiverValue),
+            LocalNative.wrap(paymentForExtraReceiverValue),
+            Gas.wrap(gasLimit),
+            refundChain,
+            refundAddress,
+            relayer.getDefaultDeliveryProvider(),
+            vaaKeys,
+            200
+        );
+    }
+
+    function resend(
+        uint16 chainId,
+        uint64 sequence,
+        uint16 targetChain,
+        uint32 newGasLimit,
+        uint128 newReceiverValue
+    ) public payable returns (uint64 resendSequence) {
+        VaaKey memory deliveryVaaKey = VaaKey(chainId, getRegisteredContract(chainId), sequence);
+        resendSequence = relayer.resendToEvm{value: msg.value}(
+            deliveryVaaKey,
+            targetChain,
+            TargetNative.wrap(newReceiverValue),
+            Gas.wrap(newGasLimit),
+            relayer.getDefaultDeliveryProvider()
+        );
+    }
+
+    function receiveWormholeMessages(
+        bytes memory payload,
+        bytes[] memory additionalVaas,
+        bytes32 sourceAddress,
+        uint16 sourceChain,
+        bytes32 deliveryHash
+    ) public payable override {
+        // loop through the array of wormhole observations from the batch and store each payload
+        require(msg.sender == address(relayer), "Wrong msg.sender");
+
+        latestDeliveryData =
+            DeliveryData(sourceAddress, sourceChain, deliveryHash, payload, additionalVaas);
+
+        Message memory message;
+        if (payload.length > 0) {
+            message = decodeMessage(payload);
+        } else {
+            return;
+        }
+
+        messageHistory.push(message.message);
+
+        if (message.version == Version.FORWARD || message.version == Version.MULTIFORWARD) {
+            relayer.forwardToEvm{value: msg.value}(
+                sourceChain,
+                getRegisteredContractAddress(sourceChain),
+                encodeMessage(Message(Version.SEND, message.forwardMessage, bytes(""))),
+                TargetNative.wrap(0),
+                LocalNative.wrap(0),
+                Gas.wrap(500000),
+                sourceChain,
+                getRegisteredContractAddress(sourceChain),
+                relayer.getDefaultDeliveryProvider(),
+                new VaaKey[](0),
+                15
+            );
+        }
+        if (message.version == Version.MULTIFORWARD) {
+            relayer.forwardToEvm{value: 0}(
+                wormhole.chainId(),
+                getRegisteredContractAddress(wormhole.chainId()),
+                encodeMessage(Message(Version.SEND, message.forwardMessage, bytes(""))),
+                TargetNative.wrap(0),
+                LocalNative.wrap(0),
+                Gas.wrap(500000),
+                wormhole.chainId(),
+                getRegisteredContractAddress(wormhole.chainId()),
+                relayer.getDefaultDeliveryProvider(),
+                new VaaKey[](0),
+                15
+            );
+        }
+    }
+
+    function getMessage() public view returns (bytes memory) {
+        if (messageHistory.length == 0) {
+            return new bytes(0);
+        }
+        return messageHistory[messageHistory.length - 1];
+    }
+
+    function getDeliveryData() public view returns (DeliveryData memory deliveryData) {
+        deliveryData = latestDeliveryData;
+    }
+
+    function getMessageHistory() public view returns (bytes[] memory) {
+        return messageHistory;
+    }
+
+    function emitterAddress() public view returns (bytes32) {
+        return bytes32(uint256(uint160(address(this))));
+    }
+
+    function registerEmitter(uint16 chainId, bytes32 emitterAddress_) public {
+        require(msg.sender == owner);
+        registeredContracts[chainId] = emitterAddress_;
+    }
+
+    function registerEmitters(XAddress[] calldata emitters) public {
+        require(msg.sender == owner);
+        for (uint256 i = 0; i < emitters.length; i++) {
+            registeredContracts[emitters[i].chainId] = emitters[i].addr;
+        }
+    }
+
+    function getRegisteredContract(uint16 chainId) public view returns (bytes32) {
+        return registeredContracts[chainId];
+    }
+
+    function getRegisteredContractAddress(uint16 chainId) public view returns (address) {
+        return address(uint160(uint256(registeredContracts[chainId])));
+    }
+
+    function encodeMessage(Message memory message) public pure returns (bytes memory encoded) {
+        return abi.encodePacked(
+            uint8(message.version),
+            uint32(message.message.length),
+            message.message,
+            uint32(message.forwardMessage.length),
+            message.forwardMessage
+        );
+    }
+
+    function decodeMessage(bytes memory encoded) internal pure returns (Message memory message) {
+        uint256 index = 0;
+        message.version = Version(encoded.toUint8(index));
+        index += 1;
+        uint32 length = encoded.toUint32(index);
+        index += 4;
+        message.message = encoded.slice(index, length);
+        index += length;
+        length = encoded.toUint32(index);
+        index += 4;
+        message.forwardMessage = encoded.slice(index, length);
+        index += length;
+        require(index == encoded.length, "Decoded message incorrectly");
+    }
+
+    receive() external payable {}
+}

+ 79 - 0
ethereum/contracts/relayer/create2Factory/Create2Factory.sol

@@ -0,0 +1,79 @@
+// SPDX-License-Identifier: Apache 2
+
+pragma solidity ^0.8.0;
+
+import "@openzeppelin/contracts/utils/Create2.sol";
+import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
+import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Upgrade.sol";
+
+/**
+ * Contract factory that facilitates predfictable deployment addresses
+ */
+contract Create2Factory {
+    event Created(address addr);
+
+    address public immutable init;
+    bytes32 immutable proxyBytecodeHash;
+
+    constructor() {
+        address initAddr = address(new Init());
+        init = initAddr;
+        proxyBytecodeHash = keccak256(
+            abi.encodePacked(type(SimpleProxy).creationCode, abi.encode(address(initAddr)))
+        );
+    }
+
+    /// @dev create2 hashes the userSalt with msg.sender, then uses the CREATE2 opcode to deterministically create a contract
+    function create2(
+        bytes memory userSalt,
+        bytes memory bytecode
+    ) public payable returns (address payable) {
+        address addr = Create2.deploy(msg.value, salt(msg.sender, userSalt), bytecode);
+        emit Created(addr);
+        return payable(addr);
+    }
+
+    function create2Proxy(
+        bytes memory userSalt,
+        address impl,
+        bytes memory call
+    ) public payable returns (address payable) {
+        address payable proxy = create2(
+            userSalt, abi.encodePacked(type(SimpleProxy).creationCode, abi.encode(address(init)))
+        );
+
+        Init(proxy).upgrade(impl, call);
+        return proxy;
+    }
+
+    function computeProxyAddress(
+        address creator,
+        bytes memory userSalt
+    ) public view returns (address) {
+        return Create2.computeAddress(salt(creator, userSalt), proxyBytecodeHash);
+    }
+
+    function computeAddress(
+        address creator,
+        bytes memory userSalt,
+        bytes32 bytecodeHash
+    ) public view returns (address) {
+        return Create2.computeAddress(salt(creator, userSalt), bytecodeHash);
+    }
+
+    function salt(address creator, bytes memory userSalt) internal pure returns (bytes32) {
+        return keccak256(abi.encodePacked(creator, userSalt));
+    }
+}
+
+contract SimpleProxy is ERC1967Proxy {
+    constructor(address impl) ERC1967Proxy(impl, new bytes(0)) {}
+}
+
+contract Init is ERC1967Upgrade {
+    constructor() {}
+
+    function upgrade(address impl, bytes memory call) external {
+        _upgradeToAndCall(impl, call, false);
+    }
+}

+ 188 - 0
ethereum/contracts/relayer/deliveryProvider/DeliveryProvider.sol

@@ -0,0 +1,188 @@
+// SPDX-License-Identifier: Apache 2
+
+pragma solidity ^0.8.19;
+
+import "./DeliveryProviderGovernance.sol";
+import "./DeliveryProviderStructs.sol";
+import "../../interfaces/relayer/IDeliveryProviderTyped.sol";
+import "../../interfaces/relayer/TypedUnits.sol";
+import "../../libraries/relayer/ExecutionParameters.sol";
+import {IWormhole} from "../../interfaces/IWormhole.sol";
+
+contract DeliveryProvider is DeliveryProviderGovernance, IDeliveryProvider {
+    using WeiLib for Wei;
+    using GasLib for Gas;
+    using GasPriceLib for GasPrice;
+    using WeiPriceLib for WeiPrice;
+    using TargetNativeLib for TargetNative;
+    using LocalNativeLib for LocalNative;
+
+    error CallerNotApproved(address msgSender);
+    error PriceIsZero(uint16 chain);
+    error Overflow(uint256 value, uint256 max);
+    error MaxRefundGreaterThanGasLimitCost(uint256 maxRefund, uint256 gasLimitCost);
+    error MaxRefundGreaterThanGasLimitCostOnSourceChain(uint256 maxRefund, uint256 gasLimitCost);
+    error ExceedsMaximumBudget(uint16 targetChain, uint256 exceedingValue, uint256 maximumBudget);
+
+    function quoteEvmDeliveryPrice(
+        uint16 targetChain,
+        Gas gasLimit,
+        TargetNative receiverValue
+    )
+        public
+        view
+        returns (LocalNative nativePriceQuote, GasPrice targetChainRefundPerUnitGasUnused)
+    {
+        (uint16 buffer, uint16 denominator) = assetConversionBuffer(targetChain);
+        targetChainRefundPerUnitGasUnused = GasPrice.wrap(gasPrice(targetChain).unwrap() * (denominator) / (uint256(denominator) + buffer));
+        
+        TargetNative maxRefund = gasLimit.toWei(targetChainRefundPerUnitGasUnused).asTargetNative();
+        TargetNative gasLimitCost = gasLimit.toWei(gasPrice(targetChain)).asTargetNative();
+        LocalNative maxRefundInSourceCurrency = quoteAssetCost(targetChain, maxRefund);
+        LocalNative gasLimitCostInSourceCurrency = quoteGasCost(targetChain, gasLimit);
+        LocalNative receiverValueCost = quoteAssetCost(targetChain, receiverValue);
+        nativePriceQuote = quoteDeliveryOverhead(targetChain) + gasLimitCostInSourceCurrency + receiverValueCost;
+        if(maxRefundInSourceCurrency.unwrap() > gasLimitCostInSourceCurrency.unwrap()) {
+            revert MaxRefundGreaterThanGasLimitCostOnSourceChain(maxRefundInSourceCurrency.unwrap(), gasLimitCostInSourceCurrency.unwrap());
+        } 
+        if(maxRefund.unwrap() > gasLimitCost.unwrap()) {
+            revert MaxRefundGreaterThanGasLimitCost(maxRefund.unwrap(), gasLimitCost.unwrap());
+        }
+        if(receiverValue.asNative() + gasLimitCost.asNative() > maximumBudget(targetChain).asNative()) {
+            revert ExceedsMaximumBudget(targetChain, receiverValue.unwrap() + gasLimitCost.unwrap(), maximumBudget(targetChain).unwrap());
+        }
+    }
+
+    function quoteDeliveryPrice(
+        uint16 targetChain,
+        TargetNative receiverValue,
+        bytes memory encodedExecutionParams
+    ) external view returns (LocalNative nativePriceQuote, bytes memory encodedExecutionInfo) {
+        ExecutionParamsVersion version = decodeExecutionParamsVersion(encodedExecutionParams);
+        if (version == ExecutionParamsVersion.EVM_V1) {
+            EvmExecutionParamsV1 memory parsed = decodeEvmExecutionParamsV1(encodedExecutionParams);
+            GasPrice targetChainRefundPerUnitGasUnused;
+            (nativePriceQuote, targetChainRefundPerUnitGasUnused) =
+                quoteEvmDeliveryPrice(targetChain, parsed.gasLimit, receiverValue);
+            return (
+                nativePriceQuote,
+                encodeEvmExecutionInfoV1(
+                    EvmExecutionInfoV1(parsed.gasLimit, targetChainRefundPerUnitGasUnused)
+                    )
+            );
+        } else {
+            revert UnsupportedExecutionParamsVersion(uint8(version));
+        }
+    }
+
+    function quoteAssetConversion(
+        uint16 targetChain,
+        LocalNative currentChainAmount
+    ) public view returns (TargetNative targetChainAmount) {
+        return quoteAssetConversion(chainId(), targetChain, currentChainAmount);
+    }
+
+    function quoteAssetConversion(
+        uint16 sourceChain,
+        uint16 targetChain,
+        LocalNative sourceChainAmount
+    ) internal view returns (TargetNative targetChainAmount) {
+        (uint16 buffer, uint16 bufferDenominator) = assetConversionBuffer(targetChain);
+        return sourceChainAmount.asNative().convertAsset(
+            nativeCurrencyPrice(sourceChain),
+            nativeCurrencyPrice(targetChain),
+            (bufferDenominator),
+            (uint32(buffer) + bufferDenominator),
+            false  // round down
+        ).asTargetNative();
+    }
+
+    //Returns the address on this chain that rewards should be sent to
+    function getRewardAddress() public view override returns (address payable) {
+        return rewardAddress();
+    }
+
+    function isChainSupported(uint16 targetChain) public view override returns (bool supported) {
+        return _state.supportedChains[targetChain];
+    }
+
+    function getTargetChainAddress(uint16 targetChain)
+        public
+        view
+        override
+        returns (bytes32 deliveryProviderAddress)
+    {
+        return targetChainAddress(targetChain);
+    }
+
+    /**
+     *
+     * HELPER METHODS
+     *
+     */
+
+    //Returns the delivery overhead fee required to deliver a message to the target chain, denominated in this chain's wei.
+    function quoteDeliveryOverhead(uint16 targetChain) public view returns (LocalNative nativePriceQuote) {
+        nativePriceQuote = quoteGasCost(targetChain, deliverGasOverhead(targetChain));
+        if(nativePriceQuote.unwrap() > type(uint128).max) {
+            revert Overflow(nativePriceQuote.unwrap(), type(uint128).max);
+        }
+    }
+
+    //Returns the price of purchasing gasAmount units of gas on the target chain, denominated in this chain's wei.
+    function quoteGasCost(uint16 targetChain, Gas gasAmount) public view returns (LocalNative totalCost) {
+        Wei gasCostInSourceChainCurrency =
+            assetConversion(targetChain, gasAmount.toWei(gasPrice(targetChain)), chainId());
+        totalCost = LocalNative.wrap(gasCostInSourceChainCurrency.unwrap());
+    }
+
+    function quoteGasPrice(uint16 targetChain) public view returns (GasPrice price) {
+        price = GasPrice.wrap(quoteGasCost(targetChain, Gas.wrap(1)).unwrap());
+        if(price.unwrap() > type(uint88).max) {
+            revert Overflow(price.unwrap(), type(uint88).max);
+        }
+    }
+
+    // relevant for chains that have dynamic execution pricing (e.g. Ethereum)
+    function assetConversion(
+        uint16 sourceChain,
+        Wei sourceAmount,
+        uint16 targetChain
+    ) internal view returns (Wei targetAmount) {
+        if(nativeCurrencyPrice(sourceChain).unwrap() == 0) {
+            revert PriceIsZero(sourceChain);
+        } 
+        if(nativeCurrencyPrice(targetChain).unwrap() == 0) {
+            revert PriceIsZero(targetChain);
+        }
+        return sourceAmount.convertAsset(
+            nativeCurrencyPrice(sourceChain),
+            nativeCurrencyPrice(targetChain),
+            1,
+            1,
+            // round up
+            true
+        );
+    }
+
+    function quoteAssetCost(
+        uint16 targetChain,
+        TargetNative targetChainAmount
+    ) internal view returns (LocalNative currentChainAmount) {
+        (uint16 buffer, uint16 bufferDenominator) = assetConversionBuffer(targetChain);
+        if(nativeCurrencyPrice(chainId()).unwrap() == 0) {
+            revert PriceIsZero(chainId());
+        } 
+        if(nativeCurrencyPrice(targetChain).unwrap() == 0) {
+            revert PriceIsZero(targetChain);
+        }
+        return targetChainAmount.asNative().convertAsset(
+            nativeCurrencyPrice(targetChain),
+            nativeCurrencyPrice(chainId()),
+            (uint32(buffer) + bufferDenominator),
+            (bufferDenominator),
+            // round up
+            true
+        ).asLocalNative();
+    }
+}

+ 65 - 0
ethereum/contracts/relayer/deliveryProvider/DeliveryProviderGetters.sol

@@ -0,0 +1,65 @@
+// contracts/Getters.sol
+// SPDX-License-Identifier: Apache 2
+
+pragma solidity ^0.8.19;
+
+import "../../interfaces/IWormhole.sol";
+import "../../interfaces/relayer/TypedUnits.sol";
+
+import "./DeliveryProviderState.sol";
+
+contract DeliveryProviderGetters is DeliveryProviderState {
+    function owner() public view returns (address) {
+        return _state.owner;
+    }
+
+    function pendingOwner() public view returns (address) {
+        return _state.pendingOwner;
+    }
+
+    function isInitialized(address impl) public view returns (bool) {
+        return _state.initializedImplementations[impl];
+    }
+
+    function chainId() public view returns (uint16) {
+        return _state.chainId;
+    }
+
+    function coreRelayer() public view returns (address) {
+        return _state.coreRelayer;
+    }
+
+    function gasPrice(uint16 targetChain) public view returns (GasPrice) {
+        return _state.data[targetChain].gasPrice;
+    }
+
+    function nativeCurrencyPrice(uint16 targetChain) public view returns (WeiPrice) {
+        return _state.data[targetChain].nativeCurrencyPrice;
+    }
+
+    function deliverGasOverhead(uint16 targetChain) public view returns (Gas) {
+        return _state.deliverGasOverhead[targetChain];
+    }
+
+    function maximumBudget(uint16 targetChain) public view returns (TargetNative) {
+        return _state.maximumBudget[targetChain];
+    }
+
+    function targetChainAddress(uint16 targetChain) public view returns (bytes32) {
+        return _state.targetChainAddresses[targetChain];
+    }
+
+    function rewardAddress() public view returns (address payable) {
+        return _state.rewardAddress;
+    }
+
+    function assetConversionBuffer(uint16 targetChain)
+        public
+        view
+        returns (uint16 tolerance, uint16 toleranceDenominator)
+    {
+        DeliveryProviderStorage.AssetConversion storage assetConversion =
+            _state.assetConversion[targetChain];
+        return (assetConversion.buffer, assetConversion.denominator);
+    }
+}

+ 329 - 0
ethereum/contracts/relayer/deliveryProvider/DeliveryProviderGovernance.sol

@@ -0,0 +1,329 @@
+// contracts/Relayer.sol
+// SPDX-License-Identifier: Apache 2
+
+pragma solidity ^0.8.19;
+
+import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Upgrade.sol";
+
+import "../../libraries/external/BytesLib.sol";
+import "../../interfaces/relayer/TypedUnits.sol";
+
+import "./DeliveryProviderGetters.sol";
+import "./DeliveryProviderSetters.sol";
+import "./DeliveryProviderStructs.sol";
+
+abstract contract DeliveryProviderGovernance is
+    DeliveryProviderGetters,
+    DeliveryProviderSetters,
+    ERC1967Upgrade
+{
+    using WeiLib for Wei;
+    using GasLib for Gas;
+    using DollarLib for Dollar;
+    using WeiPriceLib for WeiPrice;
+    using GasPriceLib for GasPrice;
+
+    error ChainIdIsZero();
+    error GasPriceIsZero();
+    error NativeCurrencyPriceIsZero();
+    error FailedToInitializeImplementation(string reason);
+    error WrongChainId();
+    error AddressIsZero();
+    error CallerMustBePendingOwner();
+    error CallerMustBeOwner();
+
+    event ContractUpgraded(address indexed oldContract, address indexed newContract);
+    event ChainSupportUpdated(uint16 targetChain, bool isSupported);
+    event OwnershipTransfered(address indexed oldOwner, address indexed newOwner);
+    event RewardAddressUpdated(address indexed newAddress);
+    event TargetChainAddressUpdated(uint16 indexed targetChain, bytes32 indexed newAddress);
+    event DeliverGasOverheadUpdated(Gas indexed oldGasOverhead, Gas indexed newGasOverhead);
+    event WormholeRelayerUpdated(address coreRelayer);
+    event AssetConversionBufferUpdated(uint16 targetChain, uint16 buffer, uint16 bufferDenominator);
+
+    function updateWormholeRelayer(address payable newAddress) external onlyOwner {
+        updateWormholeRelayerImpl(newAddress);
+    }
+
+    function updateWormholeRelayerImpl(address payable newAddress) internal {
+        setWormholeRelayer(newAddress);
+        emit WormholeRelayerUpdated(newAddress);
+    }
+
+    function updateSupportedChain(uint16 targetChain, bool isSupported) public onlyOwner {
+        setChainSupported(targetChain, isSupported);
+        emit ChainSupportUpdated(targetChain, isSupported);
+    }
+
+    function updateSupportedChains(DeliveryProviderStructs.SupportedChainUpdate[] memory updates)
+        public
+        onlyOwner
+    {
+        uint256 updatesLength = updates.length;
+        for (uint256 i = 0; i < updatesLength;) {
+            DeliveryProviderStructs.SupportedChainUpdate memory update = updates[i];
+            updateSupportedChainImpl(update.chainId, update.isSupported);
+            unchecked {
+                i += 1;
+            }
+        }
+    }
+
+    function updateSupportedChainImpl(uint16 targetChain, bool isSupported) internal {
+        setChainSupported(targetChain, isSupported);
+        emit ChainSupportUpdated(targetChain, isSupported);
+    }
+
+    function updateRewardAddress(address payable newAddress) external onlyOwner {
+        updateRewardAddressImpl(newAddress);
+    }
+
+    function updateRewardAddressImpl(address payable newAddress) internal {
+        setRewardAddress(newAddress);
+        emit RewardAddressUpdated(newAddress);
+    }
+
+    function updateTargetChainAddress(uint16 targetChain, bytes32 newAddress) public onlyOwner {
+        updateTargetChainAddressImpl(targetChain, newAddress);
+    }
+
+    function updateTargetChainAddresses(DeliveryProviderStructs.TargetChainUpdate[] memory updates)
+        external
+        onlyOwner
+    {
+        uint256 updatesLength = updates.length;
+        for (uint256 i = 0; i < updatesLength;) {
+            DeliveryProviderStructs.TargetChainUpdate memory update = updates[i];
+            updateTargetChainAddressImpl(update.chainId, update.targetChainAddress);
+            unchecked {
+                i += 1;
+            }
+        }
+    }
+
+    function updateTargetChainAddressImpl(uint16 targetChain, bytes32 newAddress) internal {
+        setTargetChainAddress(targetChain, newAddress);
+        emit TargetChainAddressUpdated(targetChain, newAddress);
+    }
+
+    function updateDeliverGasOverhead(uint16 chainId, Gas newGasOverhead) external onlyOwner {
+        updateDeliverGasOverheadImpl(chainId, newGasOverhead);
+    }
+
+    function updateDeliverGasOverheads(
+        DeliveryProviderStructs.DeliverGasOverheadUpdate[] memory overheadUpdates
+    ) external onlyOwner {
+        uint256 updatesLength = overheadUpdates.length;
+        for (uint256 i = 0; i < updatesLength;) {
+            DeliveryProviderStructs.DeliverGasOverheadUpdate memory update = overheadUpdates[i];
+            updateDeliverGasOverheadImpl(update.chainId, update.newGasOverhead);
+            unchecked {
+                i += 1;
+            }
+        }
+    }
+
+    function updateDeliverGasOverheadImpl(uint16 chainId, Gas newGasOverhead) internal {
+        Gas currentGasOverhead = deliverGasOverhead(chainId);
+        setDeliverGasOverhead(chainId, newGasOverhead);
+        emit DeliverGasOverheadUpdated(currentGasOverhead, newGasOverhead);
+    }
+
+    function updatePrice(
+        uint16 updateChainId,
+        GasPrice updateGasPrice,
+        WeiPrice updateNativeCurrencyPrice
+    ) external onlyOwner {
+        updatePriceImpl(updateChainId, updateGasPrice, updateNativeCurrencyPrice);
+    }
+
+    function updatePrices(DeliveryProviderStructs.UpdatePrice[] memory updates)
+        external
+        onlyOwner
+    {
+        uint256 pricesLength = updates.length;
+        for (uint256 i = 0; i < pricesLength;) {
+            DeliveryProviderStructs.UpdatePrice memory update = updates[i];
+            updatePriceImpl(update.chainId, update.gasPrice, update.nativeCurrencyPrice);
+            unchecked {
+                i += 1;
+            }
+        }
+    }
+
+    function updatePriceImpl(
+        uint16 updateChainId,
+        GasPrice updateGasPrice,
+        WeiPrice updateNativeCurrencyPrice
+    ) internal {
+        if (updateChainId == 0) {
+            revert ChainIdIsZero();
+        }
+        if (updateGasPrice.unwrap() == 0) {
+            revert GasPriceIsZero();
+        }
+        if (updateNativeCurrencyPrice.unwrap() == 0) {
+            revert NativeCurrencyPriceIsZero();
+        }
+
+        setPriceInfo(updateChainId, updateGasPrice, updateNativeCurrencyPrice);
+    }
+
+    function updateMaximumBudget(uint16 targetChain, Wei maximumTotalBudget) external onlyOwner {
+        setMaximumBudget(targetChain, maximumTotalBudget);
+    }
+
+    function updateMaximumBudgets(DeliveryProviderStructs.MaximumBudgetUpdate[] memory updates)
+        external
+        onlyOwner
+    {
+        uint256 updatesLength = updates.length;
+        for (uint256 i = 0; i < updatesLength;) {
+            DeliveryProviderStructs.MaximumBudgetUpdate memory update = updates[i];
+            setMaximumBudget(update.chainId, update.maximumTotalBudget);
+            unchecked {
+                i += 1;
+            }
+        }
+    }
+
+    function updateAssetConversionBuffer(
+        uint16 targetChain,
+        uint16 buffer,
+        uint16 bufferDenominator
+    ) external onlyOwner {
+        updateAssetConversionBufferImpl(targetChain, buffer, bufferDenominator);
+    }
+
+    function updateAssetConversionBuffers(
+        DeliveryProviderStructs.AssetConversionBufferUpdate[] memory updates
+    ) external onlyOwner {
+        uint256 updatesLength = updates.length;
+        for (uint256 i = 0; i < updatesLength;) {
+            DeliveryProviderStructs.AssetConversionBufferUpdate memory update = updates[i];
+            updateAssetConversionBufferImpl(update.chainId, update.buffer, update.bufferDenominator);
+            unchecked {
+                i += 1;
+            }
+        }
+    }
+
+    function updateAssetConversionBufferImpl(
+        uint16 targetChain,
+        uint16 buffer,
+        uint16 bufferDenominator
+    ) internal {
+        setAssetConversionBuffer(targetChain, buffer, bufferDenominator);
+        emit AssetConversionBufferUpdated(targetChain, buffer, bufferDenominator);
+    }
+
+    function updateConfig(
+        DeliveryProviderStructs.Update[] memory updates,
+        DeliveryProviderStructs.CoreConfig memory coreConfig
+    ) external onlyOwner {
+        uint256 updatesLength = updates.length;
+        for (uint256 i = 0; i < updatesLength;) {
+            DeliveryProviderStructs.Update memory update = updates[i];
+            if (update.updatePrice) {
+                updatePriceImpl(update.chainId, update.gasPrice, update.nativeCurrencyPrice);
+            }
+            if (update.updateTargetChainAddress) {
+                updateTargetChainAddressImpl(update.chainId, update.targetChainAddress);
+            }
+            if (update.updateDeliverGasOverhead) {
+                updateDeliverGasOverheadImpl(update.chainId, update.newGasOverhead);
+            }
+            if (update.updateMaximumBudget) {
+                setMaximumBudget(update.chainId, update.maximumTotalBudget);
+            }
+            if (update.updateAssetConversionBuffer) {
+                updateAssetConversionBufferImpl(
+                    update.chainId, update.buffer, update.bufferDenominator
+                );
+            }
+            if (update.updateSupportedChain) {
+                updateSupportedChainImpl(update.chainId, update.isSupported);
+            }
+            unchecked {
+                i += 1;
+            }
+        }
+
+        if (coreConfig.updateWormholeRelayer) {
+            updateWormholeRelayerImpl(coreConfig.coreRelayer);
+        }
+
+        if (coreConfig.updateRewardAddress) {
+            updateRewardAddressImpl(coreConfig.rewardAddress);
+        }
+    }
+
+    /// @dev upgrade serves to upgrade contract implementations
+    function upgrade(uint16 deliveryProviderChainId, address newImplementation) public onlyOwner {
+        if (deliveryProviderChainId != chainId()) {
+            revert WrongChainId();
+        }
+
+        address currentImplementation = _getImplementation();
+
+        _upgradeTo(newImplementation);
+
+        // call initialize function of the new implementation
+        (bool success, bytes memory reason) =
+            newImplementation.delegatecall(abi.encodeWithSignature("initialize()"));
+
+        if (!success) {
+            revert FailedToInitializeImplementation(string(reason));
+        }
+
+        emit ContractUpgraded(currentImplementation, newImplementation);
+    }
+
+    /**
+     * @dev submitOwnershipTransferRequest serves to begin the ownership transfer process of the contracts
+     * - it saves an address for the new owner in the pending state
+     */
+    function submitOwnershipTransferRequest(
+        uint16 thisRelayerChainId,
+        address newOwner
+    ) external onlyOwner {
+        if (thisRelayerChainId != chainId()) {
+            revert WrongChainId();
+        }
+        if (newOwner == address(0)) {
+            revert AddressIsZero();
+        }
+
+        setPendingOwner(newOwner);
+    }
+
+    /**
+     * @dev confirmOwnershipTransferRequest serves to finalize an ownership transfer
+     * - it checks that the caller is the pendingOwner to validate the wallet address
+     * - it updates the owner state variable with the pendingOwner state variable
+     */
+    function confirmOwnershipTransferRequest() external {
+        // cache the new owner address
+        address newOwner = pendingOwner();
+
+        if (msg.sender != newOwner) {
+            revert CallerMustBePendingOwner();
+        }
+
+        // cache currentOwner for Event
+        address currentOwner = owner();
+
+        // update the owner in the contract state and reset the pending owner
+        setOwner(newOwner);
+        setPendingOwner(address(0));
+
+        emit OwnershipTransfered(currentOwner, newOwner);
+    }
+
+    modifier onlyOwner() {
+        if (owner() != _msgSender()) {
+            revert CallerMustBeOwner();
+        }
+        _;
+    }
+}

+ 28 - 0
ethereum/contracts/relayer/deliveryProvider/DeliveryProviderImplementation.sol

@@ -0,0 +1,28 @@
+// contracts/Implementation.sol
+// SPDX-License-Identifier: Apache 2
+
+pragma solidity ^0.8.19;
+
+import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Upgrade.sol";
+
+import "./DeliveryProvider.sol";
+
+contract DeliveryProviderImplementation is DeliveryProvider {
+    error ImplementationAlreadyInitialized();
+
+    function initialize() public virtual initializer {
+        // this function needs to be exposed for an upgrade to pass
+    }
+
+    modifier initializer() {
+        address impl = ERC1967Upgrade._getImplementation();
+
+        if (isInitialized(impl)) {
+            revert ImplementationAlreadyInitialized();
+        }
+
+        setInitialized(impl);
+
+        _;
+    }
+}

+ 13 - 0
ethereum/contracts/relayer/deliveryProvider/DeliveryProviderProxy.sol

@@ -0,0 +1,13 @@
+// contracts/Wormhole.sol
+// SPDX-License-Identifier: Apache 2
+
+pragma solidity ^0.8.19;
+
+import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
+
+contract DeliveryProviderProxy is ERC1967Proxy {
+    constructor(
+        address implementation,
+        bytes memory initData
+    ) ERC1967Proxy(implementation, initData) {}
+}

+ 77 - 0
ethereum/contracts/relayer/deliveryProvider/DeliveryProviderSetters.sol

@@ -0,0 +1,77 @@
+// contracts/Setters.sol
+// SPDX-License-Identifier: Apache 2
+
+pragma solidity ^0.8.19;
+
+import "@openzeppelin/contracts/utils/Context.sol";
+
+import "./DeliveryProviderState.sol";
+import "../../interfaces/relayer/IDeliveryProviderTyped.sol";
+
+contract DeliveryProviderSetters is Context, DeliveryProviderState {
+    using GasPriceLib for GasPrice;
+    using WeiLib for Wei;
+
+    function setOwner(address owner_) internal {
+        _state.owner = owner_;
+    }
+
+    function setPendingOwner(address newOwner) internal {
+        _state.pendingOwner = newOwner;
+    }
+
+    function setInitialized(address implementation) internal {
+        _state.initializedImplementations[implementation] = true;
+    }
+
+    function setChainId(uint16 thisChain) internal {
+        _state.chainId = thisChain;
+    }
+
+    function setWormholeRelayer(address payable coreRelayer) internal {
+        _state.coreRelayer = coreRelayer;
+    }
+
+    function setChainSupported(uint16 targetChain, bool isSupported) internal {
+        _state.supportedChains[targetChain] = isSupported;
+    }
+
+    function setDeliverGasOverhead(uint16 chainId, Gas deliverGasOverhead) internal {
+        require(Gas.unwrap(deliverGasOverhead) <= type(uint32).max, "deliverGasOverhead too large");
+        _state.deliverGasOverhead[chainId] = deliverGasOverhead;
+    }
+
+    function setRewardAddress(address payable rewardAddress) internal {
+        _state.rewardAddress = rewardAddress;
+    }
+
+    function setTargetChainAddress(uint16 targetChain, bytes32 newAddress) internal {
+        _state.targetChainAddresses[targetChain] = newAddress;
+    }
+
+    function setMaximumBudget(uint16 targetChain, Wei amount) internal {
+        require(amount.unwrap() <= type(uint192).max, "amount too large");
+        _state.maximumBudget[targetChain] = amount.asTargetNative();
+    }
+
+    function setPriceInfo(
+        uint16 updateChainId,
+        GasPrice updateGasPrice,
+        WeiPrice updateNativeCurrencyPrice
+    ) internal {
+        require(updateGasPrice.unwrap() <= type(uint64).max, "gas price must be < 2^64");
+        _state.data[updateChainId].gasPrice = updateGasPrice;
+        _state.data[updateChainId].nativeCurrencyPrice = updateNativeCurrencyPrice;
+    }
+
+    function setAssetConversionBuffer(
+        uint16 targetChain,
+        uint16 tolerance,
+        uint16 toleranceDenominator
+    ) internal {
+        DeliveryProviderStorage.AssetConversion storage assetConversion =
+            _state.assetConversion[targetChain];
+        assetConversion.buffer = tolerance;
+        assetConversion.denominator = toleranceDenominator;
+    }
+}

+ 33 - 0
ethereum/contracts/relayer/deliveryProvider/DeliveryProviderSetup.sol

@@ -0,0 +1,33 @@
+// contracts/Setup.sol
+// SPDX-License-Identifier: Apache 2
+
+pragma solidity ^0.8.19;
+
+import "./DeliveryProviderGovernance.sol";
+
+import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Upgrade.sol";
+
+contract DeliveryProviderSetup is DeliveryProviderSetters, ERC1967Upgrade {
+    error ImplementationAddressIsZero();
+    error FailedToInitializeImplementation(string reason);
+
+    function setup(address implementation, uint16 chainId) public {
+        // sanity check initial values
+        if (implementation == address(0)) {
+            revert ImplementationAddressIsZero();
+        }
+
+        setOwner(_msgSender());
+
+        setChainId(chainId);
+
+        _upgradeTo(implementation);
+
+        // call initialize function of the new implementation
+        (bool success, bytes memory reason) =
+            implementation.delegatecall(abi.encodeWithSignature("initialize()"));
+        if (!success) {
+            revert FailedToInitializeImplementation(string(reason));
+        }
+    }
+}

+ 54 - 0
ethereum/contracts/relayer/deliveryProvider/DeliveryProviderState.sol

@@ -0,0 +1,54 @@
+// contracts/State.sol
+// SPDX-License-Identifier: Apache 2
+
+pragma solidity ^0.8.19;
+
+import "../../interfaces/relayer/TypedUnits.sol";
+
+contract DeliveryProviderStorage {
+    struct PriceData {
+        // The price of purchasing 1 unit of gas on the target chain, denominated in target chain's wei.
+        GasPrice gasPrice;
+        // The price of the native currency denominated in USD * 10^6
+        WeiPrice nativeCurrencyPrice;
+    }
+
+    struct AssetConversion {
+        // The following two fields are a percentage buffer that is used to upcharge the user for the value attached to the message sent.
+        // The cost of getting ‘targetAmount’ on the target chain for the receiverValue is
+        // (denominator + buffer) / (denominator) * (the converted amount in source chain currency using the ‘quoteAssetPrice’ values)
+        uint16 buffer;
+        uint16 denominator;
+    }
+
+    struct State {
+        // Wormhole chain id of this blockchain.
+        uint16 chainId;
+        // Current owner.
+        address owner;
+        // Pending target of ownership transfer.
+        address pendingOwner;
+        // Address of the core relayer contract.
+        address coreRelayer;
+        // Dictionary of implementation contract -> initialized flag
+        mapping(address => bool) initializedImplementations;
+        // Supported chains to deliver to
+        mapping(uint16 => bool) supportedChains;
+        // Contracts of this relay provider on other chains
+        mapping(uint16 => bytes32) targetChainAddresses;
+        // Dictionary of wormhole chain id -> price data
+        mapping(uint16 => PriceData) data;
+        // The delivery overhead gas required to deliver a message to targetChain, denominated in targetChain's gas.
+        mapping(uint16 => Gas) deliverGasOverhead;
+        // The maximum budget that is allowed for a delivery on target chain, denominated in the targetChain's wei.
+        mapping(uint16 => TargetNative) maximumBudget;
+        // Dictionary of wormhole chain id -> assetConversion
+        mapping(uint16 => AssetConversion) assetConversion;
+        // Reward address for the relayer. The WormholeRelayer contract transfers the reward for relaying messages here.
+        address payable rewardAddress;
+    }
+}
+
+contract DeliveryProviderState {
+    DeliveryProviderStorage.State _state;
+}

+ 135 - 0
ethereum/contracts/relayer/deliveryProvider/DeliveryProviderStructs.sol

@@ -0,0 +1,135 @@
+// SPDX-License-Identifier: Apache 2
+
+pragma solidity ^0.8.19;
+
+import "../../interfaces/relayer/TypedUnits.sol";
+
+abstract contract DeliveryProviderStructs {
+    struct UpdatePrice {
+        /**
+         * Wormhole chain id
+         */
+        uint16 chainId;
+        /**
+         * Gas price in ´chainId´ chain.
+         */
+        GasPrice gasPrice;
+        /**
+         * Price of the native currency in ´chainId´ chain.
+         * Native currency is typically used to pay for gas.
+         */
+        WeiPrice nativeCurrencyPrice;
+    }
+
+    struct TargetChainUpdate {
+        /**
+         * Wormhole chain id
+         */
+        uint16 chainId;
+        /**
+         * Wormhole address of the relay provider in the ´chainId´ chain.
+         */
+        bytes32 targetChainAddress;
+    }
+
+    struct MaximumBudgetUpdate {
+        /**
+         * Wormhole chain id
+         */
+        uint16 chainId;
+        /**
+         * Maximum total budget for a delivery in ´chainId´ chain.
+         */
+        Wei maximumTotalBudget;
+    }
+
+    struct DeliverGasOverheadUpdate {
+        /**
+         * Wormhole chain id
+         */
+        uint16 chainId;
+        /**
+         * The gas overhead for a delivery in ´chainId´ chain.
+         */
+        Gas newGasOverhead;
+    }
+
+    struct SupportedChainUpdate {
+        /**
+         * Wormhole chain id
+         */
+        uint16 chainId;
+        /**
+         * True if the chain is supported.
+         */
+        bool isSupported;
+    }
+
+    struct AssetConversionBufferUpdate {
+        /**
+         * Wormhole chain id
+         */
+        uint16 chainId;
+        // See DeliveryProviderState.AssetConversion
+        uint16 buffer;
+        uint16 bufferDenominator;
+    }
+
+    struct Update {
+        // Update flags
+        bool updateAssetConversionBuffer;
+        bool updateDeliverGasOverhead;
+        bool updatePrice;
+        bool updateTargetChainAddress;
+        bool updateMaximumBudget;
+        bool updateSupportedChain;
+        // SupportedChainUpdate
+        bool isSupported;
+        /**
+         * Wormhole chain id
+         */
+        uint16 chainId;
+        // AssetConversionBufferUpdate
+        // See DeliveryProviderState.AssetConversion
+        uint16 buffer;
+        uint16 bufferDenominator;
+        // DeliverGasOverheadUpdate
+        /**
+         * The gas overhead for a delivery in ´chainId´ chain.
+         */
+        Gas newGasOverhead;
+        // UpdatePrice
+        /**
+         * Gas price in ´chainId´ chain.
+         */
+        GasPrice gasPrice;
+        /**
+         * Price of the native currency in ´chainId´ chain.
+         * Native currency is typically used to pay for gas.
+         */
+        WeiPrice nativeCurrencyPrice;
+        // TargetChainUpdate
+        /**
+         * Wormhole address of the relay provider in the ´chainId´ chain.
+         */
+        bytes32 targetChainAddress;
+        // MaximumBudgetUpdate
+        /**
+         * Maximum total budget for a delivery in ´chainId´ chain.
+         */
+        Wei maximumTotalBudget;
+    }
+
+    struct CoreConfig {
+        bool updateWormholeRelayer;
+        bool updateRewardAddress;
+        /**
+         * Address of the WormholeRelayer contract
+         */
+        address payable coreRelayer;
+        /**
+         * Address where rewards are sent for successful relays and sends
+         */
+        address payable rewardAddress;
+    }
+}

+ 34 - 0
ethereum/contracts/relayer/wormholeRelayer/WormholeRelayer.sol

@@ -0,0 +1,34 @@
+// SPDX-License-Identifier: Apache 2
+
+pragma solidity ^0.8.19;
+
+import {IWormholeRelayer} from "../../interfaces/relayer/IWormholeRelayerTyped.sol";
+
+import {getDefaultDeliveryProviderState} from "./WormholeRelayerStorage.sol";
+import {WormholeRelayerGovernance} from "./WormholeRelayerGovernance.sol";
+import {WormholeRelayerSend} from "./WormholeRelayerSend.sol";
+import {WormholeRelayerDelivery} from "./WormholeRelayerDelivery.sol";
+import {WormholeRelayerBase} from "./WormholeRelayerBase.sol";
+
+//WormholeRelayerGovernance inherits from ERC1967Upgrade, i.e. this is a proxy contract!
+contract WormholeRelayer is
+    WormholeRelayerGovernance,
+    WormholeRelayerSend,
+    WormholeRelayerDelivery,
+    IWormholeRelayer
+{
+    //the only normal storage variable - everything else uses slot pattern
+    //no point doing it for this one since it is entirely one-off and of no interest to the rest
+    //  of the contract and it also can't accidentally be moved because we are at the bottom of
+    //  the inheritance hierarchy here
+    bool private initialized;
+
+    constructor(address wormhole) WormholeRelayerBase(wormhole) {}
+
+    //needs to be called upon construction of the EC1967 proxy
+    function initialize(address defaultDeliveryProvider) public {
+        assert(!initialized);
+        initialized = true;
+        getDefaultDeliveryProviderState().defaultDeliveryProvider = defaultDeliveryProvider;
+    }
+}

+ 157 - 0
ethereum/contracts/relayer/wormholeRelayer/WormholeRelayerBase.sol

@@ -0,0 +1,157 @@
+// SPDX-License-Identifier: Apache 2
+
+pragma solidity ^0.8.19;
+
+import {IWormhole} from "../../interfaces/IWormhole.sol";
+import {IDeliveryProvider} from "../../interfaces/relayer/IDeliveryProviderTyped.sol";
+import {toWormholeFormat, min, pay} from "../../libraries/relayer/Utils.sol";
+import {
+    NoDeliveryInProgress,
+    ReentrantDelivery,
+    ForwardRequestFromWrongAddress,
+    DeliveryProviderDoesNotSupportTargetChain,
+    VaaKey,
+    InvalidMsgValue,
+    IWormholeRelayerBase
+} from "../../interfaces/relayer/IWormholeRelayerTyped.sol";
+import {DeliveryInstruction} from "../../libraries/relayer/RelayerInternalStructs.sol";
+import {
+    ForwardInstruction,
+    DeliveryTmpState,
+    getDeliveryTmpState,
+    getRegisteredWormholeRelayersState
+} from "./WormholeRelayerStorage.sol";
+import "../../interfaces/relayer/TypedUnits.sol";
+
+abstract contract WormholeRelayerBase is IWormholeRelayerBase {
+    using WeiLib for Wei;
+    using GasLib for Gas;
+    using WeiPriceLib for WeiPrice;
+    using GasPriceLib for GasPrice;
+    using LocalNativeLib for LocalNative;
+
+    //see https://book.wormhole.com/wormhole/3_coreLayerContracts.html#consistency-levels
+    //  15 is valid choice for now but ultimately we want something more canonical (202?)
+    //  Also, these values should definitely not be defined here but should be provided by IWormhole!
+    uint8 internal constant CONSISTENCY_LEVEL_FINALIZED = 15;
+    uint8 internal constant CONSISTENCY_LEVEL_INSTANT = 200;
+
+    IWormhole private immutable wormhole_;
+    uint16 private immutable chainId_;
+
+    constructor(address _wormhole) {
+        wormhole_ = IWormhole(_wormhole);
+        chainId_ = uint16(wormhole_.chainId());
+    }
+
+    function getRegisteredWormholeRelayerContract(uint16 chainId) public view returns (bytes32) {
+        return getRegisteredWormholeRelayersState().registeredWormholeRelayers[chainId];
+    }
+
+    //Our get functions require view instead of pure (despite not actually reading storage) because
+    //  they can't be evaluated at compile time. (https://ethereum.stackexchange.com/a/120630/103366)
+
+    function getWormhole() internal view returns (IWormhole) {
+        return wormhole_;
+    }
+
+    function getChainId() internal view returns (uint16) {
+        return chainId_;
+    }
+
+    function getWormholeMessageFee() internal view returns (LocalNative) {
+        return LocalNative.wrap(getWormhole().messageFee());
+    }
+
+    function msgValue() internal view returns (LocalNative) {
+        return LocalNative.wrap(msg.value);
+    }
+
+    function checkMsgValue(
+        LocalNative wormholeMessageFee,
+        LocalNative deliveryPrice,
+        LocalNative paymentForExtraReceiverValue
+    ) internal view {
+        if (msgValue() != deliveryPrice + paymentForExtraReceiverValue + wormholeMessageFee) {
+            revert InvalidMsgValue(
+                msgValue(), deliveryPrice + paymentForExtraReceiverValue + wormholeMessageFee
+            );
+        }
+    }
+
+    function publishAndPay(
+        LocalNative wormholeMessageFee,
+        LocalNative deliveryQuote,
+        LocalNative paymentForExtraReceiverValue,
+        bytes memory encodedInstruction,
+        uint8 consistencyLevel,
+        address payable rewardAddress
+    ) internal returns (uint64 sequence, bool paymentSucceeded) {
+        sequence = getWormhole().publishMessage{value: wormholeMessageFee.unwrap()}(
+            0, encodedInstruction, consistencyLevel
+        );
+
+        paymentSucceeded = pay(
+            rewardAddress,
+            deliveryQuote + paymentForExtraReceiverValue
+        );
+
+        emit SendEvent(sequence, deliveryQuote, paymentForExtraReceiverValue);
+    }
+
+    // ----------------------- delivery transaction temorary storage functions -----------------------
+
+    function startDelivery(address targetAddress, address deliveryProvider, uint16 refundChain, bytes32 refundAddress) internal {
+        DeliveryTmpState storage state = getDeliveryTmpState();
+        if (state.deliveryInProgress) {
+            revert ReentrantDelivery(msg.sender, state.deliveryTarget);
+        }
+
+        state.deliveryInProgress = true;
+        state.deliveryTarget = targetAddress;
+        state.deliveryProvider = deliveryProvider;
+        state.refundChain = refundChain;
+        state.refundAddress = refundAddress;
+    }
+
+    function finishDelivery() internal {
+        DeliveryTmpState storage state = getDeliveryTmpState();
+        state.deliveryInProgress = false;
+        state.deliveryTarget = address(0);
+        state.deliveryProvider = address(0);
+        state.refundChain = 0;
+        state.refundAddress = bytes32(0);
+        delete state.forwardInstructions;
+    }
+
+    function appendForwardInstruction(ForwardInstruction memory forwardInstruction) internal {
+        getDeliveryTmpState().forwardInstructions.push(forwardInstruction);
+    }
+
+    function getForwardInstructions() internal view returns (ForwardInstruction[] storage) {
+        return getDeliveryTmpState().forwardInstructions;
+    }
+
+    function getOriginalDeliveryProvider() internal view returns (address) {
+        return getDeliveryTmpState().deliveryProvider;
+    }
+
+    function getCurrentRefundChain() internal view returns (uint16) {
+        return getDeliveryTmpState().refundChain;
+    }
+
+    function getCurrentRefundAddress() internal view returns (bytes32) {
+        return getDeliveryTmpState().refundAddress;
+    }
+
+    function checkMsgSenderInDelivery() internal view {
+        DeliveryTmpState storage state = getDeliveryTmpState();
+        if (!state.deliveryInProgress) {
+            revert NoDeliveryInProgress();
+        }
+
+        if (msg.sender != state.deliveryTarget) {
+            revert ForwardRequestFromWrongAddress(msg.sender, state.deliveryTarget);
+        }
+    }
+}

+ 688 - 0
ethereum/contracts/relayer/wormholeRelayer/WormholeRelayerDelivery.sol

@@ -0,0 +1,688 @@
+// SPDX-License-Identifier: Apache 2
+
+pragma solidity ^0.8.19;
+
+import {IWormhole} from "../../interfaces/IWormhole.sol";
+import {
+    InvalidDeliveryVaa,
+    InvalidEmitter,
+    InsufficientRelayerFunds,
+    TargetChainIsNotThisChain,
+    ForwardNotSufficientlyFunded,
+    VaaKeysLengthDoesNotMatchVaasLength,
+    VaaKeysDoNotMatchVaas,
+    InvalidOverrideGasLimit,
+    InvalidOverrideReceiverValue,
+    InvalidOverrideRefundPerGasUnused,
+    RequesterNotWormholeRelayer,
+    DeliveryProviderCannotReceivePayment,
+    VaaKey,
+    IWormholeRelayerDelivery,
+    IWormholeRelayerSend,
+    RETURNDATA_TRUNCATION_THRESHOLD
+} from "../../interfaces/relayer/IWormholeRelayerTyped.sol";
+import {IWormholeReceiver} from "../../interfaces/relayer/IWormholeReceiver.sol";
+import {IDeliveryProvider} from "../../interfaces/relayer/IDeliveryProviderTyped.sol";
+
+import {pay, min, toWormholeFormat, fromWormholeFormat, returnLengthBoundedCall} from "../../libraries/relayer/Utils.sol";
+import {
+    DeliveryInstruction,
+    DeliveryOverride,
+    EvmDeliveryInstruction
+} from "../../libraries/relayer/RelayerInternalStructs.sol";
+import {BytesParsing} from "../../libraries/relayer/BytesParsing.sol";
+import {WormholeRelayerSerde} from "./WormholeRelayerSerde.sol";
+import {ForwardInstruction} from "./WormholeRelayerStorage.sol";
+import {WormholeRelayerBase} from "./WormholeRelayerBase.sol";
+import "../../interfaces/relayer/TypedUnits.sol";
+import "../../libraries/relayer/ExecutionParameters.sol";
+
+abstract contract WormholeRelayerDelivery is WormholeRelayerBase, IWormholeRelayerDelivery {
+    using WormholeRelayerSerde for *; 
+    using BytesParsing for bytes;
+    using WeiLib for Wei;
+    using GasLib for Gas;
+    using GasPriceLib for GasPrice;
+    using TargetNativeLib for TargetNative;
+    using LocalNativeLib for LocalNative;
+
+    function deliver(
+        bytes[] memory encodedVMs,
+        bytes memory encodedDeliveryVAA,
+        address payable relayerRefundAddress,
+        bytes memory deliveryOverrides
+    ) public payable {
+
+        // Parse and verify VAA containing delivery instructions, revert if invalid
+        (IWormhole.VM memory vm, bool valid, string memory reason) =
+            getWormhole().parseAndVerifyVM(encodedDeliveryVAA);
+        if (!valid) {
+            revert InvalidDeliveryVaa(reason);
+        }
+
+        // Revert if the emitter of the VAA is not a Wormhole Relayer contract 
+        bytes32 registeredWormholeRelayer = getRegisteredWormholeRelayerContract(vm.emitterChainId);
+        if (vm.emitterAddress != registeredWormholeRelayer) {
+            revert InvalidEmitter(vm.emitterAddress, registeredWormholeRelayer, vm.emitterChainId);
+        }
+    
+        DeliveryInstruction memory instruction = vm.payload.decodeDeliveryInstruction();
+
+        // Lock the contract (and store some information about the delivery in temporary storage)
+        startDelivery(
+            fromWormholeFormat(instruction.targetAddress),
+            fromWormholeFormat(instruction.refundDeliveryProvider),
+            instruction.refundChain,
+            instruction.refundAddress
+        );
+
+        DeliveryVAAInfo memory deliveryVaaInfo = DeliveryVAAInfo({
+            sourceChain: vm.emitterChainId,
+            sourceSequence: vm.sequence,
+            deliveryVaaHash: vm.hash,
+            relayerRefundAddress: relayerRefundAddress,
+            encodedVMs: encodedVMs,
+            deliveryInstruction: instruction,
+            gasLimit: Gas.wrap(0),
+            targetChainRefundPerGasUnused: GasPrice.wrap(0),
+            totalReceiverValue: TargetNative.wrap(0),
+            encodedOverrides: deliveryOverrides,
+            redeliveryHash: bytes32(0)
+        });
+
+        // Decode information from the execution parameters
+        // (overriding them if there was an override requested)
+        // Assumes execution parameters and info are of version EVM_V1
+        (
+            deliveryVaaInfo.gasLimit,
+            deliveryVaaInfo.targetChainRefundPerGasUnused,
+            deliveryVaaInfo.totalReceiverValue,
+            deliveryVaaInfo.redeliveryHash
+        ) = getDeliveryParametersEvmV1(instruction, deliveryOverrides);
+
+        // Revert if msg.value is not enough to fund both the receiver value
+        // as well as the maximum possible refund 
+        // Note: instruction's TargetNative is delivery's LocalNative
+        LocalNative requiredFunds = (deliveryVaaInfo.gasLimit.toWei(
+            deliveryVaaInfo.targetChainRefundPerGasUnused
+        ) + deliveryVaaInfo.totalReceiverValue.asNative()).asLocalNative();
+        if (msgValue() < requiredFunds) {
+            revert InsufficientRelayerFunds(msgValue(), requiredFunds);
+        }
+
+        // Revert if the instruction's target chain is not this chain
+        if (getChainId() != instruction.targetChain) {
+            revert TargetChainIsNotThisChain(instruction.targetChain);
+        }
+
+        // Revert if the VAAs delivered do not match the descriptions specified in the instruction
+        checkVaaKeysWithVAAs(instruction.vaaKeys, encodedVMs);
+
+        executeDelivery(deliveryVaaInfo);
+
+        // Unlock contract
+        finishDelivery();
+    }
+
+    // ------------------------------------------- PRIVATE -------------------------------------------
+
+    error Cancelled(Gas gasUsed, LocalNative available, LocalNative required);
+    error DeliveryProviderReverted(Gas gasUsed);
+    error DeliveryProviderPaymentFailed(Gas gasUsed);
+
+    struct DeliveryVAAInfo {
+        uint16 sourceChain;
+        uint64 sourceSequence;
+        bytes32 deliveryVaaHash;
+        address payable relayerRefundAddress;
+        bytes[] encodedVMs;
+        DeliveryInstruction deliveryInstruction;
+        Gas gasLimit;
+        GasPrice targetChainRefundPerGasUnused;
+        TargetNative totalReceiverValue;
+        bytes encodedOverrides;
+        bytes32 redeliveryHash; //optional (0 if not present)
+    }
+
+    function getDeliveryParametersEvmV1(
+        DeliveryInstruction memory instruction,
+        bytes memory encodedOverrides
+    )
+        internal
+        pure
+        returns (
+            Gas gasLimit,
+            GasPrice targetChainRefundPerGasUnused,
+            TargetNative totalReceiverValue,
+            bytes32 redeliveryHash
+        )
+    {
+        ExecutionInfoVersion instructionExecutionInfoVersion =
+            decodeExecutionInfoVersion(instruction.encodedExecutionInfo);
+        if (instructionExecutionInfoVersion != ExecutionInfoVersion.EVM_V1) {
+            revert UnexpectedExecutionInfoVersion(
+                uint8(instructionExecutionInfoVersion), uint8(ExecutionInfoVersion.EVM_V1)
+            );
+        }
+
+        EvmExecutionInfoV1 memory executionInfo =
+            decodeEvmExecutionInfoV1(instruction.encodedExecutionInfo);
+
+        // If present, apply redelivery deliveryOverrides to current instruction
+        if (encodedOverrides.length != 0) {
+            DeliveryOverride memory deliveryOverrides = encodedOverrides.decodeDeliveryOverride();
+
+            // Check to see if gasLimit >= original gas limit, receiver value >= original receiver value, and refund >= original refund
+            // If so, replace the corresponding variables with the overriden variables
+            // If not, revert
+            (instruction.requestedReceiverValue, executionInfo) = decodeAndCheckOverridesEvmV1(
+                instruction.requestedReceiverValue, executionInfo, deliveryOverrides
+            );
+            instruction.extraReceiverValue = TargetNative.wrap(0);
+            redeliveryHash = deliveryOverrides.redeliveryHash;
+        }
+
+        gasLimit = executionInfo.gasLimit;
+        targetChainRefundPerGasUnused = executionInfo.targetChainRefundPerGasUnused;
+        totalReceiverValue = instruction.requestedReceiverValue + instruction.extraReceiverValue;
+    }
+
+    function decodeAndCheckOverridesEvmV1(
+        TargetNative receiverValue,
+        EvmExecutionInfoV1 memory executionInfo,
+        DeliveryOverride memory deliveryOverrides
+    )
+        internal
+        pure
+        returns (
+            TargetNative deliveryOverridesReceiverValue,
+            EvmExecutionInfoV1 memory deliveryOverridesExecutionInfo
+        )
+    {
+        if (deliveryOverrides.newReceiverValue.unwrap() < receiverValue.unwrap()) {
+            revert InvalidOverrideReceiverValue();
+        }
+
+        ExecutionInfoVersion deliveryOverridesExecutionInfoVersion =
+            decodeExecutionInfoVersion(deliveryOverrides.newExecutionInfo);
+        if (ExecutionInfoVersion.EVM_V1 != deliveryOverridesExecutionInfoVersion) {
+            revert VersionMismatchOverride(
+                uint8(ExecutionInfoVersion.EVM_V1), uint8(deliveryOverridesExecutionInfoVersion)
+            );
+        }
+
+        deliveryOverridesExecutionInfo =
+            decodeEvmExecutionInfoV1(deliveryOverrides.newExecutionInfo);
+        deliveryOverridesReceiverValue = deliveryOverrides.newReceiverValue;
+
+        if (
+            deliveryOverridesExecutionInfo.targetChainRefundPerGasUnused.unwrap()
+                < executionInfo.targetChainRefundPerGasUnused.unwrap()
+        ) {
+            revert InvalidOverrideRefundPerGasUnused();
+        }
+        if (deliveryOverridesExecutionInfo.gasLimit < executionInfo.gasLimit) {
+            revert InvalidOverrideGasLimit();
+        }
+    }
+
+    struct DeliveryResults {
+        Gas gasUsed;
+        DeliveryStatus status;
+        bytes additionalStatusInfo;
+    }
+
+    /**
+     * Performs the following actions:
+     * - Calls the `receiveWormholeMessages` method on the contract
+     *     `vaaInfo.deliveryInstruction.targetAddress` (with the gas limit and value specified in
+     *     vaaInfo.gasLimit and vaaInfo.totalReceiverValue, and `encodedVMs` as the input)
+     *
+     * - Calculates how much gas from `vaaInfo.gasLimit` is left
+     * - If the call succeeded and during execution of `receiveWormholeMessages` there were
+     *     forward(s), and (gas left from vaaInfo.gasLimit) * (vaaInfo.targetChainRefundPerGasUnused) is enough extra funds
+     *     to execute the forward(s), then the forward(s) are executed
+     * - else:
+     *     revert the delivery to trigger a receiver failure (or forward request failure if 
+     *     there were forward(s))
+     *     refund 'vaaInfo.targetChainRefundPerGasUnused'*(amount of vaaInfo.gasLimit unused) to vaaInfo.deliveryInstruction.refundAddress
+     *     if the call reverted, refund `vaaInfo.totalReceiverValue` to vaaInfo.deliveryInstruction.refundAddress
+     * - refund anything leftover to the relayer
+     *
+     * @param vaaInfo struct specifying:
+     *    - sourceChain chain id that the delivery originated from
+     *    - sourceSequence sequence number of the delivery VAA on the source chain
+     *    - deliveryVaaHash hash of delivery VAA
+     *    - relayerRefundAddress address that should be paid for relayer refunds
+     *    - encodedVMs list of signed wormhole messages (VAAs)
+     *    - deliveryInstruction the specific instruction which is being executed
+     *    - gasLimit the gas limit to call targetAddress with
+     *    - targetChainRefundPerGasUnused the amount of (this chain) wei to refund to refundAddress
+     *      per unit of gas unused (from gasLimit)
+     *    - totalReceiverValue the msg.value to call targetAddress with
+     *    - encodedOverrides any (encoded) overrides that were applied
+     *    - (optional) redeliveryHash hash of redelivery Vaa
+     */
+
+    function executeDelivery(DeliveryVAAInfo memory vaaInfo) private {
+
+        // If the targetAddress is the 0 address
+        // Then emit event and return
+        // (This is used for cross-chain refunds)
+        if (checkIfCrossChainRefund(vaaInfo)) {
+            return;
+        }
+
+        DeliveryResults memory results;
+
+        // Forces external call
+        // In order to catch reverts
+        // (If the user's contract requests a forward
+        // and there ends up not being enough funds,
+        // then we will revert this call)
+        try 
+        this.executeInstruction(
+            EvmDeliveryInstruction({
+                sourceChain: vaaInfo.sourceChain,
+                targetAddress: vaaInfo.deliveryInstruction.targetAddress,
+                payload: vaaInfo.deliveryInstruction.payload,
+                gasLimit: vaaInfo.gasLimit,
+                totalReceiverValue: vaaInfo.totalReceiverValue,
+                targetChainRefundPerGasUnused: vaaInfo.targetChainRefundPerGasUnused,
+                senderAddress: vaaInfo.deliveryInstruction.senderAddress,
+                deliveryHash: vaaInfo.deliveryVaaHash,
+                signedVaas: vaaInfo.encodedVMs
+            }
+        )) returns (
+            uint8 _status, Gas _gasUsed, bytes memory targetRevertDataTruncated
+        ) {
+            results = DeliveryResults(
+                _gasUsed,
+                DeliveryStatus(_status),
+                targetRevertDataTruncated
+            );
+        } 
+        catch (bytes memory revertData) {
+            // Should only revert if 
+            // 1) forward(s) were requested but not enough funds were available from the refund
+            // 2) forward(s) were requested, but the delivery provider requested for the forward reverted during execution
+            // 3) forward(s) were requested, but the payment of the delivery provider for the forward failed
+
+            // Decode returned error (into one of these three known types)
+            // obtaining the gas usage of targetAddress
+            bool knownError;
+            Gas gasUsed_;
+            (gasUsed_, knownError) = tryDecodeExecuteInstructionError(revertData);
+            results = DeliveryResults(
+                knownError? gasUsed_ : vaaInfo.gasLimit,
+                DeliveryStatus.FORWARD_REQUEST_FAILURE,
+                revertData
+            );
+        }
+
+        emit Delivery(
+            fromWormholeFormat(vaaInfo.deliveryInstruction.targetAddress),
+            vaaInfo.sourceChain,
+            vaaInfo.sourceSequence,
+            vaaInfo.deliveryVaaHash,
+            results.status,
+            results.gasUsed,
+            payRefunds(
+                vaaInfo.deliveryInstruction,
+                vaaInfo.relayerRefundAddress,
+                (vaaInfo.gasLimit - results.gasUsed).toWei(vaaInfo.targetChainRefundPerGasUnused).asLocalNative(),
+                results.status
+            ),
+            results.additionalStatusInfo,
+            (vaaInfo.redeliveryHash != 0) ? vaaInfo.encodedOverrides : new bytes(0)
+        );
+    }
+
+    function checkIfCrossChainRefund(DeliveryVAAInfo memory vaaInfo)
+        internal
+        returns (bool isCrossChainRefund)
+    {
+        if (vaaInfo.deliveryInstruction.targetAddress == 0x0) {
+            emit Delivery(
+                fromWormholeFormat(vaaInfo.deliveryInstruction.targetAddress),
+                vaaInfo.sourceChain,
+                vaaInfo.sourceSequence,
+                vaaInfo.deliveryVaaHash,
+                DeliveryStatus.SUCCESS,
+                Gas.wrap(0),
+                payRefunds(
+                    vaaInfo.deliveryInstruction,
+                    vaaInfo.relayerRefundAddress,
+                    LocalNative.wrap(0),
+                    DeliveryStatus.RECEIVER_FAILURE
+                ),
+                bytes(""),
+                (vaaInfo.redeliveryHash != 0) ? vaaInfo.encodedOverrides : new bytes(0)
+            );
+            isCrossChainRefund = true;
+        }
+    }
+
+    function executeInstruction(EvmDeliveryInstruction memory evmInstruction)
+        external
+        returns (uint8 status, Gas gasUsed, bytes memory targetRevertDataTruncated)
+    {
+        //  despite being external, we only allow ourselves to call this function (via CALL opcode)
+        //  used as a means to retroactively revert the call to the delivery target if the forwards
+        //  can't be funded
+        if (msg.sender != address(this)) {
+            revert RequesterNotWormholeRelayer();
+        }
+
+        Gas gasLimit = evmInstruction.gasLimit;
+        bool success;
+        {
+            address payable deliveryTarget = payable(fromWormholeFormat(evmInstruction.targetAddress));
+            bytes memory callData = abi.encodeCall(IWormholeReceiver.receiveWormholeMessages, (
+                evmInstruction.payload,
+                evmInstruction.signedVaas,
+                evmInstruction.senderAddress,
+                evmInstruction.sourceChain,
+                evmInstruction.deliveryHash
+            ));
+
+            // Measure gas usage of call
+            Gas preGas = Gas.wrap(gasleft());
+
+            // Calls the `receiveWormholeMessages` endpoint on the contract `evmInstruction.targetAddress`
+            // (with the gas limit and value specified in instruction, and `encodedVMs` as the input)
+            // If it reverts, returns the first 132 bytes of the revert message
+            (success, targetRevertDataTruncated) = returnLengthBoundedCall(
+                deliveryTarget,
+                callData,
+                gasLimit.unwrap(),
+                evmInstruction.totalReceiverValue.unwrap(),
+                RETURNDATA_TRUNCATION_THRESHOLD
+            );
+
+            Gas postGas = Gas.wrap(gasleft());
+
+            unchecked {
+                gasUsed = (preGas - postGas).min(gasLimit);
+            }
+        }
+
+        if (success) {
+            targetRevertDataTruncated = new bytes(0);
+            status = uint8(DeliveryStatus.SUCCESS);
+
+            ForwardInstruction[] storage forwardInstructions = getForwardInstructions();
+
+            if (forwardInstructions.length > 0) {
+                // forward(s) were requested during execution of the call to targetAddress above
+
+                // Calculate the amount to refund the user for unused gas
+                LocalNative transactionFeeRefundAmount = (gasLimit - gasUsed).toWei(
+                    evmInstruction.targetChainRefundPerGasUnused
+                ).asLocalNative();
+
+                // Check if refund amount is enough, and if so, emit forwards  
+                emitForward(gasUsed, transactionFeeRefundAmount, forwardInstructions);
+
+                // If we reach here (i.e. emitForward didn't revert) then the forward succeeded
+                status = uint8(DeliveryStatus.FORWARD_REQUEST_SUCCESS);
+            }
+        } else {
+            // Call to 'receiveWormholeMessages' on targetAddress reverted
+            status = uint8(DeliveryStatus.RECEIVER_FAILURE);
+        }
+    }
+
+    /**
+     * - Checks if enough funds were passed into a forward (and reverts if not)
+     * - Increases the 'extraReceiverValue' of the first forward in order to use all of the funds
+     * - Publishes the DeliveryInstruction
+     * - Pays the relayer's reward address to deliver the forward
+     *
+     * @param transactionFeeRefundAmount amount of maxTransactionFee that was unused
+     * @param forwardInstructions An array of structs containing information about the user's forward
+     *     request(s)
+     */
+    function emitForward(
+        Gas gasUsed,
+        LocalNative transactionFeeRefundAmount,
+        ForwardInstruction[] storage forwardInstructions
+    ) private {
+        LocalNative wormholeMessageFee = getWormholeMessageFee();
+
+        // Decode delivery instructions from each 'forward' request
+        DeliveryInstruction[] memory instructions =
+            new DeliveryInstruction[](forwardInstructions.length);
+
+        // Calculate total msg.value passed into all 'forward' requests
+        LocalNative totalMsgValue;
+        // Calculate total fee for all 'forward' requests
+        LocalNative totalFee;
+
+        for (uint256 i = 0; i < forwardInstructions.length;) {
+            unchecked {
+                totalMsgValue = totalMsgValue + forwardInstructions[i].msgValue;
+            }
+            instructions[i] =
+                (forwardInstructions[i].encodedInstruction).decodeDeliveryInstruction();
+            totalFee = totalFee + forwardInstructions[i].deliveryPrice
+                + forwardInstructions[i].paymentForExtraReceiverValue + wormholeMessageFee;
+            unchecked {
+                ++i;
+            }
+        }
+
+        //  Combine refund amount with any additional funds which were passed in to the forward as
+        //  msg.value and check that this value is enough to fund all the forwards, reverting otherwise
+        LocalNative fundsForForward;
+        unchecked {
+            fundsForForward = transactionFeeRefundAmount + totalMsgValue;
+        }
+        if (fundsForForward.unwrap() < totalFee.unwrap()) {
+            revert Cancelled(gasUsed, fundsForForward, totalFee);
+        }
+        
+        // Simulates if the user had increased the 'paymentForExtraReceiverValue' field of their first forward
+        // to the maximum value such that the forward(s) would still have enough funds
+
+        TargetNative extraReceiverValue;
+        try IDeliveryProvider(
+            fromWormholeFormat(instructions[0].sourceDeliveryProvider)
+        ).quoteAssetConversion(instructions[0].targetChain, fundsForForward - totalFee)
+        returns (TargetNative _extraReceiverValue) {
+            extraReceiverValue = _extraReceiverValue;
+        } catch {
+            revert DeliveryProviderReverted(gasUsed);
+        }
+        
+        unchecked {
+            instructions[0].extraReceiverValue =
+                instructions[0].extraReceiverValue + extraReceiverValue;
+        }
+
+        //Publishes the DeliveryInstruction(s) for each forward request and pays the associated deliveryProvider
+        for (uint256 i = 0; i < forwardInstructions.length;) {
+            (, bool paymentSucceeded) = publishAndPay(
+                wormholeMessageFee,
+                forwardInstructions[i].deliveryPrice,
+                // We had increased the 'paymentForExtraReceiverValue' of the first forward
+                forwardInstructions[i].paymentForExtraReceiverValue
+                    + ((i == 0) ? (fundsForForward - totalFee) : LocalNative.wrap(0)),
+                i == 0 ? instructions[0].encode() : forwardInstructions[i].encodedInstruction,
+                forwardInstructions[i].consistencyLevel,
+                forwardInstructions[i].rewardAddress
+            );
+            if (!paymentSucceeded) {
+                revert DeliveryProviderPaymentFailed(gasUsed);
+            }
+            unchecked {
+                ++i;
+            }
+        }
+    }
+
+    function payRefunds(
+        DeliveryInstruction memory deliveryInstruction,
+        address payable relayerRefundAddress,
+        LocalNative transactionFeeRefundAmount,
+        DeliveryStatus status
+    ) private returns (RefundStatus refundStatus) {
+        //Amount of receiverValue that is refunded to the user (0 if the call to
+        //  'receiveWormholeMessages' did not revert, or the full receiverValue otherwise)
+        LocalNative receiverValueRefundAmount = LocalNative.wrap(0);
+
+        if (
+            status == DeliveryStatus.FORWARD_REQUEST_FAILURE
+                || status == DeliveryStatus.RECEIVER_FAILURE
+        ) {
+            receiverValueRefundAmount = (
+                deliveryInstruction.requestedReceiverValue + deliveryInstruction.extraReceiverValue
+            ).asNative().asLocalNative(); // NOTE: instruction's target is delivery's local
+        }
+
+        // Total refund to the user
+        // (If the forward succeeded, the 'transactionFeeRefundAmount' was used there already)
+        LocalNative refundToRefundAddress = receiverValueRefundAmount
+            + (
+                status == DeliveryStatus.FORWARD_REQUEST_SUCCESS
+                    ? LocalNative.wrap(0)
+                    : transactionFeeRefundAmount
+            );
+
+        //Refund the user
+        refundStatus = payRefundToRefundAddress(
+            deliveryInstruction.refundChain,
+            deliveryInstruction.refundAddress,
+            refundToRefundAddress,
+            deliveryInstruction.refundDeliveryProvider
+        );
+
+        //If sending the user's refund failed, this gets added to the relayer's refund
+        LocalNative leftoverUserRefund = refundToRefundAddress;
+        if (
+            refundStatus == RefundStatus.REFUND_SENT
+                || refundStatus == RefundStatus.CROSS_CHAIN_REFUND_SENT
+        ) {
+            leftoverUserRefund = LocalNative.wrap(0);
+        }
+
+        // Refund the relayer all remaining funds
+        LocalNative relayerRefundAmount = calcRelayerRefundAmount(deliveryInstruction, transactionFeeRefundAmount, leftoverUserRefund);
+
+        bool paymentSucceeded = pay(relayerRefundAddress, relayerRefundAmount);
+        if(!paymentSucceeded) {
+            revert DeliveryProviderCannotReceivePayment();
+        }
+    }
+
+    function calcRelayerRefundAmount(
+        DeliveryInstruction memory deliveryInstruction,
+        LocalNative transactionFeeRefundAmount,
+        LocalNative leftoverUserRefund
+    ) private view returns (LocalNative) {
+        return msgValue()
+            // Note: instruction's target is delivery's local
+            - (deliveryInstruction.requestedReceiverValue + deliveryInstruction.extraReceiverValue).asNative().asLocalNative() 
+            - transactionFeeRefundAmount + leftoverUserRefund;
+    }
+
+    function payRefundToRefundAddress(
+        uint16 refundChain,
+        bytes32 refundAddress,
+        LocalNative refundAmount,
+        bytes32 relayerAddress
+    ) private returns (RefundStatus) {
+        // User requested refund on this chain
+        if (refundChain == getChainId()) {
+            return pay(payable(fromWormholeFormat(refundAddress)), refundAmount)
+                ? RefundStatus.REFUND_SENT
+                : RefundStatus.REFUND_FAIL;
+        }
+
+        // User requested refund on a different chain
+        
+        IDeliveryProvider deliveryProvider = IDeliveryProvider(fromWormholeFormat(relayerAddress));
+        
+        // Determine price of an 'empty' delivery
+        // (Note: assumes refund chain is an EVM chain)
+        LocalNative baseDeliveryPrice;
+      
+        try deliveryProvider.quoteDeliveryPrice(
+            refundChain,
+            TargetNative.wrap(0),
+            encodeEvmExecutionParamsV1(getEmptyEvmExecutionParamsV1())
+        ) returns (LocalNative quote, bytes memory) {
+            baseDeliveryPrice = quote;
+        } catch (bytes memory) {
+            return RefundStatus.CROSS_CHAIN_REFUND_FAIL_PROVIDER_NOT_SUPPORTED;
+        }
+
+        // If the refundAmount is not greater than the 'empty delivery price', the refund does not go through
+        if (refundAmount <= getWormholeMessageFee() + baseDeliveryPrice) {
+            return RefundStatus.CROSS_CHAIN_REFUND_FAIL_NOT_ENOUGH;
+        }
+        
+        // Request a 'send' with 'paymentForExtraReceiverValue' equal to the refund minus the 'empty delivery price'
+        try IWormholeRelayerSend(address(this)).send{value: refundAmount.unwrap()}(
+            refundChain,
+            bytes32(0),
+            bytes(""),
+            TargetNative.wrap(0),
+            refundAmount - getWormholeMessageFee() - baseDeliveryPrice,
+            encodeEvmExecutionParamsV1(getEmptyEvmExecutionParamsV1()),
+            refundChain,
+            refundAddress,
+            fromWormholeFormat(relayerAddress),
+            new VaaKey[](0),
+            CONSISTENCY_LEVEL_INSTANT
+        ) returns (uint64) {
+            return RefundStatus.CROSS_CHAIN_REFUND_SENT;
+        } catch (bytes memory) {
+            return RefundStatus.CROSS_CHAIN_REFUND_FAIL_PROVIDER_NOT_SUPPORTED;
+        }
+    }
+
+    function tryDecodeExecuteInstructionError(
+        bytes memory revertData
+    ) private pure returns (Gas gasUsed, bool knownError) {
+        uint offset = 0;
+        bytes4 selector;
+        // Check to see if the following decode can be performed
+        if(revertData.length < 36) {
+            return (Gas.wrap(0), false);
+        }
+        (selector, offset) = revertData.asBytes4Unchecked(offset);
+        if((selector == Cancelled.selector) || (selector == DeliveryProviderReverted.selector) || (selector == DeliveryProviderPaymentFailed.selector)) {
+            knownError = true;
+            uint256 _gasUsed;
+            (_gasUsed, offset) = revertData.asUint256Unchecked(offset);
+            gasUsed = Gas.wrap(_gasUsed);
+        }
+    }
+
+    function checkVaaKeysWithVAAs(
+        VaaKey[] memory vaaKeys,
+        bytes[] memory signedVaas
+    ) private view {
+        if (vaaKeys.length != signedVaas.length) {
+            revert VaaKeysLengthDoesNotMatchVaasLength(vaaKeys.length, signedVaas.length);
+        }
+
+        for (uint256 i = 0; i < vaaKeys.length;) {
+            IWormhole.VM memory parsedVaa = getWormhole().parseVM(signedVaas[i]);
+            VaaKey memory vaaKey = vaaKeys[i];
+            
+            if (
+                vaaKey.chainId != parsedVaa.emitterChainId
+                    || vaaKey.emitterAddress != parsedVaa.emitterAddress
+                    || vaaKey.sequence != parsedVaa.sequence
+            ) {
+                revert VaaKeysDoNotMatchVaas(uint8(i));
+            }
+
+            unchecked {
+                ++i;
+            }
+        }
+    }
+}

+ 248 - 0
ethereum/contracts/relayer/wormholeRelayer/WormholeRelayerGovernance.sol

@@ -0,0 +1,248 @@
+// SPDX-License-Identifier: Apache 2
+pragma solidity ^0.8.19;
+
+import "@openzeppelin/contracts/proxy/ERC1967/ERC1967Upgrade.sol";
+
+import {IWormhole} from "../../interfaces/IWormhole.sol";
+import {InvalidPayloadLength} from "../../interfaces/relayer/IWormholeRelayerTyped.sol";
+import {fromWormholeFormat} from "../../libraries/relayer/Utils.sol";
+import {BytesParsing} from "../../libraries/relayer/BytesParsing.sol";
+import {
+    getGovernanceState,
+    getRegisteredWormholeRelayersState,
+    getDefaultDeliveryProviderState
+} from "./WormholeRelayerStorage.sol";
+import {WormholeRelayerBase} from "./WormholeRelayerBase.sol";
+
+error GovernanceActionAlreadyConsumed(bytes32 hash);
+error InvalidGovernanceVM(string reason);
+error InvalidGovernanceChainId(uint16 parsed, uint16 expected);
+error InvalidGovernanceContract(bytes32 parsed, bytes32 expected);
+
+error InvalidPayloadChainId(uint16 parsed, uint16 expected);
+error InvalidPayloadAction(uint8 parsed, uint8 expected);
+error InvalidPayloadModule(bytes32 parsed, bytes32 expected);
+error InvalidFork();
+error ContractUpgradeFailed(bytes failure);
+error ChainAlreadyRegistered(uint16 chainId, bytes32 registeredWormholeRelayerContract);
+error InvalidDefaultDeliveryProvider(bytes32 defaultDeliveryProvider);
+
+abstract contract WormholeRelayerGovernance is WormholeRelayerBase, ERC1967Upgrade {
+    //This constant should actually be defined in IWormhole. Alas, it isn't.
+    uint16 private constant WORMHOLE_CHAINID_UNSET = 0;
+
+    /**
+     * Governance VMs are encoded in a packed fashion using the general wormhole scheme:
+     *   GovernancePacket = <Common Header|Action Parameters>
+     *
+     * For a more detailed explanation see here:
+     *   - https://docs.wormhole.com/wormhole/governance
+     *   - https://github.com/wormhole-foundation/wormhole/blob/main/whitepapers/0002_governance_messaging.md
+     */
+
+    //Right shifted ascii encoding of "WormholeRelayer"
+    bytes32 private constant module =
+        0x0000000000000000000000000000000000576f726d686f6c6552656c61796572;
+
+    /**
+     * The choice of action enumeration and parameters follows the scheme of the core bridge:
+     *   - https://github.com/wormhole-foundation/wormhole/blob/main/ethereum/contracts/bridge/BridgeGovernance.sol#L115
+     */
+
+    /**
+     * Registers a wormhole relayer contract that was deployed on another chain with the WormholeRelayer on
+     *   this chain. The equivalent to the core bridge's registerChain action.
+     *
+     * Action Parameters:
+     *   - uint16 foreignChainId 
+     *   - bytes32 foreignContractAddress 
+     */
+    uint8 private constant GOVERNANCE_ACTION_REGISTER_WORMHOLE_RELAYER_CONTRACT = 1;
+
+    /**
+     * Upgrades the WormholeRelayer contract to a new implementation. The equivalent to the core bridge's
+     *   upgrade action.
+     *
+     * Action Parameters:
+     *   - bytes32 newImplementation
+     */
+    uint8 private constant GOVERNANCE_ACTION_CONTRACT_UPGRADE = 2;
+
+    /**
+     * Sets the default relay provider for the WormholeRelayer. Has no equivalent in the core bridge.
+     *
+     * Action Parameters:
+     *   - bytes32 newProvider 
+     */
+    uint8 private constant GOVERNANCE_ACTION_UPDATE_DEFAULT_PROVIDER = 3;
+
+    //By checking that only the contract can call itself, we can enforce that the migration code is
+    //  executed upon program upgrade and that it can't be called externally by anyone else.
+    function checkAndExecuteUpgradeMigration() external {
+        assert(msg.sender == address(this));
+        executeUpgradeMigration();
+    }
+
+    function executeUpgradeMigration() internal virtual {
+        //override and implement in WormholeRelayer upon contract upgrade (if required)
+    }
+
+    function registerWormholeRelayerContract(bytes memory encodedVm) external {
+        (uint16 foreignChainId, bytes32 foreignAddress) =
+            parseAndCheckRegisterWormholeRelayerContractVm(encodedVm);
+
+        getRegisteredWormholeRelayersState().registeredWormholeRelayers[foreignChainId] =
+            foreignAddress;
+    }
+
+    event ContractUpgraded(address indexed oldContract, address indexed newContract);
+
+    function submitContractUpgrade(bytes memory encodedVm) external {
+
+        address currentImplementation = _getImplementation();
+        address newImplementation = parseAndCheckContractUpgradeVm(encodedVm);
+
+        _upgradeTo(newImplementation);
+
+        (bool success, bytes memory revertData) =
+            address(this).call(abi.encodeCall(this.checkAndExecuteUpgradeMigration, ()));
+
+        if (!success) {
+            revert ContractUpgradeFailed(revertData);
+        }
+
+         emit ContractUpgraded(currentImplementation, newImplementation);
+    }
+
+    function setDefaultDeliveryProvider(bytes memory encodedVm) external {
+        address newProvider = parseAndCheckRegisterDefaultDeliveryProviderVm(encodedVm);
+
+        getDefaultDeliveryProviderState().defaultDeliveryProvider = newProvider;
+    }
+
+    // ------------------------------------------- PRIVATE -------------------------------------------
+    using BytesParsing for bytes;
+
+    function parseAndCheckRegisterWormholeRelayerContractVm(bytes memory encodedVm)
+        private
+        returns (uint16 foreignChainId, bytes32 foreignAddress)
+    {
+        bytes memory payload = verifyAndConsumeGovernanceVM(encodedVm);
+        uint256 offset = parseAndCheckPayloadHeader(
+            payload, GOVERNANCE_ACTION_REGISTER_WORMHOLE_RELAYER_CONTRACT, true
+        );
+
+        (foreignChainId, offset) = payload.asUint16Unchecked(offset);
+        (foreignAddress, offset) = payload.asBytes32Unchecked(offset);
+
+        checkLength(payload, offset);
+
+        if(getRegisteredWormholeRelayerContract(foreignChainId) != bytes32(0)) {
+            revert ChainAlreadyRegistered(foreignChainId, getRegisteredWormholeRelayerContract(foreignChainId));
+        }
+
+    }
+
+    function parseAndCheckContractUpgradeVm(bytes memory encodedVm)
+        private
+        returns (address newImplementation)
+    {
+        bytes memory payload = verifyAndConsumeGovernanceVM(encodedVm);
+        uint256 offset =
+            parseAndCheckPayloadHeader(payload, GOVERNANCE_ACTION_CONTRACT_UPGRADE, false);
+
+        bytes32 newImplementationWhFmt;
+        (newImplementationWhFmt, offset) = payload.asBytes32Unchecked(offset);
+        //fromWormholeFormat reverts if first 12 bytes aren't zero (i.e. if it's not an EVM address)
+        newImplementation = fromWormholeFormat(newImplementationWhFmt);
+
+        checkLength(payload, offset);
+    }
+
+    function parseAndCheckRegisterDefaultDeliveryProviderVm(bytes memory encodedVm)
+        private
+        returns (address newProvider)
+    {
+        bytes memory payload = verifyAndConsumeGovernanceVM(encodedVm);
+        uint256 offset =
+            parseAndCheckPayloadHeader(payload, GOVERNANCE_ACTION_UPDATE_DEFAULT_PROVIDER, false);
+
+        bytes32 newProviderWhFmt;
+        (newProviderWhFmt, offset) = payload.asBytes32Unchecked(offset);
+        //fromWormholeFormat reverts if first 12 bytes aren't zero (i.e. if it's not an EVM address)
+        newProvider = fromWormholeFormat(newProviderWhFmt);
+
+        checkLength(payload, offset);
+
+        if(newProvider == address(0)) {
+            revert InvalidDefaultDeliveryProvider(newProviderWhFmt);
+        }
+    }
+
+    function verifyAndConsumeGovernanceVM(bytes memory encodedVm)
+        private
+        returns (bytes memory payload)
+    {
+        (IWormhole.VM memory vm, bool valid, string memory reason) =
+            getWormhole().parseAndVerifyVM(encodedVm);
+
+        if (!valid) {
+            revert InvalidGovernanceVM(reason);
+        }
+
+        uint16 governanceChainId = getWormhole().governanceChainId();
+        if (vm.emitterChainId != governanceChainId) {
+            revert InvalidGovernanceChainId(vm.emitterChainId, governanceChainId);
+        }
+
+        bytes32 governanceContract = getWormhole().governanceContract();
+        if (vm.emitterAddress != governanceContract) {
+            revert InvalidGovernanceContract(vm.emitterAddress, governanceContract);
+        }
+
+        bool consumed = getGovernanceState().consumedGovernanceActions[vm.hash];
+        if (consumed) {
+            revert GovernanceActionAlreadyConsumed(vm.hash);
+        }
+
+        getGovernanceState().consumedGovernanceActions[vm.hash] = true;
+
+        return vm.payload;
+    }
+
+    function parseAndCheckPayloadHeader(
+        bytes memory encodedPayload,
+        uint8 expectedAction,
+        bool allowUnset
+    ) private view returns (uint256 offset) {
+        bytes32 parsedModule;
+        (parsedModule, offset) = encodedPayload.asBytes32Unchecked(offset);
+        if (parsedModule != module) {
+            revert InvalidPayloadModule(parsedModule, module);
+        }
+
+        uint8 parsedAction;
+        (parsedAction, offset) = encodedPayload.asUint8Unchecked(offset);
+        if (parsedAction != expectedAction) {
+            revert InvalidPayloadAction(parsedAction, expectedAction);
+        }
+
+        uint16 parsedChainId;
+        (parsedChainId, offset) = encodedPayload.asUint16Unchecked(offset);
+        if (!(parsedChainId == WORMHOLE_CHAINID_UNSET && allowUnset)) {
+            if (getWormhole().isFork()) {
+                revert InvalidFork();
+            }
+
+            if (parsedChainId != getChainId()) {
+                revert InvalidPayloadChainId(parsedChainId, getChainId());
+            }
+        }
+    }
+
+    function checkLength(bytes memory payload, uint256 expected) private pure {
+        if (payload.length != expected) {
+            revert InvalidPayloadLength(payload.length, expected);
+        }
+    }
+}

+ 571 - 0
ethereum/contracts/relayer/wormholeRelayer/WormholeRelayerSend.sol

@@ -0,0 +1,571 @@
+// SPDX-License-Identifier: Apache 2
+
+pragma solidity ^0.8.19;
+
+import {
+    DeliveryProviderDoesNotSupportTargetChain,
+    InvalidMsgValue,
+    DeliveryProviderCannotReceivePayment,
+    VaaKey,
+    IWormholeRelayerSend
+} from "../../interfaces/relayer/IWormholeRelayerTyped.sol";
+import {IDeliveryProvider} from "../../interfaces/relayer/IDeliveryProviderTyped.sol";
+
+import {toWormholeFormat, fromWormholeFormat} from "../../libraries/relayer/Utils.sol";
+import {
+    DeliveryInstruction,
+    RedeliveryInstruction
+} from "../../libraries/relayer/RelayerInternalStructs.sol";
+import {WormholeRelayerSerde} from "./WormholeRelayerSerde.sol";
+import {ForwardInstruction, getDefaultDeliveryProviderState} from "./WormholeRelayerStorage.sol";
+import {WormholeRelayerBase} from "./WormholeRelayerBase.sol";
+import "../../interfaces/relayer/TypedUnits.sol";
+import "../../libraries/relayer/ExecutionParameters.sol";
+
+abstract contract WormholeRelayerSend is WormholeRelayerBase, IWormholeRelayerSend {
+    using WormholeRelayerSerde for *; 
+    using WeiLib for Wei;
+    using GasLib for Gas;
+    using TargetNativeLib for TargetNative;
+    using LocalNativeLib for LocalNative;
+
+    /*
+    * Public convenience overloads
+    */
+
+    function sendPayloadToEvm(
+        uint16 targetChain,
+        address targetAddress,
+        bytes memory payload,
+        TargetNative receiverValue,
+        Gas gasLimit
+    ) external payable returns (uint64 sequence) {
+        return sendToEvm(
+            targetChain,
+            targetAddress,
+            payload,
+            receiverValue,
+            LocalNative.wrap(0),
+            gasLimit,
+            targetChain,
+            getDefaultDeliveryProviderOnChain(targetChain),
+            getDefaultDeliveryProvider(),
+            new VaaKey[](0),
+            CONSISTENCY_LEVEL_FINALIZED
+        );
+    }
+
+    function sendPayloadToEvm(
+        uint16 targetChain,
+        address targetAddress,
+        bytes memory payload,
+        TargetNative receiverValue,
+        Gas gasLimit,
+        uint16 refundChain,
+        address refundAddress
+    ) external payable returns (uint64 sequence) {
+        return sendToEvm(
+            targetChain,
+            targetAddress,
+            payload,
+            receiverValue,
+            LocalNative.wrap(0),
+            gasLimit,
+            refundChain,
+            refundAddress,
+            getDefaultDeliveryProvider(),
+            new VaaKey[](0),
+            CONSISTENCY_LEVEL_FINALIZED
+        );
+    }
+
+    function sendVaasToEvm(
+        uint16 targetChain,
+        address targetAddress,
+        bytes memory payload,
+        TargetNative receiverValue,
+        Gas gasLimit,
+        VaaKey[] memory vaaKeys
+    ) external payable returns (uint64 sequence) {
+        return sendToEvm(
+            targetChain,
+            targetAddress,
+            payload,
+            receiverValue,
+            LocalNative.wrap(0),
+            gasLimit,
+            targetChain,
+            getDefaultDeliveryProviderOnChain(targetChain),
+            getDefaultDeliveryProvider(),
+            vaaKeys,
+            CONSISTENCY_LEVEL_FINALIZED
+        );
+    }
+
+    function sendVaasToEvm(
+        uint16 targetChain,
+        address targetAddress,
+        bytes memory payload,
+        TargetNative receiverValue,
+        Gas gasLimit,
+        VaaKey[] memory vaaKeys,
+        uint16 refundChain,
+        address refundAddress
+    ) external payable returns (uint64 sequence) {
+        return sendToEvm(
+            targetChain,
+            targetAddress,
+            payload,
+            receiverValue,
+            LocalNative.wrap(0),
+            gasLimit,
+            refundChain,
+            refundAddress,
+            getDefaultDeliveryProvider(),
+            vaaKeys,
+            CONSISTENCY_LEVEL_FINALIZED
+        );
+    }
+
+    function sendToEvm(
+        uint16 targetChain,
+        address targetAddress,
+        bytes memory payload,
+        TargetNative receiverValue,
+        LocalNative paymentForExtraReceiverValue,
+        Gas gasLimit,
+        uint16 refundChain,
+        address refundAddress,
+        address deliveryProviderAddress,
+        VaaKey[] memory vaaKeys,
+        uint8 consistencyLevel
+    ) public payable returns (uint64 sequence) {
+        sequence = send(
+            targetChain,
+            toWormholeFormat(targetAddress),
+            payload,
+            receiverValue,
+            paymentForExtraReceiverValue,
+            encodeEvmExecutionParamsV1(EvmExecutionParamsV1(gasLimit)),
+            refundChain,
+            toWormholeFormat(refundAddress),
+            deliveryProviderAddress,
+            vaaKeys,
+            consistencyLevel
+        );
+    }
+
+    function forwardPayloadToEvm(
+        uint16 targetChain,
+        address targetAddress,
+        bytes memory payload,
+        TargetNative receiverValue,
+        Gas gasLimit
+    ) external payable {
+        (address deliveryProvider,) =
+            getOriginalOrDefaultDeliveryProvider(targetChain);
+        forward(
+            targetChain,
+            toWormholeFormat(targetAddress),
+            payload,
+            receiverValue,
+            LocalNative.wrap(0),
+            encodeEvmExecutionParamsV1(EvmExecutionParamsV1(gasLimit)),
+            getCurrentRefundChain(),
+            getCurrentRefundAddress(),
+            deliveryProvider,
+            new VaaKey[](0),
+            CONSISTENCY_LEVEL_FINALIZED
+        );
+    }
+
+    function forwardVaasToEvm(
+        uint16 targetChain,
+        address targetAddress,
+        bytes memory payload,
+        TargetNative receiverValue,
+        Gas gasLimit,
+        VaaKey[] memory vaaKeys
+    ) external payable {
+        (address deliveryProvider,) =
+            getOriginalOrDefaultDeliveryProvider(targetChain);
+        forward(
+            targetChain,
+            toWormholeFormat(targetAddress),
+            payload,
+            receiverValue,
+            LocalNative.wrap(0),
+            encodeEvmExecutionParamsV1(EvmExecutionParamsV1(gasLimit)),
+            getCurrentRefundChain(),
+            getCurrentRefundAddress(),
+            deliveryProvider,
+            vaaKeys,
+            CONSISTENCY_LEVEL_FINALIZED
+        );
+    }
+
+    function forwardToEvm(
+        uint16 targetChain,
+        address targetAddress,
+        bytes memory payload,
+        TargetNative receiverValue,
+        LocalNative paymentForExtraReceiverValue,
+        Gas gasLimit,
+        uint16 refundChain,
+        address refundAddress,
+        address deliveryProviderAddress,
+        VaaKey[] memory vaaKeys,
+        uint8 consistencyLevel
+    ) public payable {
+        // provide ability to use original relay provider
+        if (deliveryProviderAddress == address(0)) {
+            deliveryProviderAddress = getOriginalDeliveryProvider();
+        }
+
+        forward(
+            targetChain,
+            toWormholeFormat(targetAddress),
+            payload,
+            receiverValue,
+            paymentForExtraReceiverValue,
+            encodeEvmExecutionParamsV1(EvmExecutionParamsV1(gasLimit)),
+            refundChain,
+            toWormholeFormat(refundAddress),
+            deliveryProviderAddress,
+            vaaKeys,
+            consistencyLevel
+        );
+    }
+
+    function resendToEvm(
+        VaaKey memory deliveryVaaKey,
+        uint16 targetChain,
+        TargetNative newReceiverValue,
+        Gas newGasLimit,
+        address newDeliveryProviderAddress
+    ) public payable returns (uint64 sequence) {
+        sequence = resend(
+            deliveryVaaKey,
+            targetChain,
+            newReceiverValue,
+            encodeEvmExecutionParamsV1(EvmExecutionParamsV1(newGasLimit)),
+            newDeliveryProviderAddress
+        );
+    }
+
+    function send(
+        uint16 targetChain,
+        bytes32 targetAddress,
+        bytes memory payload,
+        TargetNative receiverValue,
+        LocalNative paymentForExtraReceiverValue,
+        bytes memory encodedExecutionParameters,
+        uint16 refundChain,
+        bytes32 refundAddress,
+        address deliveryProviderAddress,
+        VaaKey[] memory vaaKeys,
+        uint8 consistencyLevel
+    ) public payable returns (uint64 sequence) {
+        sequence = send(
+            Send(
+                targetChain,
+                targetAddress,
+                payload,
+                receiverValue,
+                paymentForExtraReceiverValue,
+                encodedExecutionParameters,
+                refundChain,
+                refundAddress,
+                deliveryProviderAddress,
+                vaaKeys,
+                consistencyLevel
+            )
+        );
+    }
+
+    function forward(
+        uint16 targetChain,
+        bytes32 targetAddress,
+        bytes memory payload,
+        TargetNative receiverValue,
+        LocalNative paymentForExtraReceiverValue,
+        bytes memory encodedExecutionParameters,
+        uint16 refundChain,
+        bytes32 refundAddress,
+        address deliveryProviderAddress,
+        VaaKey[] memory vaaKeys,
+        uint8 consistencyLevel
+    ) public payable {
+        forward(
+            Send(
+                targetChain,
+                targetAddress,
+                payload,
+                receiverValue,
+                paymentForExtraReceiverValue,
+                encodedExecutionParameters,
+                refundChain,
+                refundAddress,
+                deliveryProviderAddress,
+                vaaKeys,
+                consistencyLevel
+            )
+        );
+    }
+
+    /* 
+    * Non overload logic 
+    */
+
+    struct Send {
+        uint16 targetChain;
+        bytes32 targetAddress;
+        bytes payload;
+        TargetNative receiverValue;
+        LocalNative paymentForExtraReceiverValue;
+        bytes encodedExecutionParameters;
+        uint16 refundChain;
+        bytes32 refundAddress;
+        address deliveryProviderAddress;
+        VaaKey[] vaaKeys;
+        uint8 consistencyLevel;
+    }
+
+    function send(Send memory sendParams) internal returns (uint64 sequence) {
+
+        IDeliveryProvider provider = IDeliveryProvider(sendParams.deliveryProviderAddress);
+
+        // Revert if delivery provider does not support the target chain
+        if (!provider.isChainSupported(sendParams.targetChain)) {
+            revert DeliveryProviderDoesNotSupportTargetChain(
+                sendParams.deliveryProviderAddress, sendParams.targetChain
+            );
+        }
+
+        // Obtain the delivery provider's fee for this delivery, as well as some encoded info (e.g. refund per unit of gas unused)
+        (LocalNative deliveryPrice, bytes memory encodedExecutionInfo) = provider.quoteDeliveryPrice(
+            sendParams.targetChain, sendParams.receiverValue, sendParams.encodedExecutionParameters
+        );
+
+        // Check if user passed in 'one wormhole message fee' + 'delivery provider's fee'
+        LocalNative wormholeMessageFee = getWormholeMessageFee();
+        checkMsgValue(wormholeMessageFee, deliveryPrice, sendParams.paymentForExtraReceiverValue);
+
+        // Encode all relevant info the delivery provider needs to perform the delivery as requested
+        bytes memory encodedInstruction = DeliveryInstruction({
+            targetChain: sendParams.targetChain,
+            targetAddress: sendParams.targetAddress,
+            payload: sendParams.payload,
+            requestedReceiverValue: sendParams.receiverValue,
+            extraReceiverValue: provider.quoteAssetConversion(
+                sendParams.targetChain, sendParams.paymentForExtraReceiverValue
+                ),
+            encodedExecutionInfo: encodedExecutionInfo,
+            refundChain: sendParams.refundChain,
+            refundAddress: sendParams.refundAddress,
+            refundDeliveryProvider: provider.getTargetChainAddress(sendParams.targetChain),
+            sourceDeliveryProvider: toWormholeFormat(sendParams.deliveryProviderAddress),
+            senderAddress: toWormholeFormat(msg.sender),
+            vaaKeys: sendParams.vaaKeys
+        }).encode();
+
+        // Publish the encoded delivery instruction as a wormhole message
+        // and pay the delivery provider their fee
+        bool paymentSucceeded;
+        (sequence, paymentSucceeded) = publishAndPay(
+            wormholeMessageFee,
+            deliveryPrice,
+            sendParams.paymentForExtraReceiverValue,
+            encodedInstruction,
+            sendParams.consistencyLevel,
+            provider.getRewardAddress()
+        );
+
+        if(!paymentSucceeded) 
+            revert DeliveryProviderCannotReceivePayment();
+    }
+
+    function forward(Send memory sendParams) internal {
+
+        // Revert if a delivery with targetAddress == msg.sender is not currently in progress
+        checkMsgSenderInDelivery();
+
+        IDeliveryProvider provider = IDeliveryProvider(sendParams.deliveryProviderAddress);
+
+        // Revert if delivery provider does not support the target chain
+        if (!provider.isChainSupported(sendParams.targetChain)) {
+            revert DeliveryProviderDoesNotSupportTargetChain(
+                sendParams.deliveryProviderAddress, sendParams.targetChain
+            );
+        }
+
+        // Obtain the delivery provider's fee for this delivery, as well as some encoded info (e.g. refund per unit of gas unused)
+        (LocalNative deliveryPrice, bytes memory encodedExecutionInfo) = provider.quoteDeliveryPrice(
+            sendParams.targetChain, sendParams.receiverValue, sendParams.encodedExecutionParameters
+        );
+
+        // Encode all relevant info the delivery provider needs to perform this delivery as requested
+        bytes memory encodedInstruction = DeliveryInstruction({
+            targetChain: sendParams.targetChain,
+            targetAddress: sendParams.targetAddress,
+            payload: sendParams.payload,
+            requestedReceiverValue: sendParams.receiverValue,
+            extraReceiverValue: provider.quoteAssetConversion(
+                sendParams.targetChain, sendParams.paymentForExtraReceiverValue
+                ),
+            encodedExecutionInfo: encodedExecutionInfo,
+            refundChain: sendParams.refundChain,
+            refundAddress: sendParams.refundAddress,
+            refundDeliveryProvider: provider.getTargetChainAddress(sendParams.targetChain),
+            sourceDeliveryProvider: toWormholeFormat(sendParams.deliveryProviderAddress),
+            senderAddress: toWormholeFormat(msg.sender),
+            vaaKeys: sendParams.vaaKeys
+        }).encode();
+
+        // Store information about this delivery in state
+        // so that it can be performed after the execution of the current delivery,
+        // when the 'refund' available to use for this delivery is known
+        appendForwardInstruction(
+            ForwardInstruction({
+                encodedInstruction: encodedInstruction,
+                msgValue: LocalNative.wrap(msg.value),
+                deliveryPrice: deliveryPrice,
+                paymentForExtraReceiverValue: sendParams.paymentForExtraReceiverValue,
+                consistencyLevel: sendParams.consistencyLevel,
+                rewardAddress: provider.getRewardAddress()
+            })
+        );
+    }
+
+    function resend(
+        VaaKey memory deliveryVaaKey,
+        uint16 targetChain,
+        TargetNative newReceiverValue,
+        bytes memory newEncodedExecutionParameters,
+        address newDeliveryProviderAddress
+    ) public payable returns (uint64 sequence) {
+        IDeliveryProvider provider = IDeliveryProvider(newDeliveryProviderAddress);
+
+        // Revert if delivery provider does not support the target chain
+        if (!provider.isChainSupported(targetChain)) {
+            revert DeliveryProviderDoesNotSupportTargetChain(
+                newDeliveryProviderAddress, targetChain
+            );
+        }
+        
+        // Obtain the delivery provider's fee for this delivery, as well as some encoded info (e.g. refund per unit of gas unused)
+        (LocalNative deliveryPrice, bytes memory encodedExecutionInfo) = provider.quoteDeliveryPrice(
+            targetChain, newReceiverValue, newEncodedExecutionParameters
+        );
+
+        // Check if user passed in 'one wormhole message fee' + 'delivery provider's fee'
+        LocalNative wormholeMessageFee = getWormholeMessageFee();
+        checkMsgValue(wormholeMessageFee, deliveryPrice, LocalNative.wrap(0));
+
+        // Encode all relevant info the delivery provider needs to perform this redelivery as requested
+        bytes memory encodedInstruction = RedeliveryInstruction({
+            deliveryVaaKey: deliveryVaaKey,
+            targetChain: targetChain,
+            newRequestedReceiverValue: newReceiverValue,
+            newEncodedExecutionInfo: encodedExecutionInfo,
+            newSourceDeliveryProvider: toWormholeFormat(newDeliveryProviderAddress),
+            newSenderAddress: toWormholeFormat(msg.sender)
+        }).encode();
+
+        // Publish the encoded redelivery instruction as a wormhole message
+        // and pay the delivery provider their fee
+        bool paymentSucceeded;
+        (sequence, paymentSucceeded) = publishAndPay(
+            wormholeMessageFee,
+            deliveryPrice,
+            LocalNative.wrap(0),
+            encodedInstruction,
+            CONSISTENCY_LEVEL_INSTANT,
+            provider.getRewardAddress()
+        );
+        if (!paymentSucceeded)
+            revert DeliveryProviderCannotReceivePayment();
+    }
+
+    function getDefaultDeliveryProvider() public view returns (address deliveryProvider) {
+        deliveryProvider = getDefaultDeliveryProviderState().defaultDeliveryProvider;
+    }
+
+    // Get the delivery provider's contract address on chain 'targetChain'
+    function getDefaultDeliveryProviderOnChain(uint16 targetChain)
+        public
+        view
+        returns (address deliveryProvider)
+    {
+        deliveryProvider = fromWormholeFormat(
+            IDeliveryProvider(getDefaultDeliveryProviderState().defaultDeliveryProvider)
+                .getTargetChainAddress(targetChain)
+        );
+    }
+
+    function getOriginalOrDefaultDeliveryProvider(uint16 targetChain)
+        public
+        view
+        returns (address deliveryProvider, address deliveryProviderOnTarget)
+    {
+        deliveryProvider = getOriginalDeliveryProvider();
+        if (
+            deliveryProvider == address(0)
+                || !IDeliveryProvider(deliveryProvider).isChainSupported(targetChain)
+        ) {
+            deliveryProvider = getDefaultDeliveryProvider();
+        }
+        deliveryProviderOnTarget = fromWormholeFormat(
+            IDeliveryProvider(deliveryProvider).getTargetChainAddress(targetChain)
+        );
+    }
+
+    function quoteEVMDeliveryPrice(
+        uint16 targetChain,
+        TargetNative receiverValue,
+        Gas gasLimit,
+        address deliveryProviderAddress
+    ) public view returns (LocalNative nativePriceQuote, GasPrice targetChainRefundPerGasUnused) {
+        (LocalNative quote, bytes memory encodedExecutionInfo) = quoteDeliveryPrice(
+            targetChain,
+            receiverValue,
+            encodeEvmExecutionParamsV1(EvmExecutionParamsV1(gasLimit)),
+            deliveryProviderAddress
+        );
+        nativePriceQuote = quote;
+        targetChainRefundPerGasUnused = decodeEvmExecutionInfoV1(encodedExecutionInfo).targetChainRefundPerGasUnused;
+    }
+
+    function quoteEVMDeliveryPrice(
+        uint16 targetChain,
+        TargetNative receiverValue,
+        Gas gasLimit
+    ) public view returns (LocalNative nativePriceQuote, GasPrice targetChainRefundPerGasUnused) {
+        return quoteEVMDeliveryPrice(
+            targetChain, receiverValue, gasLimit, getDefaultDeliveryProvider()
+        );
+    }
+
+    function quoteDeliveryPrice(
+        uint16 targetChain,
+        TargetNative receiverValue,
+        bytes memory encodedExecutionParameters,
+        address deliveryProviderAddress
+    ) public view returns (LocalNative nativePriceQuote, bytes memory encodedExecutionInfo) {
+        IDeliveryProvider provider = IDeliveryProvider(deliveryProviderAddress);
+        (LocalNative deliveryPrice, bytes memory _encodedExecutionInfo) = provider
+            .quoteDeliveryPrice(
+            targetChain, receiverValue, encodedExecutionParameters
+        );
+        encodedExecutionInfo = _encodedExecutionInfo;
+        nativePriceQuote = deliveryPrice + getWormholeMessageFee();
+    }
+
+    function quoteNativeForChain(
+        uint16 targetChain,
+        LocalNative currentChainAmount,
+        address deliveryProviderAddress
+    ) public view returns (TargetNative targetChainAmount) {
+        return IDeliveryProvider(deliveryProviderAddress).quoteAssetConversion(targetChain, currentChainAmount);
+    }
+}

+ 239 - 0
ethereum/contracts/relayer/wormholeRelayer/WormholeRelayerSerde.sol

@@ -0,0 +1,239 @@
+// SPDX-License-Identifier: Apache 2
+
+pragma solidity ^0.8.19;
+
+import {
+    InvalidPayloadId,
+    InvalidPayloadLength,
+    InvalidVaaKeyType,
+    VaaKey
+} from "../../interfaces/relayer/IWormholeRelayerTyped.sol";
+import {
+    DeliveryOverride,
+    DeliveryInstruction,
+    RedeliveryInstruction
+} from "../../libraries/relayer/RelayerInternalStructs.sol";
+import {BytesParsing} from "../../libraries/relayer/BytesParsing.sol";
+import "../../interfaces/relayer/TypedUnits.sol";
+
+library WormholeRelayerSerde {
+    using BytesParsing for bytes;
+    using WeiLib for Wei;
+    using GasLib for Gas;
+
+    //The slightly subtle difference between `PAYLOAD_ID`s and `VERSION`s is that payload ids carry
+    //  both type information _and_ version information, while `VERSION`s only carry the latter.
+    //That is, when deserialing a "version struct" we already know the expected type, but since we
+    //  publish both Delivery _and_ Redelivery instructions as serialized messages, we need a robust
+    //  way to distinguish both their type and their version during deserialization.
+    uint8 private constant VERSION_VAAKEY = 1;
+    uint8 private constant VERSION_DELIVERY_OVERRIDE = 1;
+    uint8 private constant PAYLOAD_ID_DELIVERY_INSTRUCTION = 1;
+    uint8 private constant PAYLOAD_ID_REDELIVERY_INSTRUCTION = 2;
+
+    // ---------------------- "public" (i.e implicitly internal) encode/decode -----------------------
+
+    //TODO GAS OPTIMIZATION: All the recursive abi.encodePacked calls in here are _insanely_ gas
+    //    inefficient (unless the optimizer is smart enough to just concatenate them tail-recursion
+    //    style which seems highly unlikely)
+
+    function encode(DeliveryInstruction memory strct)
+        internal
+        pure
+        returns (bytes memory encoded)
+    {
+        encoded = abi.encodePacked(
+            PAYLOAD_ID_DELIVERY_INSTRUCTION,
+            strct.targetChain,
+            strct.targetAddress,
+            encodeBytes(strct.payload),
+            strct.requestedReceiverValue,
+            strct.extraReceiverValue
+        );
+        encoded = abi.encodePacked(
+            encoded,
+            encodeBytes(strct.encodedExecutionInfo),
+            strct.refundChain,
+            strct.refundAddress,
+            strct.refundDeliveryProvider,
+            strct.sourceDeliveryProvider,
+            strct.senderAddress,
+            encodeVaaKeyArray(strct.vaaKeys)
+        );
+    }
+
+    function decodeDeliveryInstruction(bytes memory encoded)
+        internal
+        pure
+        returns (DeliveryInstruction memory strct)
+    {
+        uint256 offset = checkUint8(encoded, 0, PAYLOAD_ID_DELIVERY_INSTRUCTION);
+
+        uint256 requestedReceiverValue;
+        uint256 extraReceiverValue;
+
+        (strct.targetChain, offset) = encoded.asUint16Unchecked(offset);
+        (strct.targetAddress, offset) = encoded.asBytes32Unchecked(offset);
+        (strct.payload, offset) = decodeBytes(encoded, offset);
+        (requestedReceiverValue, offset) = encoded.asUint256Unchecked(offset);
+        (extraReceiverValue, offset) = encoded.asUint256Unchecked(offset);
+        (strct.encodedExecutionInfo, offset) = decodeBytes(encoded, offset);
+        (strct.refundChain, offset) = encoded.asUint16Unchecked(offset);
+        (strct.refundAddress, offset) = encoded.asBytes32Unchecked(offset);
+        (strct.refundDeliveryProvider, offset) = encoded.asBytes32Unchecked(offset);
+        (strct.sourceDeliveryProvider, offset) = encoded.asBytes32Unchecked(offset);
+        (strct.senderAddress, offset) = encoded.asBytes32Unchecked(offset);
+        (strct.vaaKeys, offset) = decodeVaaKeyArray(encoded, offset);
+
+        strct.requestedReceiverValue = TargetNative.wrap(requestedReceiverValue);
+        strct.extraReceiverValue = TargetNative.wrap(extraReceiverValue);
+
+        checkLength(encoded, offset);
+    }
+
+    function encode(RedeliveryInstruction memory strct)
+        internal
+        pure
+        returns (bytes memory encoded)
+    {
+        bytes memory vaaKey = encodeVaaKey(strct.deliveryVaaKey);
+        encoded = abi.encodePacked(
+            PAYLOAD_ID_REDELIVERY_INSTRUCTION,
+            vaaKey,
+            strct.targetChain,
+            strct.newRequestedReceiverValue,
+            encodeBytes(strct.newEncodedExecutionInfo),
+            strct.newSourceDeliveryProvider,
+            strct.newSenderAddress
+        );
+    }
+
+    function decodeRedeliveryInstruction(bytes memory encoded)
+        internal
+        pure
+        returns (RedeliveryInstruction memory strct)
+    {
+        uint256 offset = checkUint8(encoded, 0, PAYLOAD_ID_REDELIVERY_INSTRUCTION);
+
+        uint256 newRequestedReceiverValue;
+
+        (strct.deliveryVaaKey, offset) = decodeVaaKey(encoded, offset);
+        (strct.targetChain, offset) = encoded.asUint16Unchecked(offset);
+        (newRequestedReceiverValue, offset) = encoded.asUint256Unchecked(offset);
+        (strct.newEncodedExecutionInfo, offset) = decodeBytes(encoded, offset);
+        (strct.newSourceDeliveryProvider, offset) = encoded.asBytes32Unchecked(offset);
+        (strct.newSenderAddress, offset) = encoded.asBytes32Unchecked(offset);
+
+        strct.newRequestedReceiverValue = TargetNative.wrap(newRequestedReceiverValue);
+
+        checkLength(encoded, offset);
+    }
+
+    function encode(DeliveryOverride memory strct) internal pure returns (bytes memory encoded) {
+        encoded = abi.encodePacked(
+            VERSION_DELIVERY_OVERRIDE,
+            strct.newReceiverValue,
+            encodeBytes(strct.newExecutionInfo),
+            strct.redeliveryHash
+        );
+    }
+
+    function decodeDeliveryOverride(bytes memory encoded)
+        internal
+        pure
+        returns (DeliveryOverride memory strct)
+    {
+        uint256 offset = checkUint8(encoded, 0, VERSION_DELIVERY_OVERRIDE);
+
+        uint256 receiverValue;
+
+        (receiverValue, offset) = encoded.asUint256Unchecked(offset);
+        (strct.newExecutionInfo, offset) = decodeBytes(encoded, offset);
+        (strct.redeliveryHash, offset) = encoded.asBytes32Unchecked(offset);
+
+        strct.newReceiverValue = TargetNative.wrap(receiverValue);
+
+        checkLength(encoded, offset);
+    }
+
+    // ------------------------------------------ private --------------------------------------------
+
+    function encodeVaaKeyArray(VaaKey[] memory vaaKeys)
+        private
+        pure
+        returns (bytes memory encoded)
+    {
+        assert(vaaKeys.length < type(uint8).max);
+        encoded = abi.encodePacked(uint8(vaaKeys.length));
+        for (uint256 i = 0; i < vaaKeys.length;) {
+            encoded = abi.encodePacked(encoded, encodeVaaKey(vaaKeys[i]));
+            unchecked {
+                ++i;
+            }
+        }
+    }
+
+    function decodeVaaKeyArray(
+        bytes memory encoded,
+        uint256 startOffset
+    ) private pure returns (VaaKey[] memory vaaKeys, uint256 offset) {
+        uint8 vaaKeysLength;
+        (vaaKeysLength, offset) = encoded.asUint8Unchecked(startOffset);
+        vaaKeys = new VaaKey[](vaaKeysLength);
+        for (uint256 i = 0; i < vaaKeys.length;) {
+            (vaaKeys[i], offset) = decodeVaaKey(encoded, offset);
+            unchecked {
+                ++i;
+            }
+        }
+    }
+
+    function encodeVaaKey(VaaKey memory vaaKey) private pure returns (bytes memory encoded) {
+        encoded = abi.encodePacked(
+            encoded, VERSION_VAAKEY, vaaKey.chainId, vaaKey.emitterAddress, vaaKey.sequence
+        );
+    }
+
+    function decodeVaaKey(
+        bytes memory encoded,
+        uint256 startOffset
+    ) private pure returns (VaaKey memory vaaKey, uint256 offset) {
+        offset = checkUint8(encoded, startOffset, VERSION_VAAKEY);
+        (vaaKey.chainId, offset) = encoded.asUint16Unchecked(offset);
+        (vaaKey.emitterAddress, offset) = encoded.asBytes32Unchecked(offset);
+        (vaaKey.sequence, offset) = encoded.asUint64Unchecked(offset);
+    }
+
+    function encodeBytes(bytes memory payload) private pure returns (bytes memory encoded) {
+        //casting payload.length to uint32 is safe because you'll be hard-pressed to allocate 4 GB of
+        //  EVM memory in a single transaction
+        encoded = abi.encodePacked(uint32(payload.length), payload);
+    }
+
+    function decodeBytes(
+        bytes memory encoded,
+        uint256 startOffset
+    ) private pure returns (bytes memory payload, uint256 offset) {
+        uint32 payloadLength;
+        (payloadLength, offset) = encoded.asUint32Unchecked(startOffset);
+        (payload, offset) = encoded.sliceUnchecked(offset, payloadLength);
+    }
+
+    function checkUint8(
+        bytes memory encoded,
+        uint256 startOffset,
+        uint8 expectedPayloadId
+    ) private pure returns (uint256 offset) {
+        uint8 parsedPayloadId;
+        (parsedPayloadId, offset) = encoded.asUint8Unchecked(startOffset);
+        if (parsedPayloadId != expectedPayloadId) {
+            revert InvalidPayloadId(parsedPayloadId, expectedPayloadId);
+        }
+    }
+
+    function checkLength(bytes memory encoded, uint256 expected) private pure {
+        if (encoded.length != expected) {
+            revert InvalidPayloadLength(encoded.length, expected);
+        }
+    }
+}

+ 101 - 0
ethereum/contracts/relayer/wormholeRelayer/WormholeRelayerStorage.sol

@@ -0,0 +1,101 @@
+// SPDX-License-Identifier: Apache 2
+
+pragma solidity ^0.8.19;
+
+import "../../interfaces/relayer/TypedUnits.sol";
+
+// -------------------------------------- Persistent Storage ---------------------------------------
+
+//We have to hardcode the keccak256 values by hand rather than having them calculated because:
+//  solc: TypeError: Only direct number constants and references to such constants are supported by
+//          inline assembly.
+//And presumably what they mean by "direct number constants" is number literals...
+
+struct GovernanceState {
+    // mapping of IWormhole.VM.hash of previously executed governance VMs
+    mapping(bytes32 => bool) consumedGovernanceActions;
+}
+
+//keccak256("GovernanceState") - 1
+bytes32 constant GOVERNANCE_STORAGE_SLOT =
+    0x970ad24d4754c92e299cabb86552091f5df0a15abc0f1b71f37d3e30031585dc;
+
+function getGovernanceState() pure returns (GovernanceState storage state) {
+    assembly ("memory-safe") {
+        state.slot := GOVERNANCE_STORAGE_SLOT
+    }
+}
+
+struct DefaultDeliveryProviderState {
+    // address of the default relay provider on this chain
+    address defaultDeliveryProvider;
+}
+
+//keccak256("DefaultDeliveryProviderState") - 1
+bytes32 constant DEFAULT_RELAY_PROVIDER_STORAGE_SLOT =
+    0xebc28a1927f62765bfb7ada566eeab2d31a98c65dbd1e8cad64acae2a3ae45d4;
+
+function getDefaultDeliveryProviderState()
+    pure
+    returns (DefaultDeliveryProviderState storage state)
+{
+    assembly ("memory-safe") {
+        state.slot := DEFAULT_RELAY_PROVIDER_STORAGE_SLOT
+    }
+}
+
+struct RegisteredWormholeRelayersState {
+    // chainId => wormhole address mapping of relayer contracts on other chains
+    mapping(uint16 => bytes32) registeredWormholeRelayers;
+}
+
+//keccak256("RegisteredWormholeRelayersState") - 1
+bytes32 constant REGISTERED_CORE_RELAYERS_STORAGE_SLOT =
+    0x9e4e57806ba004485cfae8ca22fb13380f01c10b1b0ccf48c20464961643cf6d;
+
+function getRegisteredWormholeRelayersState()
+    pure
+    returns (RegisteredWormholeRelayersState storage state)
+{
+    assembly ("memory-safe") {
+        state.slot := REGISTERED_CORE_RELAYERS_STORAGE_SLOT
+    }
+}
+
+// ---------------------------------- Temporary/Volatile Storage -----------------------------------
+
+//Unlike proper persistent storage, everything below is only used for the lifetime of the current
+//  transaction and is (i.e. must be) reset at the end.
+
+struct ForwardInstruction {
+    bytes encodedInstruction;
+    LocalNative msgValue;
+    LocalNative deliveryPrice;
+    LocalNative paymentForExtraReceiverValue;
+    address payable rewardAddress;
+    uint8 consistencyLevel;
+}
+
+struct DeliveryTmpState {
+    bool deliveryInProgress;
+    // the target address that is currently being delivered to (0 for a simple refund)
+    address deliveryTarget;
+    // the target relay provider address for the in-progress delivery
+    address deliveryProvider;
+    // the refund chain for the in-progress delivery
+    uint16 refundChain;
+    // the refund address for the in-progress delivery
+    bytes32 refundAddress;
+    // Requests which will be forwarded from the current delivery.
+    ForwardInstruction[] forwardInstructions;
+}
+
+//keccak256("DeliveryTmpState") - 1
+bytes32 constant DELIVERY_TMP_STORAGE_SLOT =
+    0x1a2a8eb52f1d00a1242a3f8cc031e30a32870ff64f69009c4e06f75bd842fd22;
+
+function getDeliveryTmpState() pure returns (DeliveryTmpState storage state) {
+    assembly ("memory-safe") {
+        state.slot := DELIVERY_TMP_STORAGE_SLOT
+    }
+}

+ 76 - 0
ethereum/forge-test/relayer/AttackForwardIntegration.sol

@@ -0,0 +1,76 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.17;
+
+import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
+
+import "../../contracts/interfaces/IWormhole.sol";
+import "../../contracts/interfaces/relayer/IWormholeReceiver.sol";
+import "../../contracts/interfaces/relayer/IWormholeRelayerTyped.sol";
+// import "../../contracts/interfaces/relayer/IDeliveryProviderTyped.sol";
+import {toWormholeFormat} from "../../contracts/libraries/relayer/Utils.sol";
+
+/**
+ * This contract is a malicious "integration" that attempts to attack the forward mechanism.
+ */
+contract AttackForwardIntegration is IWormholeReceiver {
+    address attackerReward;
+    IWormhole immutable wormhole;
+    IWormholeRelayer immutable coreRelayer;
+    uint16 targetChain;
+
+    // Capture 30k gas for fees
+    // This just needs to be enough to pay for the call to the destination address.
+    uint32 SAFE_DELIVERY_GAS_CAPTURE = 30_000;
+
+    constructor(
+        IWormhole initWormhole,
+        IWormholeRelayer initWormholeRelayer,
+        uint16 chainId,
+        address initAttackerReward
+    ) {
+        wormhole = initWormhole;
+        attackerReward = initAttackerReward;
+        coreRelayer = initWormholeRelayer;
+        targetChain = chainId;
+    }
+
+    // This is the function which receives all messages from the remote contracts.
+    function receiveWormholeMessages(
+        bytes memory payload,
+        bytes[] memory additionalVaas,
+        bytes32 sourceAddress,
+        uint16 sourceChain,
+        bytes32 deliveryHash
+    ) public payable override {
+        // Do nothing. The attacker doesn't care about this message; he sends it himself.
+    }
+
+    receive() external payable {
+        // Request forward from the relayer network
+        // The core relayer could in principle accept the request due to this being the target of the message at the same time as being the refund address.
+        // Note that, if succesful, this forward request would be processed after the time for processing forwards is past.
+        // Thus, the request would "linger" in the forward request cache and be attended to in the next delivery.
+        forward(targetChain, attackerReward);
+    }
+
+    function forward(uint16 _targetChain, address attackerRewardAddress) internal {
+        (LocalNative deliveryPayment,) =
+            coreRelayer.quoteEVMDeliveryPrice(_targetChain, TargetNative.wrap(0), Gas.wrap(SAFE_DELIVERY_GAS_CAPTURE));
+
+        bytes memory emptyArray;
+        coreRelayer.forwardToEvm{value: LocalNative.unwrap(deliveryPayment)}(
+            _targetChain,
+            attackerRewardAddress,
+            emptyArray,
+            TargetNative.wrap(0),
+            LocalNative.wrap(0),
+            Gas.wrap(SAFE_DELIVERY_GAS_CAPTURE),
+            _targetChain,
+            // All remaining funds will be returned to the attacker
+            attackerRewardAddress,
+            coreRelayer.getDefaultDeliveryProvider(),
+            new VaaKey[](0),
+            15
+        );
+    }
+}

+ 38 - 0
ethereum/forge-test/relayer/BigRevertBufferIntegration.sol

@@ -0,0 +1,38 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.17;
+
+import "../../contracts/interfaces/relayer/IWormholeReceiver.sol";
+import "../../contracts/libraries/relayer/BytesParsing.sol";
+
+uint256 constant uint256Length = 32;
+
+/**
+ * This contract is meant to test different kinds of extreme scenarios when an integration returns data
+ * after its `receiveWormholeMessages` interface is called.
+ * 
+ * Only meant for testing purposes.
+ */
+contract BigRevertBufferIntegration is IWormholeReceiver {
+    using BytesParsing for bytes;
+    // This is the function which receives all messages from the remote contracts.
+    function receiveWormholeMessages(
+        bytes memory payload,
+        bytes[] memory /*additionalVaas*/,
+        bytes32 /*sourceAddress*/,
+        uint16 /*sourceChain*/,
+        bytes32 /*deliveryHash*/
+    ) public payable override {
+        (uint256 revertLength,) = payload.asUint256(0);
+        bytes memory revertBuffer = new bytes(revertLength);
+        for (uint256 i = 0; i < revertBuffer.length; ++i) {
+            revertBuffer[i] = bytes1(uint8(i));
+        }
+
+        // We avoid reverting with the standard `Error(string)` here because it may mess up terminals with these garbage bytes
+        // It's easier to predict what to test with this anyway.
+        assembly ("memory-safe") {
+            let buf := add(revertBuffer, uint256Length)
+            revert(buf, revertLength)
+        }
+    }
+}

+ 117 - 0
ethereum/forge-test/relayer/ForwardTester.sol

@@ -0,0 +1,117 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.0;
+
+import "../../contracts/interfaces/IWormhole.sol";
+import "../../contracts/interfaces/relayer/IWormholeReceiver.sol";
+import "../../contracts/interfaces/relayer/IWormholeRelayerTyped.sol";
+import "../../contracts/interfaces/relayer/IDeliveryProviderTyped.sol";
+import "../../contracts/libraries/external/BytesLib.sol";
+import "./MockGenericRelayer.sol";
+import "forge-std/console.sol";
+import "forge-std/Vm.sol";
+
+contract ForwardTester is IWormholeReceiver {
+    using BytesLib for bytes;
+    using GasLib for Gas;
+    using LocalNativeLib for LocalNative;
+
+    IWormhole wormhole;
+    IWormholeRelayer wormholeRelayer;
+    MockGenericRelayer genericRelayer;
+
+    Gas TOO_LOW_GAS_LIMIT = Gas.wrap(10000);
+    Gas REASONABLE_GAS_LIMIT = Gas.wrap(500000);
+
+    address private constant VM_ADDRESS =
+        address(bytes20(uint160(uint256(keccak256("hevm cheat code")))));
+
+    Vm public constant vm = Vm(VM_ADDRESS);
+
+    constructor(address _wormhole, address _wormholeRelayer, address _wormholeSimulator) {
+        wormhole = IWormhole(_wormhole);
+        wormholeRelayer = IWormholeRelayer(_wormholeRelayer);
+        genericRelayer = new MockGenericRelayer(_wormhole, _wormholeSimulator);
+        genericRelayer.setWormholeRelayerContract(wormhole.chainId(), address(wormholeRelayer));
+    }
+
+    enum Action {
+        ForwardRequestFromWrongAddress,
+        ProviderNotSupported,
+        ReentrantCall,
+        WorksCorrectly
+    }
+
+    function receiveWormholeMessages(
+        bytes memory payload,
+        bytes[] memory,
+        bytes32 sourceAddress,
+        uint16 sourceChain,
+        bytes32
+    ) public payable override {
+        Action action = Action(payload.toUint8(0));
+
+        if (action == Action.ForwardRequestFromWrongAddress) {
+            // Emitter must be a wormhole relayer
+            DummyContract dc = new DummyContract(address(wormholeRelayer));
+            dc.forward{value: msg.value}(
+                sourceChain, fromWormholeFormat(sourceAddress), REASONABLE_GAS_LIMIT, TargetNative.wrap(0), bytes("")
+            );
+        } else if (action == Action.ProviderNotSupported) {
+            wormholeRelayer.forwardPayloadToEvm{value: msg.value}(
+                32,
+                fromWormholeFormat(sourceAddress),
+                bytes(""),
+                TargetNative.wrap(0),
+                REASONABLE_GAS_LIMIT
+            );
+        } else if (action == Action.ReentrantCall) {
+            (LocalNative deliveryPrice,) =
+                wormholeRelayer.quoteEVMDeliveryPrice(sourceChain, TargetNative.wrap(0), REASONABLE_GAS_LIMIT);
+            vm.recordLogs();
+            wormholeRelayer.sendPayloadToEvm{
+                value: deliveryPrice.unwrap() + msg.value
+            }(
+                sourceChain,
+                fromWormholeFormat(sourceAddress),
+                bytes(""),
+                TargetNative.wrap(0),
+                REASONABLE_GAS_LIMIT
+            );
+            genericRelayer.relay(wormhole.chainId());
+        } else {
+            wormholeRelayer.forwardPayloadToEvm{value: msg.value}(
+                sourceChain,
+                fromWormholeFormat(sourceAddress),
+                bytes(""),
+                TargetNative.wrap(0),
+                REASONABLE_GAS_LIMIT
+            );
+        }
+    }
+
+    function fromWormholeFormat(bytes32 whAddress) public pure returns (address addr) {
+        return address(uint160(uint256(whAddress)));
+    }
+
+    receive() external payable {}
+}
+
+contract DummyContract {
+    IWormholeRelayer wormholeRelayer;
+
+    constructor(address _wormholeRelayer) {
+        wormholeRelayer = IWormholeRelayer(_wormholeRelayer);
+    }
+
+    function forward(
+        uint16 chainId,
+        address targetAddress,
+        Gas gasLimit,
+        TargetNative receiverValue,
+        bytes memory payload
+    ) public payable {
+        wormholeRelayer.forwardPayloadToEvm{value: msg.value}(
+            chainId, targetAddress, payload, receiverValue, gasLimit
+        );
+    }
+}

+ 206 - 0
ethereum/forge-test/relayer/MockGenericRelayer.sol

@@ -0,0 +1,206 @@
+// SPDX-License-Identifier: Apache 2
+
+pragma solidity ^0.8.0;
+
+import "../../contracts/interfaces/relayer/IWormholeRelayerTyped.sol";
+import {IWormhole} from "../../contracts/interfaces/IWormhole.sol";
+import {WormholeSimulator} from "./WormholeSimulator.sol";
+import {toWormholeFormat} from "../../contracts/libraries/relayer/Utils.sol";
+import {
+    DeliveryInstruction,
+    DeliveryOverride,
+    RedeliveryInstruction
+} from "../../contracts/libraries/relayer/RelayerInternalStructs.sol";
+import {WormholeRelayerSerde} from "../../contracts/relayer/wormholeRelayer/WormholeRelayerSerde.sol";
+import "../../contracts/libraries/external/BytesLib.sol";
+import "forge-std/Vm.sol";
+import "../../contracts/interfaces/relayer/TypedUnits.sol";
+import "../../contracts/libraries/relayer/ExecutionParameters.sol";
+
+contract MockGenericRelayer {
+    using BytesLib for bytes;
+    using WeiLib for Wei;
+    using GasLib for Gas;
+    using TargetNativeLib for TargetNative;
+
+    IWormhole relayerWormhole;
+    WormholeSimulator relayerWormholeSimulator;
+    uint256 transactionIndex;
+
+    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 => address) relayers;
+
+    mapping(bytes32 => bytes[]) pastEncodedVMs;
+
+    mapping(bytes32 => bytes) pastEncodedDeliveryVAA;
+
+    constructor(address _wormhole, address _wormholeSimulator) {
+        // deploy Wormhole
+
+        relayerWormhole = IWormhole(_wormhole);
+        relayerWormholeSimulator = WormholeSimulator(_wormholeSimulator);
+        transactionIndex = 0;
+    }
+
+    function getPastEncodedVMs(
+        uint16 chainId,
+        uint64 deliveryVAASequence
+    ) public view returns (bytes[] memory) {
+        return pastEncodedVMs[keccak256(abi.encodePacked(chainId, deliveryVAASequence))];
+    }
+
+    function getPastDeliveryVAA(
+        uint16 chainId,
+        uint64 deliveryVAASequence
+    ) public view returns (bytes memory) {
+        return pastEncodedDeliveryVAA[keccak256(abi.encodePacked(chainId, deliveryVAASequence))];
+    }
+
+    function setInfo(
+        uint16 chainId,
+        uint64 deliveryVAASequence,
+        bytes[] memory encodedVMs,
+        bytes memory encodedDeliveryVAA
+    ) internal {
+        pastEncodedVMs[keccak256(abi.encodePacked(chainId, deliveryVAASequence))] = encodedVMs;
+        pastEncodedDeliveryVAA[keccak256(abi.encodePacked(chainId, deliveryVAASequence))] =
+            encodedDeliveryVAA;
+    }
+
+    function setWormholeRelayerContract(uint16 chainId, address contractAddress) public {
+        wormholeRelayerContracts[chainId] = contractAddress;
+    }
+
+    function setProviderDeliveryAddress(uint16 chainId, address deliveryAddress) public {
+        relayers[chainId] = deliveryAddress;
+    }
+
+    function relay(uint16 chainId) public {
+        relay(vm.getRecordedLogs(), chainId, bytes(""));
+    }
+
+    function vaaKeyMatchesVAA(
+        VaaKey memory vaaKey,
+        bytes memory signedVaa
+    ) internal view returns (bool) {
+        IWormhole.VM memory parsedVaa = relayerWormhole.parseVM(signedVaa);
+        return (vaaKey.chainId == parsedVaa.emitterChainId)
+            && (vaaKey.emitterAddress == parsedVaa.emitterAddress)
+            && (vaaKey.sequence == parsedVaa.sequence);
+    }
+
+    function relay(Vm.Log[] memory logs, uint16 chainId, bytes memory deliveryOverrides) public {
+        Vm.Log[] memory entries = relayerWormholeSimulator.fetchWormholeMessageFromLog(logs);
+        bytes[] memory encodedVMs = new bytes[](entries.length);
+        for (uint256 i = 0; i < encodedVMs.length; i++) {
+            encodedVMs[i] = relayerWormholeSimulator.fetchSignedMessageFromLogs(
+                entries[i], chainId, address(uint160(uint256(bytes32(entries[i].topics[1]))))
+            );
+        }
+        IWormhole.VM[] memory parsed = new IWormhole.VM[](encodedVMs.length);
+        for (uint16 i = 0; i < encodedVMs.length; i++) {
+            parsed[i] = relayerWormhole.parseVM(encodedVMs[i]);
+        }
+        for (uint16 i = 0; i < encodedVMs.length; i++) {
+            if (
+                parsed[i].emitterAddress == toWormholeFormat(wormholeRelayerContracts[chainId])
+                    && (parsed[i].emitterChainId == chainId)
+            ) {
+                genericRelay(encodedVMs[i], encodedVMs, parsed[i], deliveryOverrides);
+            }
+        }
+    }
+
+    function relay(uint16 chainId, bytes memory deliveryOverrides) public {
+        relay(vm.getRecordedLogs(), chainId, deliveryOverrides);
+    }
+
+    function genericRelay(
+        bytes memory encodedDeliveryVAA,
+        bytes[] memory encodedVMs,
+        IWormhole.VM memory parsedDeliveryVAA,
+        bytes memory deliveryOverrides
+    ) internal {
+        uint8 payloadId = parsedDeliveryVAA.payload.toUint8(0);
+        if (payloadId == 1) {
+            DeliveryInstruction memory instruction =
+                WormholeRelayerSerde.decodeDeliveryInstruction(parsedDeliveryVAA.payload);
+
+            bytes[] memory encodedVMsToBeDelivered = new bytes[](instruction.vaaKeys.length);
+
+            for (uint8 i = 0; i < instruction.vaaKeys.length; i++) {
+                for (uint8 j = 0; j < encodedVMs.length; j++) {
+                    if (vaaKeyMatchesVAA(instruction.vaaKeys[i], encodedVMs[j])) {
+                        encodedVMsToBeDelivered[i] = encodedVMs[j];
+                        break;
+                    }
+                }
+            }
+
+            EvmExecutionInfoV1 memory executionInfo =
+                decodeEvmExecutionInfoV1(instruction.encodedExecutionInfo);
+            Wei budget = executionInfo.gasLimit.toWei(executionInfo.targetChainRefundPerGasUnused)
+                + instruction.requestedReceiverValue.asNative() + instruction.extraReceiverValue.asNative();
+
+            uint16 targetChain = instruction.targetChain;
+
+            vm.prank(relayers[targetChain]);
+            IWormholeRelayerDelivery(wormholeRelayerContracts[targetChain]).deliver{
+                value: budget.unwrap()
+            }(
+                encodedVMsToBeDelivered,
+                encodedDeliveryVAA,
+                payable(relayers[targetChain]),
+                deliveryOverrides
+            );
+
+            setInfo(
+                parsedDeliveryVAA.emitterChainId,
+                parsedDeliveryVAA.sequence,
+                encodedVMsToBeDelivered,
+                encodedDeliveryVAA
+            );
+        } else if (payloadId == 2) {
+            RedeliveryInstruction memory instruction =
+                WormholeRelayerSerde.decodeRedeliveryInstruction(parsedDeliveryVAA.payload);
+
+            DeliveryOverride memory deliveryOverride = DeliveryOverride({
+                newExecutionInfo: instruction.newEncodedExecutionInfo,
+                newReceiverValue: instruction.newRequestedReceiverValue,
+                redeliveryHash: parsedDeliveryVAA.hash
+            });
+
+            EvmExecutionInfoV1 memory executionInfo =
+                decodeEvmExecutionInfoV1(instruction.newEncodedExecutionInfo);
+            Wei budget = executionInfo.gasLimit.toWei(executionInfo.targetChainRefundPerGasUnused)
+                + instruction.newRequestedReceiverValue.asNative();
+
+            bytes memory oldEncodedDeliveryVAA = getPastDeliveryVAA(
+                instruction.deliveryVaaKey.chainId, instruction.deliveryVaaKey.sequence
+            );
+            bytes[] memory oldEncodedVMs = getPastEncodedVMs(
+                instruction.deliveryVaaKey.chainId, instruction.deliveryVaaKey.sequence
+            );
+
+            uint16 targetChain = WormholeRelayerSerde.decodeDeliveryInstruction(
+                relayerWormhole.parseVM(oldEncodedDeliveryVAA).payload
+            ).targetChain;
+
+            vm.prank(relayers[targetChain]);
+            IWormholeRelayerDelivery(wormholeRelayerContracts[targetChain]).deliver{
+                value: budget.unwrap()
+            }(
+                oldEncodedVMs,
+                oldEncodedDeliveryVAA,
+                payable(relayers[targetChain]),
+                WormholeRelayerSerde.encode(deliveryOverride)
+            );
+        }
+    }
+}

+ 289 - 0
ethereum/forge-test/relayer/MockWormhole.sol

@@ -0,0 +1,289 @@
+// SPDX-License-Identifier: Apache 2
+pragma solidity ^0.8.17;
+
+import "../../contracts/interfaces/IWormhole.sol";
+import "../../contracts/libraries/external/BytesLib.sol";
+
+contract MockWormhole is IWormhole {
+    using BytesLib for bytes;
+
+    uint256 private constant VM_VERSION_SIZE = 1;
+    uint256 private constant VM_GUARDIAN_SET_SIZE = 4;
+    uint256 private constant VM_SIGNATURE_COUNT_SIZE = 1;
+    uint256 private constant VM_TIMESTAMP_SIZE = 4;
+    uint256 private constant VM_NONCE_SIZE = 4;
+    uint256 private constant VM_EMITTER_CHAIN_ID_SIZE = 2;
+    uint256 private constant VM_EMITTER_ADDRESS_SIZE = 32;
+    uint256 private constant VM_SEQUENCE_SIZE = 8;
+    uint256 private constant VM_CONSISTENCY_LEVEL_SIZE = 1;
+    uint256 private constant VM_SIZE_MINIMUM = VM_VERSION_SIZE + VM_GUARDIAN_SET_SIZE
+        + VM_SIGNATURE_COUNT_SIZE + VM_TIMESTAMP_SIZE + VM_NONCE_SIZE + VM_EMITTER_CHAIN_ID_SIZE
+        + VM_EMITTER_ADDRESS_SIZE + VM_SEQUENCE_SIZE + VM_CONSISTENCY_LEVEL_SIZE;
+
+    uint256 private constant SIGNATURE_GUARDIAN_INDEX_SIZE = 1;
+    uint256 private constant SIGNATURE_R_SIZE = 32;
+    uint256 private constant SIGNATURE_S_SIZE = 32;
+    uint256 private constant SIGNATURE_V_SIZE = 1;
+    uint256 private constant SIGNATURE_SIZE_TOTAL =
+        SIGNATURE_GUARDIAN_INDEX_SIZE + SIGNATURE_R_SIZE + SIGNATURE_S_SIZE + SIGNATURE_V_SIZE;
+
+    mapping(address => uint64) public sequences;
+    // Dictionary of VMs that must be mocked as invalid.
+    mapping(bytes32 => bool) public invalidVMs;
+
+    uint256 currentMsgFee;
+    uint16 immutable wormholeChainId;
+    uint256 immutable boundEvmChainId;
+
+    constructor(uint16 initChainId, uint256 initEvmChainId) {
+        wormholeChainId = initChainId;
+        boundEvmChainId = initEvmChainId;
+    }
+
+    function invalidateVM(bytes calldata encodedVm) external {
+        VM memory vm = _parseVM(encodedVm);
+        invalidVMs[vm.hash] = true;
+    }
+
+    function publishMessage(
+        uint32 nonce,
+        bytes memory payload,
+        uint8 consistencyLevel
+    ) external payable returns (uint64 sequence) {
+        require(msg.value == currentMsgFee, "invalid fee");
+        sequence = sequences[msg.sender]++;
+        emit LogMessagePublished(msg.sender, sequence, nonce, payload, consistencyLevel);
+    }
+
+    function parseVM(bytes calldata encodedVm) external pure returns (VM memory vm) {
+        vm = _parseVM(encodedVm);
+    }
+
+    function parseAndVerifyVM(bytes calldata encodedVm)
+        external
+        view
+        returns (VM memory vm, bool valid, string memory reason)
+    {
+        vm = _parseVM(encodedVm);
+        //behold the rigorous checking!
+        valid = !invalidVMs[vm.hash];
+        reason = "";
+    }
+
+    function _parseVM(bytes calldata encodedVm) internal pure returns (VM memory vm) {
+        require(encodedVm.length >= VM_SIZE_MINIMUM, "vm too small");
+
+        bytes memory body;
+
+        uint256 offset = 0;
+        vm.version = encodedVm.toUint8(offset);
+        offset += 1;
+
+        vm.guardianSetIndex = encodedVm.toUint32(offset);
+        offset += 4;
+
+        (vm.signatures, offset) = parseSignatures(encodedVm, offset);
+
+        body = encodedVm[offset:];
+        vm.timestamp = encodedVm.toUint32(offset);
+        offset += 4;
+
+        vm.nonce = encodedVm.toUint32(offset);
+        offset += 4;
+
+        vm.emitterChainId = encodedVm.toUint16(offset);
+        offset += 2;
+
+        vm.emitterAddress = encodedVm.toBytes32(offset);
+        offset += 32;
+
+        vm.sequence = encodedVm.toUint64(offset);
+        offset += 8;
+
+        vm.consistencyLevel = encodedVm.toUint8(offset);
+        offset += 1;
+
+        vm.payload = encodedVm[offset:];
+        vm.hash = keccak256(abi.encodePacked(keccak256(body)));
+    }
+
+    function parseSignatures(
+        bytes calldata encodedVm,
+        uint256 offset
+    ) internal pure returns (Signature[] memory signatures, uint256 offsetAfterParse) {
+        uint256 sigCount = uint256(encodedVm.toUint8(offset));
+        offset += 1;
+
+        require(
+            encodedVm.length >= (VM_SIZE_MINIMUM + sigCount * SIGNATURE_SIZE_TOTAL), "vm too small"
+        );
+
+        signatures = new Signature[](sigCount);
+        for (uint256 i = 0; i < sigCount; ++i) {
+            uint8 guardianIndex = encodedVm.toUint8(offset);
+            offset += 1;
+
+            bytes32 r = encodedVm.toBytes32(offset);
+            offset += 32;
+
+            bytes32 s = encodedVm.toBytes32(offset);
+            offset += 32;
+
+            uint8 v = encodedVm.toUint8(offset);
+            offset += 1;
+
+            signatures[i] = Signature({
+                r: r,
+                s: s,
+                // The hardcoded 27 comes from the base offset for public key recovery ids, public key type and network
+                // used in ECDSA signatures for bitcoin and ethereum.
+                // See https://bitcoin.stackexchange.com/a/5089
+                v: v + 27,
+                guardianIndex: guardianIndex
+            });
+        }
+
+        return (signatures, offset);
+    }
+
+    function initialize() external {}
+
+    function quorum(uint256 /*numGuardians*/ )
+        external
+        pure
+        returns (uint256 /*numSignaturesRequiredForQuorum*/ )
+    {
+        return 1;
+    }
+
+    /**
+     * General state and chain observers
+     */
+    function chainId() external view returns (uint16) {
+        return wormholeChainId;
+    }
+
+    function evmChainId() external view returns (uint256) {
+        return boundEvmChainId;
+    }
+
+    function getCurrentGuardianSetIndex() external pure returns (uint32) {
+        return 0;
+    }
+
+    function getGuardianSet(uint32 /*index*/ ) external pure returns (GuardianSet memory) {
+        revert("unsupported getGuardianSet in wormhole mock");
+    }
+
+    function getGuardianSetExpiry() external pure returns (uint32) {
+        return 0;
+    }
+
+    function governanceActionIsConsumed(bytes32 /*hash*/ ) external pure returns (bool) {
+        return false;
+    }
+
+    function isInitialized(address /*impl*/ ) external pure returns (bool) {
+        return true;
+    }
+
+    function isFork() external pure returns (bool) {
+        return false;
+    }
+
+    function governanceChainId() external pure returns (uint16) {
+        return 1;
+    }
+
+    function governanceContract() external pure returns (bytes32) {
+        return bytes32(0x0000000000000000000000000000000000000000000000000000000000000004);
+    }
+
+    function messageFee() external view returns (uint256) {
+        return currentMsgFee;
+    }
+
+    function nextSequence(address emitter) external view returns (uint64) {
+        return sequences[emitter];
+    }
+
+    function verifyVM(VM memory /*vm*/ )
+        external
+        pure
+        returns (bool, /*valid*/ string memory /*reason*/ )
+    {
+        revert("unsupported verifyVM in wormhole mock");
+    }
+
+    function verifySignatures(
+        bytes32, /*hash*/
+        Signature[] memory, /*signatures*/
+        GuardianSet memory /*guardianSet*/
+    ) external pure returns (bool, /*valid*/ string memory /*reason*/ ) {
+        revert("unsupported verifySignatures in wormhole mock");
+    }
+
+    function parseContractUpgrade(bytes memory /*encodedUpgrade*/ )
+        external
+        pure
+        returns (ContractUpgrade memory /*cu*/ )
+    {
+        revert("unsupported parseContractUpgrade in wormhole mock");
+    }
+
+    function parseGuardianSetUpgrade(bytes memory /*encodedUpgrade*/ )
+        external
+        pure
+        returns (GuardianSetUpgrade memory /*gsu*/ )
+    {
+        revert("unsupported parseGuardianSetUpgrade in wormhole mock");
+    }
+
+    function parseSetMessageFee(bytes memory /*encodedSetMessageFee*/ )
+        external
+        pure
+        returns (SetMessageFee memory /*smf*/ )
+    {
+        revert("unsupported parseSetMessageFee in wormhole mock");
+    }
+
+    function parseTransferFees(bytes memory /*encodedTransferFees*/ )
+        external
+        pure
+        returns (TransferFees memory /*tf*/ )
+    {
+        revert("unsupported parseTransferFees in wormhole mock");
+    }
+
+    function parseRecoverChainId(bytes memory /*encodedRecoverChainId*/ )
+        external
+        pure
+        returns (RecoverChainId memory /*rci*/ )
+    {
+        revert("unsupported parseRecoverChainId in wormhole mock");
+    }
+
+    function submitContractUpgrade(bytes memory /*_vm*/ ) external pure {
+        revert("unsupported submitContractUpgrade in wormhole mock");
+    }
+
+    function submitSetMessageFee(bytes memory /*_vm*/ ) external pure {
+        revert("unsupported submitSetMessageFee in wormhole mock");
+    }
+
+    function setMessageFee(uint256 newFee) external {
+        currentMsgFee = newFee;
+    }
+
+    function submitNewGuardianSet(bytes memory /*_vm*/ ) external pure {
+        revert("unsupported submitNewGuardianSet in wormhole mock");
+    }
+
+    function submitTransferFees(bytes memory /*_vm*/ ) external pure {
+        revert("unsupported submitTransferFees in wormhole mock");
+    }
+
+    function submitRecoverChainId(bytes memory /*_vm*/ ) external pure {
+        revert("unsupported submitRecoverChainId in wormhole mock");
+    }
+}

+ 375 - 0
ethereum/forge-test/relayer/RelayProvider.t.sol

@@ -0,0 +1,375 @@
+// SPDX-License-Identifier: Apache 2
+
+pragma solidity ^0.8.0;
+
+import "../../contracts/interfaces/relayer/IDeliveryProviderTyped.sol";
+import "../../contracts/relayer/deliveryProvider/DeliveryProvider.sol";
+import "../../contracts/relayer/deliveryProvider/DeliveryProviderSetup.sol";
+import "../../contracts/relayer/deliveryProvider/DeliveryProviderImplementation.sol";
+import "../../contracts/relayer/deliveryProvider/DeliveryProviderProxy.sol";
+import "../../contracts/relayer/deliveryProvider/DeliveryProviderStructs.sol";
+import "../../contracts/interfaces/relayer/TypedUnits.sol";
+import {MockWormhole} from "./MockWormhole.sol";
+import {IWormhole} from "../../contracts/interfaces/IWormhole.sol";
+import {WormholeSimulator, FakeWormholeSimulator} from "./WormholeSimulator.sol";
+
+import "forge-std/Test.sol";
+
+contract TestDeliveryProvider is Test {
+    using WeiLib for Wei;
+    using GasLib for Gas;
+    using WeiPriceLib for WeiPrice;
+    using GasPriceLib for GasPrice;
+
+    uint16 constant TEST_ORACLE_CHAIN_ID = 2;
+
+    DeliveryProvider internal deliveryProvider;
+
+    function initializeDeliveryProvider() internal {
+        DeliveryProviderSetup deliveryProviderSetup = new DeliveryProviderSetup();
+        DeliveryProviderImplementation deliveryProviderImplementation =
+            new DeliveryProviderImplementation();
+        DeliveryProviderProxy myDeliveryProvider = new DeliveryProviderProxy(
+            address(deliveryProviderSetup),
+            abi.encodeCall(
+                DeliveryProviderSetup.setup,
+                (
+                    address(deliveryProviderImplementation),
+                    TEST_ORACLE_CHAIN_ID
+                )
+            )
+        );
+
+        deliveryProvider = DeliveryProvider(address(myDeliveryProvider));
+
+        require(deliveryProvider.owner() == address(this), "owner() != expected");
+        require(deliveryProvider.chainId() == TEST_ORACLE_CHAIN_ID, "chainId() != expected");
+    }
+
+    function testCannotUpdatePriceWithChainIdZero(
+        GasPrice updateGasPrice,
+        WeiPrice updateNativeCurrencyPrice
+    ) public {
+        vm.assume(updateGasPrice.unwrap() > 0);
+        vm.assume(updateNativeCurrencyPrice.unwrap() > 0);
+
+        initializeDeliveryProvider();
+
+        // you shall not pass
+        vm.expectRevert(abi.encodeWithSignature("ChainIdIsZero()"));
+        deliveryProvider.updatePrice(
+            0, // updateChainId
+            updateGasPrice,
+            updateNativeCurrencyPrice
+        );
+    }
+
+    function testCannotUpdatePriceWithGasPriceZero(
+        uint16 updateChainId,
+        WeiPrice updateNativeCurrencyPrice
+    ) public {
+        vm.assume(updateChainId > 0);
+        vm.assume(updateNativeCurrencyPrice.unwrap() > 0);
+
+        initializeDeliveryProvider();
+
+        // you shall not pass
+        vm.expectRevert(abi.encodeWithSignature("GasPriceIsZero()"));
+        deliveryProvider.updatePrice(
+            updateChainId,
+            GasPrice.wrap(0), // updateGasPrice == 0
+            updateNativeCurrencyPrice
+        );
+    }
+
+    function testCannotUpdatePriceWithNativeCurrencyPriceZero(
+        uint16 updateChainId,
+        GasPrice updateGasPrice
+    ) public {
+        vm.assume(updateChainId > 0);
+        vm.assume(updateGasPrice.unwrap() > 0);
+
+        initializeDeliveryProvider();
+
+        // you shall not pass
+        vm.expectRevert(abi.encodeWithSignature("NativeCurrencyPriceIsZero()"));
+        deliveryProvider.updatePrice(
+            updateChainId,
+            updateGasPrice,
+            WeiPrice.wrap(0) // updateNativeCurrencyPrice == 0
+        );
+    }
+
+    function testCanUpdatePriceOnlyAsOwner(
+        address oracleOwner,
+        uint16 updateChainId,
+        GasPrice updateGasPrice,
+        WeiPrice updateNativeCurrencyPrice
+    ) public {
+        vm.assume(oracleOwner != address(0));
+        vm.assume(oracleOwner != address(this));
+        vm.assume(updateChainId > 0);
+        vm.assume(updateGasPrice.unwrap() > 0);
+        vm.assume(updateNativeCurrencyPrice.unwrap() > 0);
+
+        initializeDeliveryProvider();
+
+        // you shall not pass
+        vm.prank(oracleOwner);
+        vm.expectRevert(abi.encodeWithSignature("CallerMustBeOwner()"));
+        deliveryProvider.updatePrice(updateChainId, updateGasPrice, updateNativeCurrencyPrice);
+    }
+
+
+    function testCannotGetPriceBeforeUpdateSrcPrice(
+        uint16 dstChainId,
+        uint64 dstGasPrice,
+        WeiPrice dstNativeCurrencyPrice
+    )
+        public
+    {
+        vm.assume(dstChainId > 0);
+        vm.assume(dstChainId != TEST_ORACLE_CHAIN_ID);
+        vm.assume(dstGasPrice > 0);
+        vm.assume(dstNativeCurrencyPrice.unwrap() > 0);
+
+        initializeDeliveryProvider();
+
+        // update the price with reasonable values
+        deliveryProvider.updatePrice(dstChainId, GasPrice.wrap(dstGasPrice), dstNativeCurrencyPrice);
+
+        // you shall not pass
+        vm.expectRevert(abi.encodeWithSignature("PriceIsZero(uint16)", TEST_ORACLE_CHAIN_ID));
+        deliveryProvider.quoteDeliveryOverhead(dstChainId);
+    }
+
+    function testCannotGetPriceBeforeUpdateDstPrice(
+        uint16 dstChainId,
+        uint64 srcGasPrice,
+        WeiPrice srcNativeCurrencyPrice
+    )
+        public
+    {
+        vm.assume(dstChainId > 0);
+        vm.assume(dstChainId != TEST_ORACLE_CHAIN_ID);
+        vm.assume(srcGasPrice > 0);
+        vm.assume(srcNativeCurrencyPrice.unwrap() > 0);
+
+        initializeDeliveryProvider();
+
+        // update the price with reasonable values
+        //vm.prank(deliveryProvider.owner());
+        deliveryProvider.updatePrice(TEST_ORACLE_CHAIN_ID, GasPrice.wrap(srcGasPrice), srcNativeCurrencyPrice);
+
+        // you shall not pass
+        vm.expectRevert(abi.encodeWithSignature("PriceIsZero(uint16)", dstChainId));
+        deliveryProvider.quoteDeliveryOverhead(dstChainId);
+    }
+    
+
+    function testUpdatePrice(
+        uint16 dstChainId,
+        uint64 dstGasPrice,
+        uint64 dstNativeCurrencyPrice,
+        uint64 srcGasPrice,
+        uint64 srcNativeCurrencyPrice
+    ) public {
+        vm.assume(dstChainId > 0);
+        vm.assume(dstChainId != TEST_ORACLE_CHAIN_ID);
+        vm.assume(dstGasPrice > 0);
+        vm.assume(dstNativeCurrencyPrice > 0);
+        vm.assume(srcGasPrice > 0);
+        vm.assume(srcNativeCurrencyPrice > 0);
+        vm.assume(uint256(dstGasPrice) * srcNativeCurrencyPrice >= dstNativeCurrencyPrice);
+        vm.assume(dstGasPrice * uint256(dstNativeCurrencyPrice) / srcNativeCurrencyPrice < 2 ** 72);
+
+        initializeDeliveryProvider();
+
+        // update the prices with reasonable values
+        deliveryProvider.updatePrice(
+            dstChainId, GasPrice.wrap(dstGasPrice), WeiPrice.wrap(dstNativeCurrencyPrice)
+        );
+        deliveryProvider.updatePrice(
+            TEST_ORACLE_CHAIN_ID, GasPrice.wrap(srcGasPrice), WeiPrice.wrap(srcNativeCurrencyPrice)
+        );
+
+        // verify price
+        uint256 expected = (
+            uint256(dstNativeCurrencyPrice) * (uint256(dstGasPrice)) + (srcNativeCurrencyPrice - 1)
+        ) / srcNativeCurrencyPrice;
+        GasPrice readValues = deliveryProvider.quoteGasPrice(dstChainId);
+        console.log(readValues.unwrap(), expected);
+        require(readValues.unwrap() == expected, "deliveryProvider.quotePrices != expected");
+    }
+
+    struct UpdatePrice {
+        uint16 chainId;
+        uint128 gasPrice;
+        uint128 nativeCurrencyPrice;
+    }
+
+    function testBulkUpdatePrices(
+        uint16 dstChainId,
+        uint64 dstGasPrice,
+        uint64 dstNativeCurrencyPrice,
+        uint64 srcGasPrice,
+        uint64 srcNativeCurrencyPrice
+    ) public {
+        vm.assume(dstChainId > 0);
+        vm.assume(dstChainId != TEST_ORACLE_CHAIN_ID); // wormhole.chainId()
+        vm.assume(dstGasPrice > 0);
+        vm.assume(dstNativeCurrencyPrice > 0);
+        vm.assume(srcGasPrice > 0);
+        vm.assume(srcNativeCurrencyPrice > 0);
+        vm.assume(dstGasPrice >= dstNativeCurrencyPrice / srcNativeCurrencyPrice);
+        vm.assume(dstGasPrice * uint256(dstNativeCurrencyPrice) / srcNativeCurrencyPrice < 2 ** 72);
+
+        initializeDeliveryProvider();
+
+        DeliveryProviderStructs.UpdatePrice[] memory updates =
+            new DeliveryProviderStructs.UpdatePrice[](2);
+        updates[0] = DeliveryProviderStructs.UpdatePrice({
+            chainId: TEST_ORACLE_CHAIN_ID,
+            gasPrice: GasPrice.wrap(srcGasPrice),
+            nativeCurrencyPrice: WeiPrice.wrap(srcNativeCurrencyPrice)
+        });
+        updates[1] = DeliveryProviderStructs.UpdatePrice({
+            chainId: dstChainId,
+            gasPrice: GasPrice.wrap(dstGasPrice),
+            nativeCurrencyPrice: WeiPrice.wrap(dstNativeCurrencyPrice)
+        });
+
+        // update the prices with reasonable values
+        deliveryProvider.updatePrices(updates);
+
+        // verify price
+        uint256 expected = (
+            uint256(dstNativeCurrencyPrice) * (uint256(dstGasPrice)) + (srcNativeCurrencyPrice - 1)
+        ) / srcNativeCurrencyPrice;
+        GasPrice readValues = deliveryProvider.quoteGasPrice(dstChainId);
+        require(readValues.unwrap() == expected, "deliveryProvider.quotePrices != expected");
+    }
+
+    function testUpdateTargetChainContracts(uint16 targetChain, bytes32 newAddress) public {
+        initializeDeliveryProvider();
+
+        deliveryProvider.updateTargetChainAddress(targetChain, newAddress);
+        bytes32 updated = deliveryProvider.getTargetChainAddress(targetChain);
+
+        assertTrue(newAddress == updated);
+    }
+
+    function testUpdateRewardAddress(address payable newAddress) public {
+        initializeDeliveryProvider();
+
+        deliveryProvider.updateRewardAddress(newAddress);
+        address payable updated = deliveryProvider.getRewardAddress();
+
+        assertTrue(newAddress == updated);
+    }
+
+    function testQuoteDeliveryOverhead(
+        uint16 dstChainId,
+        uint64 dstGasPrice,
+        uint64 dstNativeCurrencyPrice,
+        uint64 srcGasPrice,
+        uint64 srcNativeCurrencyPrice,
+        uint32 gasOverhead
+    ) public {
+        initializeDeliveryProvider();
+
+        vm.assume(dstChainId > 0);
+        vm.assume(dstChainId != TEST_ORACLE_CHAIN_ID); // wormhole.chainId()
+        vm.assume(dstGasPrice > 0);
+        vm.assume(dstNativeCurrencyPrice > 0);
+        vm.assume(srcGasPrice > 0);
+        vm.assume(srcNativeCurrencyPrice > 0);
+        vm.assume(dstGasPrice >= dstNativeCurrencyPrice / srcNativeCurrencyPrice);
+        vm.assume(dstGasPrice * uint256(dstNativeCurrencyPrice) / srcNativeCurrencyPrice < 2 ** 72);
+
+        vm.assume(gasOverhead < uint256(2)**31);
+
+        // update the prices with reasonable values
+        deliveryProvider.updatePrice(
+            dstChainId, GasPrice.wrap(dstGasPrice), WeiPrice.wrap(dstNativeCurrencyPrice)
+        );
+        deliveryProvider.updatePrice(
+            TEST_ORACLE_CHAIN_ID, GasPrice.wrap(srcGasPrice), WeiPrice.wrap(srcNativeCurrencyPrice)
+        );
+
+        deliveryProvider.updateAssetConversionBuffer(dstChainId, 5, 100);
+
+        deliveryProvider.updateDeliverGasOverhead(dstChainId, Gas.wrap(gasOverhead));
+
+        deliveryProvider.updateMaximumBudget(dstChainId, Wei.wrap(uint256(2)**191));
+
+        // verify price
+        uint256 expectedOverhead = (
+            uint256(dstNativeCurrencyPrice) * (uint256(dstGasPrice) * gasOverhead) + (srcNativeCurrencyPrice - 1)
+        ) / srcNativeCurrencyPrice;
+
+        LocalNative deliveryOverhead = deliveryProvider.quoteDeliveryOverhead(dstChainId);
+
+        require(expectedOverhead == LocalNative.unwrap(deliveryOverhead), "deliveryProvider overhead quote is not what is expected");
+    }
+
+    function testQuoteDeliveryPrice(
+        uint16 dstChainId,
+        uint64 dstGasPrice,
+        uint64 dstNativeCurrencyPrice,
+        uint64 srcGasPrice,
+        uint64 srcNativeCurrencyPrice,
+        uint32 gasLimit,
+        uint32 gasOverhead,
+        uint64 receiverValue
+    ) public {
+        initializeDeliveryProvider();
+
+        vm.assume(dstChainId > 0);
+        vm.assume(dstChainId != TEST_ORACLE_CHAIN_ID); // wormhole.chainId()
+        vm.assume(dstGasPrice > 0);
+        vm.assume(dstNativeCurrencyPrice > 0);
+        vm.assume(srcGasPrice > 0);
+        vm.assume(srcNativeCurrencyPrice > 0);
+        vm.assume(dstGasPrice >= dstNativeCurrencyPrice / srcNativeCurrencyPrice);
+        vm.assume(dstGasPrice * uint256(dstNativeCurrencyPrice) / srcNativeCurrencyPrice < 2 ** 72);
+
+        vm.assume(gasLimit < uint256(2)**31);
+        vm.assume(gasOverhead < uint256(2)**31);
+
+        // update the prices with reasonable values
+        deliveryProvider.updatePrice(
+            dstChainId, GasPrice.wrap(dstGasPrice), WeiPrice.wrap(dstNativeCurrencyPrice)
+        );
+        deliveryProvider.updatePrice(
+            TEST_ORACLE_CHAIN_ID, GasPrice.wrap(srcGasPrice), WeiPrice.wrap(srcNativeCurrencyPrice)
+        );
+
+        deliveryProvider.updateAssetConversionBuffer(dstChainId, 5, 100);
+
+        deliveryProvider.updateDeliverGasOverhead(dstChainId, Gas.wrap(gasOverhead));
+
+        deliveryProvider.updateMaximumBudget(dstChainId, Wei.wrap(uint256(2)**191));
+
+        // verify price
+        uint256 expectedGasCost = (
+            uint256(dstNativeCurrencyPrice) * (uint256(dstGasPrice) * (gasLimit)) + (srcNativeCurrencyPrice - 1)
+        ) / srcNativeCurrencyPrice;
+
+        uint256 expectedOverheadCost = (
+            uint256(dstNativeCurrencyPrice) * (uint256(dstGasPrice) * ( gasOverhead)) + (srcNativeCurrencyPrice - 1)
+        ) / srcNativeCurrencyPrice;
+
+        uint256 expectedReceiverValueCost = (
+            uint256(dstNativeCurrencyPrice) * (receiverValue) * 105 + (srcNativeCurrencyPrice * uint256(100) - 1)
+        ) / srcNativeCurrencyPrice / 100;
+
+        (LocalNative nativePriceQuote,) = deliveryProvider.quoteEvmDeliveryPrice(dstChainId, Gas.wrap(gasLimit), TargetNative.wrap(receiverValue));
+
+        require(expectedGasCost == LocalNative.unwrap(deliveryProvider.quoteGasCost(dstChainId, Gas.wrap(gasLimit))), "Gas cost is not what is expected");
+        require(expectedOverheadCost == LocalNative.unwrap(deliveryProvider.quoteGasCost(dstChainId, Gas.wrap(gasOverhead))), "Overhead cost is not what is expected");
+
+        require(expectedGasCost + expectedOverheadCost + expectedReceiverValueCost == LocalNative.unwrap(nativePriceQuote), "deliveryProvider price quote is not what is expected");
+
+    }
+}

+ 134 - 0
ethereum/forge-test/relayer/TestHelpers.sol

@@ -0,0 +1,134 @@
+// SPDX-License-Identifier: Apache 2
+
+pragma solidity ^0.8.0;
+
+import {IDeliveryProvider} from "../../contracts/interfaces/relayer/IDeliveryProviderTyped.sol";
+import {DeliveryProvider} from "../../contracts/relayer/deliveryProvider/DeliveryProvider.sol";
+import {DeliveryProviderSetup} from
+    "../../contracts/relayer/deliveryProvider/DeliveryProviderSetup.sol";
+import {DeliveryProviderImplementation} from
+    "../../contracts/relayer/deliveryProvider/DeliveryProviderImplementation.sol";
+import {DeliveryProviderProxy} from
+    "../../contracts/relayer/deliveryProvider/DeliveryProviderProxy.sol";
+import {IWormholeRelayer} from "../../contracts/interfaces/relayer/IWormholeRelayerTyped.sol";
+import {WormholeRelayer} from "../../contracts/relayer/wormholeRelayer/WormholeRelayer.sol";
+import {Create2Factory} from "../../contracts/relayer/create2Factory/Create2Factory.sol";
+import {MockGenericRelayer} from "./MockGenericRelayer.sol";
+import {MockWormhole} from "./MockWormhole.sol";
+import {IWormhole} from "../../contracts/interfaces/IWormhole.sol";
+import {WormholeSimulator, FakeWormholeSimulator} from "./WormholeSimulator.sol";
+import "../../contracts/libraries/external/BytesLib.sol";
+
+import "forge-std/Test.sol";
+import "forge-std/console.sol";
+import "forge-std/Vm.sol";
+
+contract TestHelpers {
+    using BytesLib for bytes;
+
+    address private constant VM_ADDRESS =
+        address(bytes20(uint160(uint256(keccak256("hevm cheat code")))));
+
+    Vm public constant vm = Vm(VM_ADDRESS);
+
+    WormholeSimulator helperWormholeSimulator;
+
+    constructor() {
+        (, helperWormholeSimulator) = setUpWormhole(1);
+    }
+
+    function registerWormholeRelayerContract(
+        WormholeRelayer governance,
+        IWormhole wormhole,
+        uint16 currentChainId,
+        uint16 chainId,
+        bytes32 coreRelayerContractAddress
+    ) public {
+        bytes32 wormholeRelayerModule =
+            0x0000000000000000000000000000000000576f726d686f6c6552656c61796572;
+        bytes memory message = abi.encodePacked(
+            wormholeRelayerModule, uint8(1), currentChainId, chainId, coreRelayerContractAddress
+        );
+        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 = helperWormholeSimulator.encodeAndSignMessage(preSignedMessage);
+        governance.registerWormholeRelayerContract(signed);
+    }
+
+    function setUpWormhole(uint16 chainId)
+        public
+        returns (IWormhole wormholeContract, WormholeSimulator wormholeSimulator)
+    {
+        // deploy Wormhole
+        MockWormhole wormhole = new MockWormhole({
+            initChainId: chainId,
+            initEvmChainId: block.chainid
+        });
+
+        // replace Wormhole with the Wormhole Simulator contract (giving access to some nice helper methods for signing)
+        wormholeSimulator = new FakeWormholeSimulator(
+            wormhole
+        );
+
+        wormholeContract = wormhole;
+    }
+
+    function setUpDeliveryProvider(
+        uint16 chainId
+    ) public returns (DeliveryProvider deliveryProvider) {
+        vm.prank(msg.sender);
+        DeliveryProviderSetup deliveryProviderSetup = new DeliveryProviderSetup();
+        vm.prank(msg.sender);
+        DeliveryProviderImplementation deliveryProviderImplementation =
+            new DeliveryProviderImplementation();
+        vm.prank(msg.sender);
+        DeliveryProviderProxy myDeliveryProvider = new DeliveryProviderProxy(
+            address(deliveryProviderSetup),
+            abi.encodeCall(
+                DeliveryProviderSetup.setup,
+                (
+                    address(deliveryProviderImplementation),
+                    chainId
+                )
+            )
+        );
+
+        deliveryProvider = DeliveryProvider(address(myDeliveryProvider));
+    }
+
+    function setUpWormholeRelayer(
+        IWormhole wormhole,
+        address defaultDeliveryProvider
+    ) public returns (IWormholeRelayer coreRelayer) {
+        Create2Factory create2Factory = new Create2Factory();
+
+        address proxyAddressComputed =
+            create2Factory.computeProxyAddress(address(this), "0xGenericRelayer");
+
+        WormholeRelayer coreRelayerImplementation = new WormholeRelayer(address(wormhole));
+
+        bytes memory initCall =
+            abi.encodeCall(WormholeRelayer.initialize, (defaultDeliveryProvider));
+
+        coreRelayer = IWormholeRelayer(
+            create2Factory.create2Proxy(
+                "0xGenericRelayer", address(coreRelayerImplementation), initCall
+            )
+        );
+        require(
+            address(coreRelayer) == proxyAddressComputed, "computed must match actual proxy addr"
+        );
+    }
+}

+ 121 - 0
ethereum/forge-test/relayer/TypedUnits.t.sol

@@ -0,0 +1,121 @@
+// SPDX-License-Identifier: UNLICENSED
+pragma solidity ^0.8.13;
+
+import "forge-std/Test.sol";
+import "forge-std/console.sol";
+import "../../contracts/interfaces/relayer/TypedUnits.sol";
+
+contract UVDTTest is Test {
+    using WeiLib for Wei;
+    using GasLib for Gas;
+    using DollarLib for Dollar;
+
+    function setUp() public {}
+
+    function testWeiBasic(uint64 x) public pure {
+        Wei w = Wei.wrap(x);
+        WeiPrice p = WeiPrice.wrap(100);
+        Dollar value = w.toDollars(p);
+
+        require(Dollar.unwrap(value) == uint256(x) * 100, "value should be 100*x");
+    }
+
+    function testWeiToGas(uint64 x) public pure {
+        Wei w = Wei.wrap(x);
+        GasPrice p = GasPrice.wrap(100);
+        Gas value = w.toGas(p);
+
+        require(Gas.unwrap(value) == uint256(x) / 100, "value should be x/100");
+    }
+
+    function testGasToWei(uint64 x) public pure {
+        Gas w = Gas.wrap(x);
+        GasPrice p = GasPrice.wrap(100);
+        Wei value = w.toWei(p);
+
+        require(Wei.unwrap(value) == uint256(x) * 100, "value should be 100*x");
+    }
+
+    function convertAsset(
+        uint64 source,
+        uint64 fromPrice,
+        uint64 toPrice,
+        uint32 multNum,
+        uint32 multDenom,
+        bool roundUp
+    ) public pure {
+        Wei w = Wei.wrap(source);
+        WeiPrice fp = WeiPrice.wrap(fromPrice);
+        WeiPrice tp = WeiPrice.wrap(toPrice);
+        Wei value = w.convertAsset(fp, tp, multNum, multDenom, roundUp);
+
+        uint256 expected;
+        if (roundUp) {
+            expected = (
+                uint256(source) * uint256(fromPrice) * multNum + (uint256(toPrice) * multDenom) - 1
+            ) / (uint256(toPrice) * multDenom);
+        } else {
+            expected =
+                (uint256(source) * uint256(fromPrice) * multNum) / (uint256(toPrice) * multDenom);
+        }
+
+        require(Wei.unwrap(value) == expected, "value should be expected");
+    }
+
+    function sourceWeiToTargetGas(uint64 sourceWei) public pure {
+        Wei w = Wei.wrap(sourceWei);
+
+        // gets smaller
+        {
+            WeiPrice fp = WeiPrice.wrap(10);
+            WeiPrice tp = WeiPrice.wrap(100);
+            GasPrice gp = GasPrice.wrap(5);
+            Gas targetGas = w.convertAsset(fp, tp, 1, 1, false).toGas(gp);
+            require(Gas.unwrap(targetGas) == sourceWei / 50, "targetGas should be 2");
+        }
+
+        // round up
+        {
+            WeiPrice fp = WeiPrice.wrap(100);
+            WeiPrice tp = WeiPrice.wrap(11);
+            GasPrice gp = GasPrice.wrap(5);
+            Gas targetGas = w.convertAsset(fp, tp, 1, 1, true).toGas(gp);
+            require(Gas.unwrap(targetGas) == sourceWei, "round down sourceWei * 1.8 => sourceWei");
+        }
+        // round down
+        {
+            WeiPrice fp = WeiPrice.wrap(100);
+            WeiPrice tp = WeiPrice.wrap(11);
+            GasPrice gp = GasPrice.wrap(5);
+            Gas targetGas = w.convertAsset(fp, tp, 1, 1, false).toGas(gp);
+            require(
+                Gas.unwrap(targetGas) == sourceWei * 2, "round up sourceWei * 1.8 => sourceWei * 2"
+            );
+        }
+    }
+
+    function testDollarToWei(uint128 x) public pure {
+        Dollar d = Dollar.wrap(x);
+        WeiPrice p = WeiPrice.wrap(100);
+        Wei value = d.toWei(p, false);
+
+        require(Wei.unwrap(value) == uint256(x) / 100, "value should be x/100");
+    }
+
+    function testDollarToGas(uint128 x) public pure {
+        Dollar d = Dollar.wrap(x);
+        GasPrice gp = GasPrice.wrap(1 << 32);
+        WeiPrice wp = WeiPrice.wrap(1 << 32);
+        Gas value = d.toGas(gp, wp);
+
+        require(Gas.unwrap(value) == uint256(x) / (1 << 64), "value should be x/(1<<32)");
+    }
+
+    function testGasMin(uint64 x, uint64 y) public pure {
+        Gas a = Gas.wrap(x);
+        Gas b = Gas.wrap(y);
+        Gas minVal = a.min(b);
+
+        require(Gas.unwrap(minVal) == (x < y ? x : y), "minVal should be min(x, y)");
+    }
+}

+ 2433 - 0
ethereum/forge-test/relayer/WormholeRelayer.t.sol

@@ -0,0 +1,2433 @@
+// SPDX-License-Identifier: Apache 2
+
+pragma solidity ^0.8.0;
+
+import {IDeliveryProvider} from "../../contracts/interfaces/relayer/IDeliveryProvider.sol";
+import {DeliveryProvider} from "../../contracts/relayer/deliveryProvider/DeliveryProvider.sol";
+import {DeliveryProviderSetup} from
+    "../../contracts/relayer/deliveryProvider/DeliveryProviderSetup.sol";
+import {DeliveryProviderImplementation} from
+    "../../contracts/relayer/deliveryProvider/DeliveryProviderImplementation.sol";
+import {DeliveryProviderProxy} from
+    "../../contracts/relayer/deliveryProvider/DeliveryProviderProxy.sol";
+import {DeliveryProviderStructs} from
+    "../../contracts/relayer/deliveryProvider/DeliveryProviderStructs.sol";
+import "../../contracts/interfaces/relayer/IWormholeRelayerTyped.sol";
+import {
+    DeliveryInstruction,
+    RedeliveryInstruction,
+    DeliveryOverride,
+    EvmDeliveryInstruction
+} from "../../contracts/libraries/relayer/RelayerInternalStructs.sol";
+import {WormholeRelayer} from "../../contracts/relayer/wormholeRelayer/WormholeRelayer.sol";
+import {MockGenericRelayer} from "./MockGenericRelayer.sol";
+import {MockWormhole} from "./MockWormhole.sol";
+import {IWormhole} from "../../contracts/interfaces/IWormhole.sol";
+import {WormholeSimulator, FakeWormholeSimulator} from "./WormholeSimulator.sol";
+import {IWormholeReceiver} from "../../contracts/interfaces/relayer/IWormholeReceiver.sol";
+import {AttackForwardIntegration} from "./AttackForwardIntegration.sol";
+import {
+    MockRelayerIntegration,
+    XAddress,
+    DeliveryData
+} from "../../contracts/mock/relayer/MockRelayerIntegration.sol";
+import {BigRevertBufferIntegration} from "./BigRevertBufferIntegration.sol";
+import {ForwardTester} from "./ForwardTester.sol";
+import {TestHelpers} from "./TestHelpers.sol";
+import {WormholeRelayerSerde} from "../../contracts/relayer/wormholeRelayer/WormholeRelayerSerde.sol";
+import {
+    EvmExecutionInfoV1,
+    ExecutionInfoVersion,
+    decodeEvmExecutionInfoV1,
+    encodeEvmExecutionInfoV1
+} from "../../contracts/libraries/relayer/ExecutionParameters.sol";
+import {toWormholeFormat, fromWormholeFormat} from "../../contracts/libraries/relayer/Utils.sol";
+import {BytesParsing} from "../../contracts/libraries/relayer/BytesParsing.sol";
+import "../../contracts/interfaces/relayer/TypedUnits.sol";
+
+import "forge-std/Test.sol";
+import "forge-std/console.sol";
+import "forge-std/Vm.sol";
+
+contract WormholeRelayerTests is Test {
+    using BytesParsing for bytes;
+    using WeiLib for Wei;
+    using GasLib for Gas;
+    using WeiPriceLib for WeiPrice;
+    using GasPriceLib for GasPrice;
+    using TargetNativeLib for TargetNative;
+    using LocalNativeLib for LocalNative;
+
+    Gas REASONABLE_GAS_LIMIT = Gas.wrap(500000);
+    Gas REASONABLE_GAS_LIMIT_FORWARDS = Gas.wrap(1000000);
+    Gas TOO_LOW_GAS_LIMIT = Gas.wrap(10000);
+
+    struct GasParameters {
+        uint32 evmGasOverhead;
+        uint32 targetGasLimit;
+        uint56 targetGasPrice;
+        uint56 sourceGasPrice;
+    }
+
+    struct FeeParameters {
+        uint56 targetNativePrice;
+        uint56 sourceNativePrice;
+        uint32 wormholeFeeOnSource;
+        uint32 wormholeFeeOnTarget;
+        uint64 receiverValueTarget;
+    }
+
+    struct GasParametersTyped {
+        Gas evmGasOverhead;
+        Gas targetGasLimit;
+        GasPrice targetGasPrice;
+        GasPrice sourceGasPrice;
+    }
+
+    struct FeeParametersTyped {
+        WeiPrice targetNativePrice;
+        WeiPrice sourceNativePrice;
+        Wei wormholeFeeOnSource;
+        Wei wormholeFeeOnTarget;
+        Wei receiverValueTarget;
+    }
+
+    function toGasParametersTyped(GasParameters memory gasParams)
+        internal
+        pure
+        returns (GasParametersTyped memory)
+    {
+        return GasParametersTyped({
+            evmGasOverhead: Gas.wrap(gasParams.evmGasOverhead),
+            targetGasLimit: Gas.wrap(gasParams.targetGasLimit),
+            targetGasPrice: GasPrice.wrap(gasParams.targetGasPrice),
+            sourceGasPrice: GasPrice.wrap(gasParams.sourceGasPrice)
+        });
+    }
+
+    function toFeeParametersTyped(FeeParameters memory feeParams)
+        internal
+        pure
+        returns (FeeParametersTyped memory)
+    {
+        return FeeParametersTyped({
+            targetNativePrice: WeiPrice.wrap(feeParams.targetNativePrice),
+            sourceNativePrice: WeiPrice.wrap(feeParams.sourceNativePrice),
+            wormholeFeeOnSource: Wei.wrap(feeParams.wormholeFeeOnSource),
+            wormholeFeeOnTarget: Wei.wrap(feeParams.wormholeFeeOnTarget),
+            receiverValueTarget: Wei.wrap(feeParams.receiverValueTarget)
+        });
+    }
+
+    IWormhole relayerWormhole;
+    WormholeSimulator relayerWormholeSimulator;
+    MockGenericRelayer genericRelayer;
+    TestHelpers helpers;
+
+    /**
+     *
+     *  SETUP
+     *
+     */
+
+    function setUp() public {
+        // deploy Wormhole
+        MockWormhole wormhole = new MockWormhole({
+            initChainId: 2,
+            initEvmChainId: block.chainid
+        });
+
+        relayerWormhole = wormhole;
+        relayerWormholeSimulator = new FakeWormholeSimulator(
+            wormhole
+        );
+
+        helpers = new TestHelpers();
+
+        genericRelayer =
+            new MockGenericRelayer(address(wormhole), address(relayerWormholeSimulator));
+
+        setUpChains(5);
+    }
+
+    struct StandardSetupTwoChains {
+        uint16 sourceChain;
+        uint16 targetChain;
+        uint16 differentChainId;
+        Contracts source;
+        Contracts target;
+    }
+
+    function standardAssume(
+        GasParameters memory gasParams,
+        FeeParameters memory feeParams,
+        uint32 minTargetGasLimit
+    ) public pure {
+        vm.assume(gasParams.evmGasOverhead > 0);
+        vm.assume(gasParams.targetGasLimit > 0);
+        vm.assume(feeParams.targetNativePrice > 0);
+        vm.assume(gasParams.targetGasPrice > 0);
+        vm.assume(gasParams.sourceGasPrice > 0);
+        vm.assume(feeParams.sourceNativePrice > 0);
+
+        vm.assume(
+            (
+                uint256(gasParams.sourceGasPrice) * feeParams.sourceNativePrice
+                    + feeParams.targetNativePrice - 1
+            ) / uint256(feeParams.targetNativePrice) < type(uint88).max
+        );
+        vm.assume(
+            (
+                uint256(gasParams.targetGasPrice) * feeParams.targetNativePrice
+                    + feeParams.sourceNativePrice - 1
+            ) / uint256(feeParams.sourceNativePrice) < type(uint88).max
+        );
+
+        vm.assume(
+            feeParams.targetNativePrice
+                < (uint256(2) ** 126)
+                    / (
+                        uint256(1) * gasParams.targetGasPrice
+                            * (uint256(0) + gasParams.targetGasLimit + gasParams.evmGasOverhead)
+                            + feeParams.wormholeFeeOnTarget
+                    )
+        );
+        vm.assume(
+            feeParams.sourceNativePrice
+                < (uint256(2) ** 126)
+                    / (
+                        uint256(1) * gasParams.sourceGasPrice
+                            * (uint256(gasParams.targetGasLimit) + gasParams.evmGasOverhead)
+                            + feeParams.wormholeFeeOnSource
+                    )
+        );
+
+        vm.assume(gasParams.targetGasLimit >= minTargetGasLimit);
+    }
+
+    function standardAssumeAndSetupTwoChains(
+        GasParameters memory gasParams_,
+        FeeParameters memory feeParams_,
+        uint32 minTargetGasLimit
+    ) public returns (StandardSetupTwoChains memory s) {
+        return standardAssumeAndSetupTwoChains(
+            gasParams_,
+            feeParams_,
+            Gas.wrap(minTargetGasLimit)
+        );
+    }
+
+    function standardAssumeAndSetupTwoChains(
+        GasParameters memory gasParams_,
+        FeeParameters memory feeParams_,
+        Gas minTargetGasLimit
+    ) public returns (StandardSetupTwoChains memory s) {
+        standardAssume(gasParams_, feeParams_, uint32(minTargetGasLimit.unwrap()));
+        GasParametersTyped memory gasParams = toGasParametersTyped(gasParams_);
+        FeeParametersTyped memory feeParams = toFeeParametersTyped(feeParams_);
+
+        s.sourceChain = 1;
+        s.targetChain = 2;
+        s.differentChainId = 3;
+        s.source = map[s.sourceChain];
+        s.target = map[s.targetChain];
+
+        vm.deal(s.source.relayer, type(uint256).max / 2);
+        vm.deal(s.target.relayer, type(uint256).max / 2);
+        vm.deal(address(this), type(uint256).max / 2);
+        vm.deal(address(s.target.integration), type(uint256).max / 2);
+        vm.deal(address(s.source.integration), type(uint256).max / 2);
+
+        // set deliveryProvider prices
+        s.source.deliveryProvider.updatePrice(
+            s.targetChain, gasParams.targetGasPrice, feeParams.targetNativePrice
+        );
+        s.source.deliveryProvider.updatePrice(
+            s.sourceChain, gasParams.sourceGasPrice, feeParams.sourceNativePrice
+        );
+        s.target.deliveryProvider.updatePrice(
+            s.targetChain, gasParams.targetGasPrice, feeParams.targetNativePrice
+        );
+        s.target.deliveryProvider.updatePrice(
+            s.sourceChain, gasParams.sourceGasPrice, feeParams.sourceNativePrice
+        );
+
+        s.source.deliveryProvider.updateDeliverGasOverhead(s.targetChain, gasParams.evmGasOverhead);
+        s.target.deliveryProvider.updateDeliverGasOverhead(s.sourceChain, gasParams.evmGasOverhead);
+
+        s.source.wormholeSimulator.setMessageFee(feeParams.wormholeFeeOnSource.unwrap());
+        s.target.wormholeSimulator.setMessageFee(feeParams.wormholeFeeOnTarget.unwrap());
+    }
+
+    struct Contracts {
+        IWormhole wormhole;
+        WormholeSimulator wormholeSimulator;
+        DeliveryProvider deliveryProvider;
+        IWormholeRelayer coreRelayer;
+        WormholeRelayer coreRelayerFull;
+        MockRelayerIntegration integration;
+        address relayer;
+        address payable rewardAddress;
+        address payable refundAddress;
+        uint16 chainId;
+    }
+
+    mapping(uint16 => Contracts) map;
+
+    function setUpChains(uint16 numChains) internal {
+        for (uint16 i = 1; i <= numChains; i++) {
+            Contracts memory mapEntry;
+            (mapEntry.wormhole, mapEntry.wormholeSimulator) = helpers.setUpWormhole(i);
+            mapEntry.deliveryProvider = helpers.setUpDeliveryProvider(i);
+            mapEntry.coreRelayer =
+                helpers.setUpWormholeRelayer(mapEntry.wormhole, address(mapEntry.deliveryProvider));
+            mapEntry.coreRelayerFull = WormholeRelayer(payable(address(mapEntry.coreRelayer)));
+            genericRelayer.setWormholeRelayerContract(i, address(mapEntry.coreRelayer));
+            mapEntry.integration =
+            new MockRelayerIntegration(address(mapEntry.wormhole), address(mapEntry.coreRelayer));
+            mapEntry.relayer =
+                address(uint160(uint256(keccak256(abi.encodePacked(bytes("relayer"), i)))));
+            genericRelayer.setProviderDeliveryAddress(i, mapEntry.relayer);
+            mapEntry.refundAddress = payable(
+                address(uint160(uint256(keccak256(abi.encodePacked(bytes("refundAddress"), i)))))
+            );
+            mapEntry.rewardAddress = payable(
+                address(uint160(uint256(keccak256(abi.encodePacked(bytes("rewardAddress"), i)))))
+            );
+            mapEntry.chainId = i;
+            map[i] = mapEntry;
+        }
+
+        uint192 maxBudget = uint192(2 ** 192 - 1);
+        for (uint16 i = 1; i <= numChains; i++) {
+            for (uint16 j = 1; j <= numChains; j++) {
+                map[i].deliveryProvider.updateSupportedChain(j, true);
+                map[i].deliveryProvider.updateAssetConversionBuffer(j, 500, 10000);
+                map[i].deliveryProvider.updateTargetChainAddress(
+                    j, bytes32(uint256(uint160(address(map[j].deliveryProvider))))
+                );
+                map[i].deliveryProvider.updateRewardAddress(map[i].rewardAddress);
+                helpers.registerWormholeRelayerContract(
+                    map[i].coreRelayerFull,
+                    map[i].wormhole,
+                    i,
+                    j,
+                    bytes32(uint256(uint160(address(map[j].coreRelayer))))
+                );
+                map[i].deliveryProvider.updateMaximumBudget(j, Wei.wrap(maxBudget));
+                map[i].integration.registerEmitter(
+                    j, bytes32(uint256(uint160(address(map[j].integration))))
+                );
+                XAddress[] memory addresses = new XAddress[](1);
+                addresses[0] = XAddress(j, bytes32(uint256(uint160(address(map[j].integration)))));
+                map[i].integration.registerEmitters(addresses);
+            }
+        }
+    }
+
+    function getDeliveryVAAHash(Vm.Log[] memory logs) internal pure returns (bytes32 vaaHash) {
+        (vaaHash,) = logs[0].data.asBytes32(0);
+    }
+
+    function getDeliveryStatus(Vm.Log memory log)
+        internal
+        pure
+        returns (IWormholeRelayerDelivery.DeliveryStatus status)
+    {
+        (uint256 parsed,) = log.data.asUint256(32);
+        status = IWormholeRelayerDelivery.DeliveryStatus(parsed);
+    }
+
+    function getDeliveryStatus()
+        internal
+        returns (IWormholeRelayerDelivery.DeliveryStatus status)
+    {
+        Vm.Log[] memory logs = vm.getRecordedLogs();
+        status = getDeliveryStatus(logs[logs.length - 1]);
+    }
+
+    function getRefundStatus(Vm.Log memory log)
+        internal
+        pure
+        returns (IWormholeRelayerDelivery.RefundStatus status)
+    {
+        (uint256 parsed,) = log.data.asUint256(32 + 32 + 32);
+        status = IWormholeRelayerDelivery.RefundStatus(parsed);
+    }
+
+    function getRefundStatus()
+        internal
+        returns (IWormholeRelayerDelivery.RefundStatus status)
+    {
+        Vm.Log[] memory logs = vm.getRecordedLogs();
+        status = getRefundStatus(logs[logs.length - 1]);
+    }
+
+    function vaaKeyArray(
+        uint16 chainId,
+        uint64 sequence,
+        address emitterAddress
+    ) internal pure returns (VaaKey[] memory vaaKeys) {
+        vaaKeys = new VaaKey[](1);
+        vaaKeys[0] = VaaKey(chainId, toWormholeFormat(emitterAddress), sequence);
+    }
+
+    function sendMessageToTargetChain(
+        StandardSetupTwoChains memory setup,
+        Gas gasLimit,
+        uint128 receiverValue,
+        bytes memory message
+    ) internal returns (uint64 sequence) {
+        return sendMessageToTargetChain(
+            setup,
+            uint32(gasLimit.unwrap()),
+            receiverValue,
+            message
+        );
+    }
+
+    function sendMessageToTargetChain(
+        StandardSetupTwoChains memory setup,
+        uint32 gasLimit,
+        uint128 receiverValue,
+        bytes memory message
+    ) internal returns (uint64 sequence) {
+        (LocalNative deliveryCost,) = setup.source.coreRelayer.quoteEVMDeliveryPrice(
+            setup.targetChain, TargetNative.wrap(receiverValue), Gas.wrap(gasLimit)
+        );
+        sequence = setup.source.integration.sendMessage{
+            value: LocalNative.unwrap(deliveryCost)
+        }(message, setup.targetChain, gasLimit, receiverValue);
+    }
+
+    function sendMessageToTargetChainExpectingForwardedResponse(
+        StandardSetupTwoChains memory setup,
+        uint32 gasLimit,
+        uint128 receiverValue,
+        bytes memory message,
+        bytes memory forwardedMessage,
+        bool forwardShouldSucceed
+    ) internal returns (uint64 sequence) {
+        (LocalNative forwardDeliveryCost,) = setup.target.coreRelayer.quoteEVMDeliveryPrice(
+            setup.sourceChain, TargetNative.wrap(0), REASONABLE_GAS_LIMIT
+        );
+        uint256 neededReceiverValue = forwardDeliveryCost.unwrap();
+        vm.assume(neededReceiverValue <= type(uint128).max);
+        if (forwardShouldSucceed) {
+            vm.assume(receiverValue >= neededReceiverValue);
+        } else {
+            vm.assume(receiverValue < neededReceiverValue);
+        }
+
+        (LocalNative deliveryCost,) = setup.source.coreRelayer.quoteEVMDeliveryPrice(
+            setup.targetChain, TargetNative.wrap(receiverValue), Gas.wrap(gasLimit)
+        );
+
+        sequence = setup.source.integration.sendMessageWithForwardedResponse{
+            value: deliveryCost.unwrap()
+        }(message, forwardedMessage, setup.targetChain, gasLimit, receiverValue);
+    }
+
+    function resendMessageToTargetChain(
+        StandardSetupTwoChains memory setup,
+        uint64 sequence,
+        uint32 gasLimit,
+        uint128 receiverValue,
+        bytes memory 
+    ) internal {
+        (LocalNative newDeliveryCost,) = setup.source.coreRelayer.quoteEVMDeliveryPrice(
+            setup.targetChain, TargetNative.wrap(receiverValue), Gas.wrap(gasLimit)
+        );
+
+        setup.source.integration.resend{value: newDeliveryCost.unwrap()}(
+            setup.sourceChain, sequence, setup.targetChain, gasLimit, receiverValue
+        );
+    }
+
+    /**
+     *
+     * TEST SUITE!
+     *
+     */
+
+    /**
+     * Basic Functionality Tests: Send, Forward, and Resend
+     */
+
+    function testSend(
+        GasParameters memory gasParams,
+        FeeParameters memory feeParams,
+        bytes memory message
+    ) public {
+        StandardSetupTwoChains memory setup =
+            standardAssumeAndSetupTwoChains(gasParams, feeParams, REASONABLE_GAS_LIMIT);
+
+        vm.recordLogs();
+
+        sendMessageToTargetChain(setup, gasParams.targetGasLimit, 0, message);
+
+        genericRelayer.relay(setup.sourceChain);
+
+        assertTrue(keccak256(setup.target.integration.getMessage()) == keccak256(message));
+        assertTrue(getDeliveryStatus() == IWormholeRelayerDelivery.DeliveryStatus.SUCCESS);
+    }
+
+    function testForward(
+        GasParameters memory gasParams,
+        FeeParameters memory feeParams,
+        bytes memory message,
+        bytes memory forwardedMessage
+    ) public {
+        StandardSetupTwoChains memory setup =
+            standardAssumeAndSetupTwoChains(gasParams, feeParams, REASONABLE_GAS_LIMIT_FORWARDS);
+
+        vm.recordLogs();
+
+        sendMessageToTargetChainExpectingForwardedResponse(
+            setup,
+            gasParams.targetGasLimit,
+            feeParams.receiverValueTarget,
+            message,
+            forwardedMessage,
+            true
+        );
+
+        genericRelayer.relay(setup.sourceChain);
+
+        assertTrue(keccak256(setup.target.integration.getMessage()) == keccak256(message));
+
+        genericRelayer.relay(setup.targetChain);
+
+        assertTrue(keccak256(setup.source.integration.getMessage()) == keccak256(forwardedMessage));
+    }
+
+    function testResend(
+        GasParameters memory gasParams,
+        FeeParameters memory feeParams,
+        bytes memory message
+    ) public {
+        StandardSetupTwoChains memory setup =
+            standardAssumeAndSetupTwoChains(gasParams, feeParams, REASONABLE_GAS_LIMIT);
+
+        vm.recordLogs();
+
+        vm.assume(keccak256(message) != keccak256(bytes("")));
+
+        uint64 sequence = sendMessageToTargetChain(setup, TOO_LOW_GAS_LIMIT, 0, message);
+
+        genericRelayer.relay(setup.sourceChain);
+
+        assertTrue(keccak256(setup.target.integration.getMessage()) != keccak256(message));
+        assertTrue(getDeliveryStatus() == IWormholeRelayerDelivery.DeliveryStatus.RECEIVER_FAILURE);
+
+        resendMessageToTargetChain(setup, sequence, uint32(REASONABLE_GAS_LIMIT.unwrap()), 0, message);
+
+        genericRelayer.relay(setup.sourceChain);
+
+        assertTrue(keccak256(setup.target.integration.getMessage()) == keccak256(message));
+        assertTrue(getDeliveryStatus() == IWormholeRelayerDelivery.DeliveryStatus.SUCCESS);
+    }
+
+    /**
+     * More functionality tests
+     */
+
+    function testForwardFailure(
+        GasParameters memory gasParams,
+        FeeParameters memory feeParams
+    ) public {
+        feeParams.receiverValueTarget = 0;
+        vm.assume(
+            uint256(20) * feeParams.targetNativePrice * gasParams.targetGasPrice
+                < uint256(1) * feeParams.sourceNativePrice * gasParams.sourceGasPrice
+        );
+
+        vm.recordLogs();
+        gasParams.targetGasLimit = 600000;
+        StandardSetupTwoChains memory setup =
+            standardAssumeAndSetupTwoChains(gasParams, feeParams, Gas.wrap(gasParams.targetGasLimit));
+
+        sendMessageToTargetChainExpectingForwardedResponse(
+            setup,
+            gasParams.targetGasLimit,
+            feeParams.receiverValueTarget,
+            bytes("Hello!"),
+            bytes("Forwarded Message!"),
+            false
+        );
+
+        genericRelayer.relay(setup.sourceChain);
+
+        assertTrue(keccak256(setup.target.integration.getMessage()) != keccak256(bytes("Hello!")));
+        assertTrue(
+            getDeliveryStatus() == IWormholeRelayerDelivery.DeliveryStatus.FORWARD_REQUEST_FAILURE
+        );
+    }
+
+    function testMultipleForwards(
+        GasParameters memory gasParams,
+        FeeParameters memory feeParams,
+        bytes memory message,
+        bytes memory forwardedMessage
+    ) public {
+        StandardSetupTwoChains memory setup =
+            standardAssumeAndSetupTwoChains(gasParams, feeParams, REASONABLE_GAS_LIMIT);
+
+        (LocalNative firstForwardDeliveryCost,) = setup.target.coreRelayer.quoteEVMDeliveryPrice(
+            setup.sourceChain, TargetNative.wrap(0), REASONABLE_GAS_LIMIT
+        );
+        (LocalNative secondForwardDeliveryCost,) = setup.target.coreRelayer.quoteEVMDeliveryPrice(
+            setup.targetChain, TargetNative.wrap(0), REASONABLE_GAS_LIMIT
+        );
+
+        uint256 receiverValue = firstForwardDeliveryCost.unwrap() + secondForwardDeliveryCost.unwrap();
+        vm.assume(receiverValue <= type(uint128).max);
+
+        (LocalNative deliveryCost,) = setup.source.coreRelayer.quoteEVMDeliveryPrice(
+            setup.targetChain, TargetNative.wrap(receiverValue), Gas.wrap(REASONABLE_GAS_LIMIT_FORWARDS.unwrap() * 2)
+        );
+
+        vm.recordLogs();
+
+        setup.source.integration.sendMessageWithMultiForwardedResponse{
+            value: deliveryCost.unwrap()
+        }(
+            message,
+            forwardedMessage,
+            setup.targetChain,
+            uint32(REASONABLE_GAS_LIMIT_FORWARDS.unwrap() * 2),
+            uint128(receiverValue)
+        );
+
+        genericRelayer.relay(setup.sourceChain);
+
+        assertTrue(keccak256(setup.target.integration.getMessage()) == keccak256(message));
+
+        genericRelayer.relay(setup.targetChain);
+
+        assertTrue(keccak256(setup.source.integration.getMessage()) == keccak256(forwardedMessage));
+
+        assertTrue(keccak256(setup.target.integration.getMessage()) == keccak256(forwardedMessage));
+    }
+
+    function testResendFailAndSucceed(
+        GasParameters memory gasParams,
+        FeeParameters memory feeParams,
+        bytes memory message
+    ) public {
+        StandardSetupTwoChains memory setup =
+            standardAssumeAndSetupTwoChains(gasParams, feeParams, REASONABLE_GAS_LIMIT);
+
+        vm.recordLogs();
+
+        vm.assume(keccak256(message) != keccak256(bytes("")));
+
+        uint64 sequence = sendMessageToTargetChain(setup, TOO_LOW_GAS_LIMIT, 0, message);
+
+        genericRelayer.relay(setup.sourceChain);
+
+        assertTrue(keccak256(setup.target.integration.getMessage()) != keccak256(message));
+        assertTrue(getDeliveryStatus() == IWormholeRelayerDelivery.DeliveryStatus.RECEIVER_FAILURE);
+
+        for (uint32 i = 2; i < 10; i++) {
+            resendMessageToTargetChain(setup, sequence, uint32(TOO_LOW_GAS_LIMIT.unwrap() * i), 0, message);
+            genericRelayer.relay(setup.sourceChain);
+            assertTrue(keccak256(setup.target.integration.getMessage()) != keccak256(message));
+            assertTrue(
+                getDeliveryStatus() == IWormholeRelayerDelivery.DeliveryStatus.RECEIVER_FAILURE
+            );
+        }
+
+        resendMessageToTargetChain(setup, sequence, uint32(REASONABLE_GAS_LIMIT.unwrap()), 0, message);
+
+        genericRelayer.relay(setup.sourceChain);
+
+        assertTrue(keccak256(setup.target.integration.getMessage()) == keccak256(message));
+        assertTrue(getDeliveryStatus() == IWormholeRelayerDelivery.DeliveryStatus.SUCCESS);
+    }
+
+    /**
+     * Funds correct test (testing each address receives the correct payment)
+     */
+
+    struct FundsCorrectTest {
+        uint256 refundAddressBalance;
+        uint256 relayerBalance;
+        uint256 rewardAddressBalance;
+        uint256 destinationBalance;
+        uint256 sourceContractBalance;
+        uint256 targetContractBalance;
+        uint128 receiverValue;
+        uint256 deliveryPrice;
+        uint256 targetChainRefundPerGasUnused;
+        uint32 gasAmount;
+        uint256 refundAddressAmount;
+        uint256 relayerPayment;
+        uint256 rewardAddressAmount;
+        uint256 destinationAmount;
+    }
+
+    function setupFundsCorrectTest(
+        GasParameters memory gasParams_,
+        FeeParameters memory feeParams_,
+        Gas minGasLimit
+    ) public returns (StandardSetupTwoChains memory s, FundsCorrectTest memory test) {
+        return setupFundsCorrectTest(gasParams_, feeParams_, uint32(minGasLimit.unwrap()));
+    }
+
+    function setupFundsCorrectTest(
+        GasParameters memory gasParams_,
+        FeeParameters memory feeParams_,
+        uint32 minGasLimit
+    ) public returns (StandardSetupTwoChains memory s, FundsCorrectTest memory test) {
+        s = standardAssumeAndSetupTwoChains(gasParams_, feeParams_, Gas.wrap(minGasLimit));
+
+        test.refundAddressBalance = s.target.refundAddress.balance;
+        test.relayerBalance = s.target.relayer.balance;
+        test.rewardAddressBalance = s.source.rewardAddress.balance;
+        test.destinationBalance = address(s.target.integration).balance;
+        test.sourceContractBalance = address(s.source.coreRelayer).balance;
+        test.targetContractBalance = address(s.target.coreRelayer).balance;
+        test.receiverValue = feeParams_.receiverValueTarget;
+        (LocalNative deliveryPrice, GasPrice targetChainRefundPerGasUnused) = s
+            .source
+            .coreRelayer
+            .quoteEVMDeliveryPrice(s.targetChain, TargetNative.wrap(test.receiverValue), Gas.wrap(gasParams_.targetGasLimit));
+        test.deliveryPrice = deliveryPrice.unwrap();
+        test.targetChainRefundPerGasUnused = targetChainRefundPerGasUnused.unwrap();
+        vm.assume(test.targetChainRefundPerGasUnused > 0);
+    }
+
+    function testFundsCorrectForASend(
+        GasParameters memory gasParams,
+        FeeParameters memory feeParams
+    ) public {
+        vm.recordLogs();
+        (StandardSetupTwoChains memory setup, FundsCorrectTest memory test) =
+            setupFundsCorrectTest(gasParams, feeParams, 170000);
+
+        setup.source.integration.sendMessageWithRefund{
+            value: test.deliveryPrice
+        }(
+            bytes("Hello!"),
+            setup.targetChain,
+            gasParams.targetGasLimit,
+            test.receiverValue,
+            setup.targetChain,
+            setup.target.refundAddress
+        );
+
+        genericRelayer.relay(setup.sourceChain);
+
+        assertTrue(keccak256(setup.target.integration.getMessage()) == keccak256("Hello!"));
+        assertTrue(getDeliveryStatus() == IWormholeRelayerDelivery.DeliveryStatus.SUCCESS);
+
+        test.refundAddressAmount = setup.target.refundAddress.balance - test.refundAddressBalance;
+        test.rewardAddressAmount = setup.source.rewardAddress.balance - test.rewardAddressBalance;
+        test.relayerPayment = test.relayerBalance - setup.target.relayer.balance;
+        test.destinationAmount = address(setup.target.integration).balance - test.destinationBalance;
+
+        assertTrue(test.sourceContractBalance == address(setup.source.coreRelayer).balance);
+        assertTrue(test.targetContractBalance == address(setup.target.coreRelayer).balance);
+        assertTrue(
+            test.destinationAmount == test.receiverValue, "Receiver value was sent to the contract"
+        );
+        assertTrue(
+            test.rewardAddressAmount + feeParams.wormholeFeeOnSource == test.deliveryPrice, "Reward address was paid correctly"
+        );
+
+        test.gasAmount = uint32(
+            gasParams.targetGasLimit - test.refundAddressAmount / test.targetChainRefundPerGasUnused
+        );
+        console.log(test.gasAmount);
+        assertTrue(
+            test.gasAmount >= 140000,
+            "Gas amount (calculated from refund address payment) lower than expected. NOTE: This assert is purely to ensure the gas usage is consistent, and thus (since this was computed using the refund amount) the refund amount is correct."
+        );
+        assertTrue(
+            test.gasAmount <= 160000,
+            "Gas amount (calculated from refund address payment) higher than expected. NOTE: This assert is purely to ensure the gas usage is consistent, and thus (since this was computed using the refund amount) the refund amount is correct."
+        );
+        assertTrue(
+            test.relayerPayment == test.destinationAmount + test.refundAddressAmount,
+            "Relayer paid the correct amount"
+        );
+    }
+
+    function testFundsCorrectForASendFailureDueToGasExceeded(
+        GasParameters memory gasParams,
+        FeeParameters memory feeParams
+    ) public {
+        vm.recordLogs();
+        gasParams.targetGasLimit = uint32(TOO_LOW_GAS_LIMIT.unwrap());
+        (StandardSetupTwoChains memory setup, FundsCorrectTest memory test) =
+            setupFundsCorrectTest(gasParams, feeParams, 0);
+
+        setup.source.integration.sendMessageWithRefund{
+            value: test.deliveryPrice
+        }(
+            bytes("Hello!"),
+            setup.targetChain,
+            gasParams.targetGasLimit,
+            test.receiverValue,
+            setup.targetChain,
+            setup.target.refundAddress
+        );
+
+        genericRelayer.relay(setup.sourceChain);
+
+        assertTrue(keccak256(setup.target.integration.getMessage()) != keccak256(bytes("Hello!")));
+        assertTrue(getDeliveryStatus() == IWormholeRelayerDelivery.DeliveryStatus.RECEIVER_FAILURE);
+
+        test.refundAddressAmount = setup.target.refundAddress.balance - test.refundAddressBalance;
+        test.rewardAddressAmount = setup.source.rewardAddress.balance - test.rewardAddressBalance;
+        test.relayerPayment = test.relayerBalance - setup.target.relayer.balance;
+        test.destinationAmount = address(setup.target.integration).balance - test.destinationBalance;
+
+        assertTrue(test.sourceContractBalance == address(setup.source.coreRelayer).balance);
+        assertTrue(test.targetContractBalance == address(setup.target.coreRelayer).balance);
+        assertTrue(test.destinationAmount == 0, "No receiver value was sent to the contract");
+        assertTrue(
+            test.rewardAddressAmount + feeParams.wormholeFeeOnSource == test.deliveryPrice, "Reward address was paid correctly"
+        );
+        assertTrue(test.refundAddressAmount == test.receiverValue, "Receiver value was refunded");
+        assertTrue(
+            test.relayerPayment == test.destinationAmount + test.refundAddressAmount,
+            "Relayer paid the correct amount"
+        );
+    }
+
+    function testFundsCorrectForASendFailureDueToRevert(
+        GasParameters memory gasParams,
+        FeeParameters memory feeParams
+    ) public {
+        vm.recordLogs();
+        (StandardSetupTwoChains memory setup, FundsCorrectTest memory test) =
+            setupFundsCorrectTest(gasParams, feeParams, REASONABLE_GAS_LIMIT_FORWARDS);
+
+        setup.target.deliveryProvider.updateSupportedChain(1, false);
+
+        setup.source.integration.sendMessageWithForwardedResponse{
+            value: test.deliveryPrice
+        }(
+            bytes("Hello!"),
+            bytes("Forwarded Message"),
+            setup.targetChain,
+            gasParams.targetGasLimit,
+            test.receiverValue,
+            setup.targetChain,
+            setup.target.refundAddress
+        );
+
+        genericRelayer.relay(setup.sourceChain);
+
+        assertTrue(keccak256(setup.target.integration.getMessage()) != keccak256(bytes("Hello!")));
+        assertTrue(getDeliveryStatus() == IWormholeRelayerDelivery.DeliveryStatus.RECEIVER_FAILURE);
+
+        test.refundAddressAmount = setup.target.refundAddress.balance - test.refundAddressBalance;
+        test.rewardAddressAmount = setup.source.rewardAddress.balance - test.rewardAddressBalance;
+        test.relayerPayment = test.relayerBalance - setup.target.relayer.balance;
+        test.destinationAmount = address(setup.target.integration).balance - test.destinationBalance;
+
+        assertTrue(test.sourceContractBalance == address(setup.source.coreRelayer).balance);
+        assertTrue(test.targetContractBalance == address(setup.target.coreRelayer).balance);
+        assertTrue(test.destinationAmount == 0, "No receiver value was sent to the contract");
+        assertTrue(
+            test.rewardAddressAmount + feeParams.wormholeFeeOnSource == test.deliveryPrice, "Reward address was paid correctly"
+        );
+        test.gasAmount = uint32(
+            gasParams.targetGasLimit
+                - (test.refundAddressAmount - test.receiverValue) / test.targetChainRefundPerGasUnused
+        );
+        console.log(test.gasAmount);
+        assertTrue(
+            test.gasAmount >= 165000,
+            "Gas amount (calculated from refund address payment) lower than expected. NOTE: This assert is purely to ensure the gas usage is consistent, and thus (since this was computed using the refund amount) the refund amount is correct."
+        );
+        assertTrue(
+            test.gasAmount <= 190000,
+            "Gas amount (calculated from refund address payment) higher than expected. NOTE: This assert is purely to ensure the gas usage is consistent, and thus (since this was computed using the refund amount) the refund amount is correct."
+        );
+        assertTrue(
+            test.relayerPayment == test.destinationAmount + test.refundAddressAmount,
+            "Relayer paid the correct amount"
+        );
+    }
+
+    function testFundsCorrectForAForward(
+        GasParameters memory gasParams,
+        FeeParameters memory feeParams
+    ) public {
+        vm.recordLogs();
+        (StandardSetupTwoChains memory setup, FundsCorrectTest memory test) =
+            setupFundsCorrectTest(gasParams, feeParams, REASONABLE_GAS_LIMIT_FORWARDS);
+
+        (LocalNative forwardDeliveryCost,) = setup.target.coreRelayer.quoteEVMDeliveryPrice(
+            setup.sourceChain, TargetNative.wrap(0), REASONABLE_GAS_LIMIT
+        );
+        uint256 receiverValue = forwardDeliveryCost.unwrap();
+        vm.assume(receiverValue <= type(uint128).max);
+        vm.assume(feeParams.receiverValueTarget >= receiverValue);
+
+        uint256 rewardAddressBalanceTarget = setup.target.rewardAddress.balance;
+
+        setup.source.integration.sendMessageWithForwardedResponse{
+            value: test.deliveryPrice 
+        }(
+            bytes("Hello!"),
+            bytes("Forwarded Message!"),
+            setup.targetChain,
+            gasParams.targetGasLimit,
+            test.receiverValue,
+            setup.targetChain,
+            setup.target.refundAddress
+        );
+
+        genericRelayer.relay(setup.sourceChain);
+
+        assertTrue(keccak256(setup.target.integration.getMessage()) == keccak256(bytes("Hello!")));
+
+        genericRelayer.relay(setup.targetChain);
+
+        assertTrue(
+            keccak256(setup.source.integration.getMessage())
+                == keccak256(bytes("Forwarded Message!"))
+        );
+
+        test.refundAddressAmount = setup.target.refundAddress.balance - test.refundAddressBalance;
+
+        test.rewardAddressAmount = setup.source.rewardAddress.balance - test.rewardAddressBalance;
+
+        test.relayerPayment = test.relayerBalance - setup.target.relayer.balance;
+
+        test.destinationAmount = test.destinationBalance - address(setup.target.integration).balance;
+
+        assertTrue(
+            test.sourceContractBalance == address(setup.source.coreRelayer).balance,
+            "Source contract has extra balance"
+        );
+        assertTrue(
+            test.targetContractBalance == address(setup.target.coreRelayer).balance,
+            "Target contract has extra balance"
+        );
+        assertTrue(test.refundAddressAmount == 0, "All refund amount was forwarded");
+        assertTrue(test.destinationAmount == 0, "All receiver amount was sent to forward");
+        assertTrue(
+            test.rewardAddressAmount + feeParams.wormholeFeeOnSource == test.deliveryPrice,
+            "Source reward address was paid correctly"
+        );
+
+        uint256 refundIntermediate = (
+            setup.target.rewardAddress.balance - rewardAddressBalanceTarget
+        ) + feeParams.wormholeFeeOnTarget - test.receiverValue;
+
+        test.gasAmount = uint32(
+            gasParams.targetGasLimit - refundIntermediate / test.targetChainRefundPerGasUnused
+        );
+
+        console.log(test.gasAmount);
+
+        assertTrue(
+            test.relayerPayment == test.receiverValue + refundIntermediate,
+            "Relayer paid the correct amount"
+        );
+        assertTrue(
+            test.gasAmount >= 500000,
+            "Gas amount (calculated from refund address payment) lower than expected. NOTE: This assert is purely to ensure the gas usage is consistent, and thus (since this was computed using the refund amount) the refund amount is correct."
+        );
+        assertTrue(
+            test.gasAmount <= 600000,
+            "Gas amount (calculated from refund address payment) higher than expected. NOTE: This assert is purely to ensure the gas usage is consistent, and thus (since this was computed using the refund amount) the refund amount is correct."
+        );
+    }
+
+    function testFundsCorrectForAForwardFailure(
+        GasParameters memory gasParams,
+        FeeParameters memory feeParams
+    ) public {
+        feeParams.receiverValueTarget = 0;
+        vm.assume(
+            uint256(20) * feeParams.targetNativePrice * gasParams.targetGasPrice
+                < uint256(1) * feeParams.sourceNativePrice * gasParams.sourceGasPrice
+        );
+
+        vm.recordLogs();
+        gasParams.targetGasLimit = 600000;
+        (StandardSetupTwoChains memory setup, FundsCorrectTest memory test) =
+            setupFundsCorrectTest(gasParams, feeParams, 600000);
+
+        (LocalNative forwardDeliveryCost,) =
+            setup.target.coreRelayer.quoteEVMDeliveryPrice(setup.sourceChain, TargetNative.wrap(0), REASONABLE_GAS_LIMIT);
+        uint256 receiverValue = forwardDeliveryCost.unwrap();
+        vm.assume(receiverValue <= type(uint128).max);
+        vm.assume(feeParams.receiverValueTarget < receiverValue);
+
+        setup.source.integration.sendMessageWithForwardedResponse{
+            value: test.deliveryPrice
+        }(
+            bytes("Hello!"),
+            bytes("Forwarded Message!"),
+            setup.targetChain,
+            gasParams.targetGasLimit,
+            test.receiverValue,
+            setup.targetChain,
+            setup.target.refundAddress
+        );
+
+        genericRelayer.relay(setup.sourceChain);
+
+        assertTrue(keccak256(setup.target.integration.getMessage()) != keccak256(bytes("Hello!")));
+        assertTrue(
+            getDeliveryStatus() == IWormholeRelayerDelivery.DeliveryStatus.FORWARD_REQUEST_FAILURE
+        );
+
+        test.refundAddressAmount = setup.target.refundAddress.balance - test.refundAddressBalance;
+
+        test.rewardAddressAmount = setup.source.rewardAddress.balance - test.rewardAddressBalance;
+
+        test.relayerPayment = test.relayerBalance - setup.target.relayer.balance;
+
+        test.destinationAmount = test.destinationBalance - address(setup.target.integration).balance;
+
+        assertTrue(
+            test.sourceContractBalance == address(setup.source.coreRelayer).balance,
+            "Source contract has extra balance"
+        );
+        assertTrue(
+            test.targetContractBalance == address(setup.target.coreRelayer).balance,
+            "Target contract has extra balance"
+        );
+        assertTrue(test.destinationAmount == 0, "No receiver value was sent to contract");
+        assertTrue(
+            test.rewardAddressAmount + feeParams.wormholeFeeOnSource == test.deliveryPrice,
+            "Source reward address was paid correctly"
+        );
+
+        test.gasAmount = uint32(
+            gasParams.targetGasLimit
+                - (test.refundAddressAmount - test.receiverValue) / test.targetChainRefundPerGasUnused
+        );
+
+        console.log(test.gasAmount);
+
+        assertTrue(
+            test.relayerPayment == test.refundAddressAmount, "Relayer paid the correct amount"
+        );
+        assertTrue(
+            test.gasAmount >= 500000,
+            "Gas amount (calculated from refund address payment) lower than expected. NOTE: This assert is purely to ensure the gas usage is consistent, and thus (since this was computed using the refund amount) the refund amount is correct."
+        );
+        assertTrue(
+            test.gasAmount <= 600000,
+            "Gas amount (calculated from refund address payment) higher than expected. NOTE: This assert is purely to ensure the gas usage is consistent, and thus (since this was computed using the refund amount) the refund amount is correct."
+        );
+    }
+
+    function testFundsCorrectForAResend(
+        GasParameters memory gasParams,
+        FeeParameters memory feeParams
+    ) public {
+        vm.recordLogs();
+        (StandardSetupTwoChains memory setup, FundsCorrectTest memory test) =
+            setupFundsCorrectTest(gasParams, feeParams, 170000);
+
+        (LocalNative notEnoughDeliveryPrice,) =
+            setup.source.coreRelayer.quoteEVMDeliveryPrice(setup.targetChain, TargetNative.wrap(0), TOO_LOW_GAS_LIMIT);
+
+        uint64 sequence = setup.source.integration.sendMessageWithRefund{
+            value: notEnoughDeliveryPrice.unwrap()
+        }(
+            bytes("Hello!"),
+            setup.targetChain,
+            uint32(TOO_LOW_GAS_LIMIT.unwrap()),
+            0,
+            setup.targetChain,
+            setup.target.refundAddress
+        );
+
+        genericRelayer.relay(setup.sourceChain);
+        assertTrue(keccak256(setup.target.integration.getMessage()) != keccak256(bytes("Hello!")));
+        assertTrue(getDeliveryStatus() == IWormholeRelayerDelivery.DeliveryStatus.RECEIVER_FAILURE);
+
+        (, test) = setupFundsCorrectTest(gasParams, feeParams, 170000);
+
+        //call a resend for the orignal message
+        setup.source.integration.resend{
+            value: test.deliveryPrice
+        }(
+            setup.sourceChain,
+            sequence,
+            setup.targetChain,
+            gasParams.targetGasLimit,
+            test.receiverValue
+        );
+
+        genericRelayer.relay(setup.sourceChain);
+
+        assertTrue(keccak256(setup.target.integration.getMessage()) == keccak256(bytes("Hello!")));
+        assertTrue(getDeliveryStatus() == IWormholeRelayerDelivery.DeliveryStatus.SUCCESS);
+
+        test.refundAddressAmount = setup.target.refundAddress.balance - test.refundAddressBalance;
+        test.rewardAddressAmount = setup.source.rewardAddress.balance - test.rewardAddressBalance;
+        test.relayerPayment = test.relayerBalance - setup.target.relayer.balance;
+        test.destinationAmount = address(setup.target.integration).balance - test.destinationBalance;
+
+        assertTrue(test.sourceContractBalance == address(setup.source.coreRelayer).balance);
+        assertTrue(test.targetContractBalance == address(setup.target.coreRelayer).balance);
+        console.log(test.destinationAmount);
+        console.log(feeParams.receiverValueTarget);
+        assertTrue(
+            test.destinationAmount == feeParams.receiverValueTarget,
+            "Receiver value was sent to the contract"
+        );
+        assertTrue(
+            test.rewardAddressAmount + feeParams.wormholeFeeOnSource == test.deliveryPrice, "Reward address was paid correctly"
+        );
+        test.gasAmount = uint32(
+            gasParams.targetGasLimit - test.refundAddressAmount / test.targetChainRefundPerGasUnused
+        );
+        console.log(test.gasAmount);
+        assertTrue(
+            test.gasAmount >= 135000,
+            "Gas amount (calculated from refund address payment) lower than expected. NOTE: This assert is purely to ensure the gas usage is consistent, and thus (since this was computed using the refund amount) the refund amount is correct."
+        );
+        assertTrue(
+            test.gasAmount <= 160000,
+            "Gas amount (calculated from refund address payment) higher than expected. NOTE: This assert is purely to ensure the gas usage is consistent, and thus (since this was computed using the refund amount) the refund amount is correct."
+        );
+        assertTrue(
+            test.relayerPayment == test.destinationAmount + test.refundAddressAmount,
+            "Relayer paid the correct amount"
+        );
+    }
+
+    function testFundsCorrectForASendCrossChainRefundSuccess(
+        GasParameters memory gasParams,
+        FeeParameters memory feeParams
+    ) public {
+        vm.recordLogs();
+        (StandardSetupTwoChains memory setup, FundsCorrectTest memory test) =
+            setupFundsCorrectTest(gasParams, feeParams, 170000);
+
+        uint256 refundRewardAddressBalance = setup.target.rewardAddress.balance;
+        uint256 refundAddressBalance = setup.source.refundAddress.balance;
+
+        setup.source.integration.sendMessageWithRefund{
+            value: test.deliveryPrice
+        }(
+            bytes("Hello!"),
+            setup.targetChain,
+            gasParams.targetGasLimit,
+            test.receiverValue,
+            setup.sourceChain,
+            setup.source.refundAddress
+        );
+
+        genericRelayer.relay(setup.sourceChain);
+
+        assertTrue(keccak256(setup.target.integration.getMessage()) == keccak256(bytes("Hello!")));
+
+        genericRelayer.relay(setup.targetChain);
+
+        assertTrue(
+            test.deliveryPrice  == setup.source.rewardAddress.balance - test.rewardAddressBalance + feeParams.wormholeFeeOnSource,
+            "The source to target relayer's reward address was paid appropriately"
+        );
+   
+        uint256 amountToGetInRefundTarget =
+            (setup.target.rewardAddress.balance - refundRewardAddressBalance);
+
+        vm.assume(amountToGetInRefundTarget > 0);
+
+        uint256 refundSource; 
+        (LocalNative baseFee,) = setup.target.coreRelayer.quoteEVMDeliveryPrice(setup.sourceChain, TargetNative.wrap(0), Gas.wrap(0));
+
+            TargetNative tmp = setup.target.coreRelayer.quoteNativeForChain(
+                setup.sourceChain,
+                LocalNative.wrap(amountToGetInRefundTarget + feeParams.wormholeFeeOnTarget - baseFee.unwrap()),
+                setup.target.coreRelayer.getDefaultDeliveryProvider()
+            );
+            refundSource = tmp.unwrap();
+
+
+        // Calculate amount that must have been spent on gas, by reverse engineering from the amount that was paid to the provider's reward address on the target chain
+        test.gasAmount = uint32(
+            gasParams.targetGasLimit
+                - (amountToGetInRefundTarget + feeParams.wormholeFeeOnTarget)
+                    / test.targetChainRefundPerGasUnused
+        );
+        test.relayerPayment = test.relayerBalance - setup.target.relayer.balance;
+        test.destinationAmount = address(setup.target.integration).balance - test.destinationBalance;
+
+        assertTrue(
+            test.destinationAmount == feeParams.receiverValueTarget,
+            "Receiver value was sent to the contract"
+        );
+        assertTrue(
+            test.relayerPayment
+                == amountToGetInRefundTarget + feeParams.wormholeFeeOnTarget
+                    + feeParams.receiverValueTarget,
+            "Relayer paid the correct amount"
+        );
+        assertTrue(
+            refundSource == setup.source.refundAddress.balance - refundAddressBalance,
+            "Refund wasn't the correct amount"
+        );
+        console.log(test.gasAmount);
+        assertTrue(
+            test.gasAmount >= 140000,
+            "Gas amount (calculated from refund address payment) lower than expected. NOTE: This assert is purely to ensure the gas usage is consistent, and thus (since this was computed using the refund amount) the refund amount is correct."
+        );
+        assertTrue(
+            test.gasAmount <= 160000,
+            "Gas amount (calculated from refund address payment) higher than expected. NOTE: This assert is purely to ensure the gas usage is consistent, and thus (since this was computed using the refund amount) the refund amount is correct."
+        );
+    }
+
+    function testFundsCorrectForASendCrossChainRefundFailProviderNotSupported(
+        GasParameters memory gasParams,
+        FeeParameters memory feeParams
+    ) public {
+         vm.recordLogs();
+        (StandardSetupTwoChains memory setup, FundsCorrectTest memory test) =
+            setupFundsCorrectTest(gasParams, feeParams, uint32(170000 + REASONABLE_GAS_LIMIT.unwrap()));
+
+        setup.target.deliveryProvider.updateSupportedChain(setup.sourceChain, false);
+        vm.assume(test.targetChainRefundPerGasUnused * REASONABLE_GAS_LIMIT.unwrap() >= feeParams.wormholeFeeOnTarget + uint256(1) * gasParams.evmGasOverhead * gasParams.sourceGasPrice * (uint256(feeParams.sourceNativePrice) / feeParams.targetNativePrice + 1));
+
+        setup.source.integration.sendMessageWithRefund{value: test.deliveryPrice}(
+            bytes("Hello!"),
+            setup.targetChain,
+            gasParams.targetGasLimit,
+            test.receiverValue,
+            setup.sourceChain,
+            setup.source.refundAddress
+        );
+
+        genericRelayer.relay(setup.sourceChain);
+
+        assertTrue(keccak256(setup.target.integration.getMessage()) == keccak256(bytes("Hello!")));
+
+        assertTrue(
+            test.deliveryPrice
+                == setup.source.rewardAddress.balance - test.rewardAddressBalance + feeParams.wormholeFeeOnSource, 
+            "The source to target relayer's reward address was paid appropriately"
+        );
+       
+        test.relayerPayment = test.relayerBalance - setup.target.relayer.balance;
+        test.destinationAmount = address(setup.target.integration).balance - test.destinationBalance;
+
+        assertTrue(
+            test.destinationAmount == feeParams.receiverValueTarget,
+            "Receiver value was sent to the contract"
+        );
+        assertTrue(
+            test.relayerPayment == feeParams.receiverValueTarget,
+            "Relayer only paid the receiver value, and received the full transaction fee refund"
+        );
+        uint8 refundStatus = uint8(getRefundStatus());
+        assertTrue(refundStatus == uint8(IWormholeRelayerDelivery.RefundStatus.CROSS_CHAIN_REFUND_FAIL_PROVIDER_NOT_SUPPORTED));
+    }
+
+    function testFundsCorrectForASendCrossChainRefundNotEnough(
+        GasParameters memory gasParams,
+        FeeParameters memory feeParams
+    ) public {
+         vm.recordLogs();
+        (StandardSetupTwoChains memory setup, FundsCorrectTest memory test) =
+            setupFundsCorrectTest(gasParams, feeParams, 170000);
+        vm.assume(uint256(1) * gasParams.evmGasOverhead * gasParams.sourceGasPrice * feeParams.sourceNativePrice > uint256(1) * feeParams.targetNativePrice * test.targetChainRefundPerGasUnused * gasParams.targetGasLimit);
+
+        setup.source.integration.sendMessageWithRefund{value: test.deliveryPrice}(
+            bytes("Hello!"),
+            setup.targetChain,
+            gasParams.targetGasLimit,
+            test.receiverValue,
+            setup.sourceChain,
+            setup.source.refundAddress
+        );
+
+        genericRelayer.relay(setup.sourceChain);
+
+        assertTrue(keccak256(setup.target.integration.getMessage()) == keccak256(bytes("Hello!")));
+
+        assertTrue(
+            test.deliveryPrice
+                == setup.source.rewardAddress.balance - test.rewardAddressBalance + feeParams.wormholeFeeOnSource,
+            "The source to target relayer's reward address was paid appropriately"
+        );  
+
+        test.relayerPayment = test.relayerBalance - setup.target.relayer.balance;
+        test.destinationAmount = address(setup.target.integration).balance - test.destinationBalance;
+
+        assertTrue(
+            test.destinationAmount == feeParams.receiverValueTarget,
+            "Receiver value was sent to the contract"
+        );
+        assertTrue(
+            test.relayerPayment == feeParams.receiverValueTarget,
+            "Relayer only paid the receiver value, and received the full transaction fee refund"
+        );
+
+
+        assertTrue(uint8(getRefundStatus()) == uint8(IWormholeRelayerDelivery.RefundStatus.CROSS_CHAIN_REFUND_FAIL_NOT_ENOUGH));
+
+    }
+
+    /**
+     * Unit tests for Send and Resend: Ensuring the correct struct is logged
+     */
+
+    struct UnitTestParams {
+        address targetAddress;
+        bytes payload;
+        uint128 receiverValue;
+        uint128 paymentForExtraReceiverValue;
+        uint32 gasLimit;
+        uint16 refundChain;
+        address refundAddress;
+        VaaKey[3] vaaKeysFixed;
+    }
+
+    function testUnitTestSend(
+        GasParameters memory gasParams,
+        FeeParameters memory feeParams,
+        UnitTestParams memory params
+    ) public {
+        gasParams.targetGasLimit = params.gasLimit;
+        StandardSetupTwoChains memory setup =
+            standardAssumeAndSetupTwoChains(gasParams, feeParams, params.gasLimit);
+        VaaKey[] memory vaaKeys = new VaaKey[](3);
+        for (uint256 j = 0; j < 3; j++) {
+            vaaKeys[j] = params.vaaKeysFixed[j];
+        }
+        vm.recordLogs();
+
+        (LocalNative deliveryCost, GasPrice targetChainRefundPerGasUnused) = setup
+            .source
+            .coreRelayer
+            .quoteEVMDeliveryPrice(setup.targetChain, TargetNative.wrap(params.receiverValue), Gas.wrap(params.gasLimit));
+        uint256 value =
+            deliveryCost.unwrap() + params.paymentForExtraReceiverValue;
+        setup.source.integration.sendToEvm{value: value}(
+            setup.targetChain,
+            params.targetAddress,
+            params.gasLimit,
+            params.refundChain,
+            params.refundAddress,
+            params.receiverValue,
+            params.paymentForExtraReceiverValue,
+            params.payload,
+            vaaKeys
+        );
+
+        bytes memory encodedExecutionInfo = abi.encode(
+            uint8(ExecutionInfoVersion.EVM_V1), params.gasLimit, targetChainRefundPerGasUnused
+        );
+        TargetNative extraReceiverValue = setup.source.coreRelayer.quoteNativeForChain(
+            setup.targetChain,
+            LocalNative.wrap(params.paymentForExtraReceiverValue),
+            address(setup.source.deliveryProvider)
+        );
+
+        DeliveryInstruction memory expectedInstruction = DeliveryInstruction({
+            targetChain: setup.targetChain,
+            targetAddress: toWormholeFormat(params.targetAddress),
+            payload: params.payload,
+            requestedReceiverValue: TargetNative.wrap(params.receiverValue),
+            extraReceiverValue: extraReceiverValue,
+            encodedExecutionInfo: encodedExecutionInfo,
+            refundChain: params.refundChain,
+            refundAddress: toWormholeFormat(params.refundAddress),
+            refundDeliveryProvider: setup.source.deliveryProvider.getTargetChainAddress(
+                setup.targetChain
+                ),
+            sourceDeliveryProvider: toWormholeFormat(address(setup.source.deliveryProvider)),
+            senderAddress: toWormholeFormat(address(setup.source.integration)),
+            vaaKeys: vaaKeys
+        });
+
+        checkInstructionEquality(
+            relayerWormholeSimulator.parseVMFromLogs(vm.getRecordedLogs()[0]).payload,
+            expectedInstruction
+        );
+    }
+
+    struct UnitTestResendParams {
+        VaaKey deliveryVaaKey;
+        uint128 newReceiverValue;
+        uint32 newGasLimit;
+        address senderAddress;
+    }
+
+    function testUnitTestResend(
+        GasParameters memory gasParams,
+        FeeParameters memory feeParams,
+        UnitTestResendParams memory params
+    ) public {
+        gasParams.targetGasLimit = params.newGasLimit;
+        StandardSetupTwoChains memory setup =
+            standardAssumeAndSetupTwoChains(gasParams, feeParams, params.newGasLimit);
+
+        vm.recordLogs();
+
+        (LocalNative deliveryCost, GasPrice targetChainRefundPerGasUnused) = setup
+            .source
+            .coreRelayer
+            .quoteEVMDeliveryPrice(setup.targetChain, TargetNative.wrap(params.newReceiverValue), Gas.wrap(params.newGasLimit));
+        uint256 value = deliveryCost.unwrap();
+        vm.deal(params.senderAddress, value);
+        vm.prank(params.senderAddress);
+        setup.source.coreRelayer.resendToEvm{value: value}(
+            params.deliveryVaaKey,
+            setup.targetChain,
+            TargetNative.wrap(params.newReceiverValue),
+            Gas.wrap(params.newGasLimit),
+            address(setup.source.deliveryProvider)
+        );
+
+        bytes memory encodedExecutionInfo = abi.encode(
+            uint8(ExecutionInfoVersion.EVM_V1), params.newGasLimit, targetChainRefundPerGasUnused
+        );
+
+        RedeliveryInstruction memory expectedInstruction = RedeliveryInstruction({
+            deliveryVaaKey: params.deliveryVaaKey,
+            targetChain: setup.targetChain,
+            newRequestedReceiverValue: TargetNative.wrap(params.newReceiverValue),
+            newEncodedExecutionInfo: encodedExecutionInfo,
+            newSourceDeliveryProvider: toWormholeFormat(address(setup.source.deliveryProvider)),
+            newSenderAddress: toWormholeFormat(params.senderAddress)
+        });
+
+        checkRedeliveryInstructionEquality(
+            relayerWormholeSimulator.parseVMFromLogs(vm.getRecordedLogs()[0]).payload,
+            expectedInstruction
+        );
+    }
+
+    function checkVaaKey(
+        bytes memory data,
+        uint256 _index,
+        VaaKey memory vaaKey
+    ) public returns (uint256 index) {
+        VaaKey memory decodedVaaKey;
+        uint8 payloadId;
+        index = _index;
+        (payloadId, index) = data.asUint8(index);
+        assertTrue(payloadId == 1, "Is a vaa key version 1");
+        (decodedVaaKey.chainId, index) = data.asUint16(index);
+        assertTrue(decodedVaaKey.chainId == vaaKey.chainId, "Wrong chain id");
+        (decodedVaaKey.emitterAddress, index) = data.asBytes32(index);
+        assertTrue(decodedVaaKey.emitterAddress == vaaKey.emitterAddress, "Wrong emitter address");
+        (decodedVaaKey.sequence, index) = data.asUint64(index);
+        assertTrue(decodedVaaKey.sequence == vaaKey.sequence, "Wrong sequence");
+    }
+
+    function checkInstructionEquality(
+        bytes memory data,
+        DeliveryInstruction memory expectedInstruction
+    ) public {
+        uint256 index = 0;
+        uint32 length = 0;
+        uint8 payloadId;
+        DeliveryInstruction memory decodedInstruction;
+        (payloadId, index) = data.asUint8(index);
+        assertTrue(payloadId == 1, "Is a delivery instruction");
+        (decodedInstruction.targetChain, index) = data.asUint16(index);
+        assertTrue(
+            decodedInstruction.targetChain == expectedInstruction.targetChain,
+            "Wrong target chain id"
+        );
+        (decodedInstruction.targetAddress, index) = data.asBytes32(index);
+        assertTrue(
+            decodedInstruction.targetAddress == expectedInstruction.targetAddress,
+            "Wrong target address"
+        );
+        (length, index) = data.asUint32(index);
+        (decodedInstruction.payload, index) = data.slice(index, length);
+        assertTrue(
+            keccak256(decodedInstruction.payload) == keccak256(expectedInstruction.payload),
+            "Wrong payload"
+        );
+        uint256 requestedReceiverValue;
+        uint256 extraReceiverValue;
+        (requestedReceiverValue, index) = data.asUint256(index);
+        assertTrue(
+            requestedReceiverValue == expectedInstruction.requestedReceiverValue.unwrap(),
+            "Wrong requested receiver value"
+        );
+        (extraReceiverValue, index) = data.asUint256(index);
+        assertTrue(
+            extraReceiverValue == expectedInstruction.extraReceiverValue.unwrap(),
+            "Wrong extra receiver value"
+        );
+        (length, index) = data.asUint32(index);
+        (decodedInstruction.encodedExecutionInfo, index) = data.slice(index, length);
+        assertTrue(
+            keccak256(decodedInstruction.encodedExecutionInfo)
+                == keccak256(expectedInstruction.encodedExecutionInfo),
+            "Wrong encoded execution info"
+        );
+        (decodedInstruction.refundChain, index) = data.asUint16(index);
+        assertTrue(
+            decodedInstruction.refundChain == expectedInstruction.refundChain,
+            "Wrong refund chain id"
+        );
+        (decodedInstruction.refundAddress, index) = data.asBytes32(index);
+        assertTrue(
+            decodedInstruction.refundAddress == expectedInstruction.refundAddress,
+            "Wrong refund address"
+        );
+        (decodedInstruction.refundDeliveryProvider, index) = data.asBytes32(index);
+        assertTrue(
+            decodedInstruction.refundDeliveryProvider == expectedInstruction.refundDeliveryProvider,
+            "Wrong refund relay provider"
+        );
+        (decodedInstruction.sourceDeliveryProvider, index) = data.asBytes32(index);
+        assertTrue(
+            decodedInstruction.sourceDeliveryProvider == expectedInstruction.sourceDeliveryProvider,
+            "Wrong source relay provider"
+        );
+        (decodedInstruction.senderAddress, index) = data.asBytes32(index);
+        assertTrue(
+            decodedInstruction.senderAddress == expectedInstruction.senderAddress,
+            "Wrong sender address"
+        );
+        uint8 vaaKeysLength;
+        (vaaKeysLength, index) = data.asUint8(index);
+        decodedInstruction.vaaKeys = new VaaKey[](vaaKeysLength);
+        for (uint256 i = 0; i < vaaKeysLength; i++) {
+            index = checkVaaKey(data, index, expectedInstruction.vaaKeys[i]);
+        }
+        assertTrue(index == data.length, "Wrong length of data");
+    }
+
+    function checkRedeliveryInstructionEquality(
+        bytes memory data,
+        RedeliveryInstruction memory expectedInstruction
+    ) public {
+        uint256 index = 0;
+        uint32 length = 0;
+        uint8 payloadId;
+        RedeliveryInstruction memory decodedInstruction;
+        (payloadId, index) = data.asUint8(index);
+        assertTrue(payloadId == 2, "Is a redelivery instruction");
+        index = checkVaaKey(data, index, expectedInstruction.deliveryVaaKey);
+        (decodedInstruction.targetChain, index) = data.asUint16(index);
+        assertTrue(
+            decodedInstruction.targetChain == expectedInstruction.targetChain,
+            "Wrong target chain id"
+        );
+        uint256 requestedReceiverValue;
+        (requestedReceiverValue, index) = data.asUint256(index);
+        assertTrue(
+            requestedReceiverValue == expectedInstruction.newRequestedReceiverValue.unwrap(),
+            "Wrong requested receiver value"
+        );
+        (length, index) = data.asUint32(index);
+        (decodedInstruction.newEncodedExecutionInfo, index) = data.slice(index, length);
+        assertTrue(
+            keccak256(decodedInstruction.newEncodedExecutionInfo)
+                == keccak256(expectedInstruction.newEncodedExecutionInfo),
+            "Wrong encoded execution info"
+        );
+        (decodedInstruction.newSourceDeliveryProvider, index) = data.asBytes32(index);
+        assertTrue(
+            decodedInstruction.newSourceDeliveryProvider
+                == expectedInstruction.newSourceDeliveryProvider,
+            "Wrong source relay provider"
+        );
+        (decodedInstruction.newSenderAddress, index) = data.asBytes32(index);
+        assertTrue(
+            decodedInstruction.newSenderAddress == expectedInstruction.newSenderAddress,
+            "Wrong sender address"
+        );
+        assertTrue(index == data.length, "Wrong length of data");
+    }
+
+    /**
+     * Tests related to reverts in deliver()
+     *
+     */
+
+    function invalidateVM(bytes memory message, WormholeSimulator simulator) internal {
+        change(message, message.length - 1);
+        simulator.invalidateVM(message);
+    }
+
+    function change(bytes memory message, uint256 index) internal pure {
+        if (message[index] == 0x02) {
+            message[index] = 0x04;
+        } else {
+            message[index] = 0x02;
+        }
+    }
+
+    struct DeliveryStack {
+        bytes32 deliveryVaaHash;
+        uint256 payment;
+        Vm.Log[] entries;
+        bytes encodedDeliveryVAA;
+        bytes[] encodedVMs;
+        IWormhole.VM parsed;
+        uint256 budget;
+        address payable relayerRefundAddress;
+        DeliveryInstruction instruction;
+    }
+
+    function prepareDeliveryStack(
+        DeliveryStack memory stack,
+        StandardSetupTwoChains memory setup,
+        uint256 numVaas
+    ) internal {
+        stack.entries = vm.getRecordedLogs();
+        stack.encodedVMs = new bytes[](0);
+
+        stack.encodedDeliveryVAA = relayerWormholeSimulator.fetchSignedMessageFromLogs(
+            stack.entries[numVaas], setup.sourceChain, address(setup.source.coreRelayer)
+        );
+
+        stack.relayerRefundAddress = payable(setup.target.relayer);
+        stack.parsed = relayerWormhole.parseVM(stack.encodedDeliveryVAA);
+        stack.instruction = WormholeRelayerSerde.decodeDeliveryInstruction(stack.parsed.payload);
+        EvmExecutionInfoV1 memory executionInfo =
+            decodeEvmExecutionInfoV1(stack.instruction.encodedExecutionInfo);
+        stack.budget = Wei.unwrap(
+            executionInfo.gasLimit.toWei(executionInfo.targetChainRefundPerGasUnused)
+                + stack.instruction.extraReceiverValue.asNative()
+        );
+    }
+
+    function testRevertDeliveryInvalidDeliveryVAA(
+        GasParameters memory gasParams,
+        FeeParameters memory feeParams,
+        bytes memory message
+    ) public {
+        StandardSetupTwoChains memory setup =
+            standardAssumeAndSetupTwoChains(gasParams, feeParams, 1000000);
+
+        vm.recordLogs();
+
+        DeliveryStack memory stack;
+
+        sendMessageToTargetChain(setup, gasParams.targetGasLimit, 0, message);
+
+        prepareDeliveryStack(stack, setup, 0);
+
+        bytes memory fakeVM = abi.encodePacked(stack.encodedDeliveryVAA);
+
+        invalidateVM(fakeVM, setup.target.wormholeSimulator);
+
+        stack.encodedDeliveryVAA = fakeVM;
+
+        vm.prank(setup.target.relayer);
+        vm.expectRevert(abi.encodeWithSignature("InvalidDeliveryVaa(string)", ""));
+        setup.target.coreRelayerFull.deliver{value: stack.budget}(
+            stack.encodedVMs, stack.encodedDeliveryVAA, stack.relayerRefundAddress, bytes("")
+        );
+    }
+
+    function testRevertDeliveryInvalidEmitter(
+        GasParameters memory gasParams,
+        FeeParameters memory feeParams,
+        bytes memory message
+    ) public {
+        StandardSetupTwoChains memory setup =
+            standardAssumeAndSetupTwoChains(gasParams, feeParams, 1000000);
+
+        vm.recordLogs();
+
+        DeliveryStack memory stack;
+
+        sendMessageToTargetChain(setup, gasParams.targetGasLimit, 0, message);
+
+        prepareDeliveryStack(stack, setup, 0);
+
+        // Create valid VAA with wrong emitter address
+        IWormhole.VM memory vm_ = relayerWormholeSimulator.parseVMFromLogs(stack.entries[0]);
+        vm_.version = uint8(1);
+        vm_.timestamp = uint32(block.timestamp);
+        vm_.emitterChainId = setup.sourceChain;
+        vm_.emitterAddress = toWormholeFormat(address(setup.source.integration));
+        bytes memory deliveryVaaWithWrongEmitter =
+            relayerWormholeSimulator.encodeAndSignMessage(vm_);
+
+        vm.prank(setup.target.relayer);
+        vm.expectRevert(
+            abi.encodeWithSelector(
+                InvalidEmitter.selector,
+                setup.source.integration,
+                setup.source.coreRelayer,
+                setup.source.chainId
+            )
+        );
+        setup.target.coreRelayerFull.deliver{value: stack.budget}(
+            stack.encodedVMs, deliveryVaaWithWrongEmitter, stack.relayerRefundAddress, bytes("")
+        );
+    }
+
+    function testRevertDeliveryInsufficientRelayerFunds(
+        GasParameters memory gasParams,
+        FeeParameters memory feeParams,
+        bytes memory message
+    ) public {
+        StandardSetupTwoChains memory setup =
+            standardAssumeAndSetupTwoChains(gasParams, feeParams, 1000000);
+
+        vm.recordLogs();
+
+        vm.assume(gasParams.targetGasPrice > 1);
+
+        DeliveryStack memory stack;
+
+        sendMessageToTargetChain(setup, gasParams.targetGasLimit, 0, message);
+
+        prepareDeliveryStack(stack, setup, 0);
+
+        vm.prank(setup.target.relayer);
+        vm.expectRevert(
+            abi.encodeWithSelector(
+                InsufficientRelayerFunds.selector, stack.budget - 1, stack.budget
+            )
+        );
+        setup.target.coreRelayerFull.deliver{value: stack.budget - 1}(
+            stack.encodedVMs, stack.encodedDeliveryVAA, stack.relayerRefundAddress, bytes("")
+        );
+    }
+
+    function testRevertDeliveryTargetChainIsNotThisChain(
+        GasParameters memory gasParams,
+        FeeParameters memory feeParams,
+        bytes memory message
+    ) public {
+        StandardSetupTwoChains memory setup =
+            standardAssumeAndSetupTwoChains(gasParams, feeParams, 1000000);
+
+        vm.recordLogs();
+
+        DeliveryStack memory stack;
+
+        sendMessageToTargetChain(setup, gasParams.targetGasLimit, 0, message);
+
+        prepareDeliveryStack(stack, setup, 0);
+
+        vm.prank(setup.target.relayer);
+        vm.expectRevert(abi.encodeWithSignature("TargetChainIsNotThisChain(uint16)", 2));
+        map[setup.differentChainId].coreRelayerFull.deliver{value: stack.budget}(
+            stack.encodedVMs, stack.encodedDeliveryVAA, stack.relayerRefundAddress, bytes("")
+        );
+    }
+
+    function testRevertDeliveryVaaKeysLengthDoesNotMatchVaasLength(
+        GasParameters memory gasParams,
+        FeeParameters memory feeParams,
+        bytes memory message
+    ) public {
+        StandardSetupTwoChains memory setup =
+            standardAssumeAndSetupTwoChains(gasParams, feeParams, 1000000);
+
+        vm.recordLogs();
+
+        DeliveryStack memory stack;
+
+        sendMessageToTargetChain(setup, gasParams.targetGasLimit, 0, message);
+
+        prepareDeliveryStack(stack, setup, 0);
+
+        stack.encodedVMs = new bytes[](1);
+        stack.encodedVMs[0] = stack.encodedDeliveryVAA;
+
+        vm.prank(setup.target.relayer);
+        vm.expectRevert(
+            abi.encodeWithSignature("VaaKeysLengthDoesNotMatchVaasLength(uint256,uint256)", 0, 1)
+        );
+        setup.target.coreRelayerFull.deliver{value: stack.budget}(
+            stack.encodedVMs, stack.encodedDeliveryVAA, stack.relayerRefundAddress, bytes("")
+        );
+    }
+
+    function testRevertDeliveryVaaKeysDoNotMatchVaas(
+        GasParameters memory gasParams,
+        FeeParameters memory feeParams,
+        bytes memory message
+    ) public {
+        StandardSetupTwoChains memory setup =
+            standardAssumeAndSetupTwoChains(gasParams, feeParams, 1000000);
+
+        vm.recordLogs();
+
+        DeliveryStack memory stack;
+
+        (LocalNative payment_,) = setup.source.coreRelayer.quoteEVMDeliveryPrice(
+            setup.targetChain, TargetNative.wrap(0), Gas.wrap(gasParams.targetGasLimit)
+        );
+        stack.payment = payment_.unwrap();
+
+        uint64 sequence = setup.source.wormhole.publishMessage{value: feeParams.wormholeFeeOnSource}(
+            1, bytes(""), 200
+        );
+        setup.source.integration.sendToEvm{value: stack.payment}(
+            setup.targetChain,
+            address(setup.target.integration),
+            gasParams.targetGasLimit,
+            setup.sourceChain,
+            address(this),
+            0,
+            0,
+            message,
+            vaaKeyArray(setup.sourceChain, sequence, address(this))
+        );
+
+        prepareDeliveryStack(stack, setup, 1);
+
+        stack.encodedVMs = new bytes[](1);
+        stack.encodedVMs[0] = stack.encodedDeliveryVAA;
+
+        vm.prank(setup.target.relayer);
+        vm.expectRevert(abi.encodeWithSignature("VaaKeysDoNotMatchVaas(uint8)", 0));
+        setup.target.coreRelayerFull.deliver{value: stack.budget}(
+            stack.encodedVMs, stack.encodedDeliveryVAA, stack.relayerRefundAddress, bytes("")
+        );
+    }
+
+    /**
+     * Tests related to reverts due to delivering with deliveryOverrides
+     */
+
+    function testDeliveryWithOverrides(
+        GasParameters memory gasParams,
+        FeeParameters memory feeParams,
+        bytes memory message
+    ) public {
+        StandardSetupTwoChains memory setup =
+            standardAssumeAndSetupTwoChains(gasParams, feeParams, 1000000);
+
+        vm.recordLogs();
+
+        DeliveryStack memory stack;
+
+        sendMessageToTargetChain(setup, gasParams.targetGasLimit, 0, message);
+
+        prepareDeliveryStack(stack, setup, 0);
+
+        DeliveryOverride memory deliveryOverride = DeliveryOverride(
+            stack.instruction.requestedReceiverValue,
+            stack.instruction.encodedExecutionInfo,
+            stack.deliveryVaaHash //really redeliveryHash
+        );
+
+        setup.target.coreRelayerFull.deliver{value: stack.budget}(
+            stack.encodedVMs,
+            stack.encodedDeliveryVAA,
+            stack.relayerRefundAddress,
+            WormholeRelayerSerde.encode(deliveryOverride)
+        );
+        assertTrue(keccak256(setup.target.integration.getMessage()) == keccak256(message));
+    }
+
+    function testRevertDeliveryWithOverrideGasLimit(
+        GasParameters memory gasParams,
+        FeeParameters memory feeParams,
+        bytes memory message
+    ) public {
+        StandardSetupTwoChains memory setup =
+            standardAssumeAndSetupTwoChains(gasParams, feeParams, 1000000);
+
+        vm.recordLogs();
+
+        DeliveryStack memory stack;
+
+        sendMessageToTargetChain(setup, gasParams.targetGasLimit, 0, message);
+
+        prepareDeliveryStack(stack, setup, 0);
+
+        EvmExecutionInfoV1 memory executionInfo =
+            decodeEvmExecutionInfoV1(stack.instruction.encodedExecutionInfo);
+
+        DeliveryOverride memory deliveryOverride = DeliveryOverride(
+            stack.instruction.requestedReceiverValue,
+            encodeEvmExecutionInfoV1(
+                EvmExecutionInfoV1({
+                    gasLimit: executionInfo.gasLimit - Gas.wrap(1),
+                    targetChainRefundPerGasUnused: executionInfo.targetChainRefundPerGasUnused
+                })
+            ),
+            stack.deliveryVaaHash //really redeliveryHash
+        );
+
+        vm.expectRevert(abi.encodeWithSignature("InvalidOverrideGasLimit()"));
+        setup.target.coreRelayerFull.deliver{value: stack.budget}(
+            stack.encodedVMs,
+            stack.encodedDeliveryVAA,
+            stack.relayerRefundAddress,
+            WormholeRelayerSerde.encode(deliveryOverride)
+        );
+    }
+
+    function testRevertDeliveryWithOverrideReceiverValue(
+        GasParameters memory gasParams,
+        FeeParameters memory feeParams,
+        bytes memory message
+    ) public {
+        StandardSetupTwoChains memory setup =
+            standardAssumeAndSetupTwoChains(gasParams, feeParams, 1000000);
+
+        vm.recordLogs();
+
+        vm.assume(feeParams.receiverValueTarget > 0);
+
+        DeliveryStack memory stack;
+
+        sendMessageToTargetChain(
+            setup, gasParams.targetGasLimit, feeParams.receiverValueTarget, message
+        );
+
+        prepareDeliveryStack(stack, setup, 0);
+
+        DeliveryOverride memory deliveryOverride = DeliveryOverride(
+            stack.instruction.requestedReceiverValue - TargetNative.wrap(1),
+            stack.instruction.encodedExecutionInfo,
+            stack.deliveryVaaHash //really redeliveryHash
+        );
+
+        vm.expectRevert(abi.encodeWithSignature("InvalidOverrideReceiverValue()"));
+        setup.target.coreRelayerFull.deliver{value: stack.budget}(
+            stack.encodedVMs,
+            stack.encodedDeliveryVAA,
+            stack.relayerRefundAddress,
+            WormholeRelayerSerde.encode(deliveryOverride)
+        );
+    }
+
+    function testRevertDeliveryWithOverrideMaximumRefund(
+        GasParameters memory gasParams,
+        FeeParameters memory feeParams,
+        bytes memory message
+    ) public {
+        StandardSetupTwoChains memory setup =
+            standardAssumeAndSetupTwoChains(gasParams, feeParams, 1000000);
+
+        vm.assume(gasParams.targetGasPrice > 1);
+
+        vm.recordLogs();
+
+        DeliveryStack memory stack;
+
+        sendMessageToTargetChain(setup, gasParams.targetGasLimit, 0, message);
+
+        prepareDeliveryStack(stack, setup, 0);
+
+        EvmExecutionInfoV1 memory executionInfo =
+            decodeEvmExecutionInfoV1(stack.instruction.encodedExecutionInfo);
+
+        DeliveryOverride memory deliveryOverride = DeliveryOverride(
+            stack.instruction.requestedReceiverValue,
+            encodeEvmExecutionInfoV1(
+                EvmExecutionInfoV1({
+                    gasLimit: executionInfo.gasLimit,
+                    targetChainRefundPerGasUnused: GasPrice.wrap(
+                        executionInfo.targetChainRefundPerGasUnused.unwrap() - 1
+                        )
+                })
+            ),
+            stack.deliveryVaaHash //really redeliveryHash
+        );
+
+        vm.expectRevert(abi.encodeWithSignature("InvalidOverrideRefundPerGasUnused()"));
+        setup.target.coreRelayerFull.deliver{value: stack.budget}(
+            stack.encodedVMs,
+            stack.encodedDeliveryVAA,
+            stack.relayerRefundAddress,
+            WormholeRelayerSerde.encode(deliveryOverride)
+        );
+    }
+
+    function testRevertDeliveryWithOverrideUnexpectedExecutionInfoVersion(
+        GasParameters memory gasParams,
+        FeeParameters memory feeParams,
+        bytes memory message
+    ) public {
+        StandardSetupTwoChains memory setup =
+            standardAssumeAndSetupTwoChains(gasParams, feeParams, 1000000);
+
+        vm.recordLogs();
+
+        vm.assume(feeParams.receiverValueTarget > 0);
+
+        DeliveryStack memory stack;
+
+        sendMessageToTargetChain(
+            setup, gasParams.targetGasLimit, feeParams.receiverValueTarget, message
+        );
+
+        prepareDeliveryStack(stack, setup, 0);
+
+        DeliveryOverride memory deliveryOverride = DeliveryOverride(
+            stack.instruction.requestedReceiverValue,
+            abi.encodePacked(uint8(4), stack.instruction.encodedExecutionInfo),
+            stack.deliveryVaaHash //really redeliveryHash
+        );
+
+        // Note: Reverts when trying to abi.decode the ExecutionInfoVersion. No revert message
+        vm.expectRevert();
+        setup.target.coreRelayerFull.deliver{value: stack.budget}(
+            stack.encodedVMs,
+            stack.encodedDeliveryVAA,
+            stack.relayerRefundAddress,
+            WormholeRelayerSerde.encode(deliveryOverride)
+        );
+    }
+
+    function testRevertSendMsgValueTooLow(
+        GasParameters memory gasParams,
+        FeeParameters memory feeParams,
+        bytes memory message
+    ) public {
+        StandardSetupTwoChains memory setup =
+            standardAssumeAndSetupTwoChains(gasParams, feeParams, REASONABLE_GAS_LIMIT);
+
+        vm.recordLogs();
+
+        (LocalNative deliveryCost,) = setup.source.coreRelayer.quoteEVMDeliveryPrice(
+            setup.targetChain, TargetNative.wrap(0), Gas.wrap(gasParams.targetGasLimit)
+        );
+
+        vm.expectRevert(
+            abi.encodeWithSelector(
+                InvalidMsgValue.selector,
+                deliveryCost.unwrap() - 1,
+                deliveryCost.unwrap()
+            )
+        );
+        setup.source.integration.sendMessage{
+            value: deliveryCost.unwrap() - 1
+        }(message, setup.targetChain, gasParams.targetGasLimit, 0);
+    }
+
+    function testRevertSendMsgValueTooHigh(
+        GasParameters memory gasParams,
+        FeeParameters memory feeParams,
+        bytes memory message
+    ) public {
+        StandardSetupTwoChains memory setup =
+            standardAssumeAndSetupTwoChains(gasParams, feeParams, REASONABLE_GAS_LIMIT);
+
+        vm.recordLogs();
+
+        (LocalNative deliveryCost,) = setup.source.coreRelayer.quoteEVMDeliveryPrice(
+            setup.targetChain, TargetNative.wrap(0), Gas.wrap(gasParams.targetGasLimit)
+        );
+
+        vm.expectRevert(
+            abi.encodeWithSelector(
+                InvalidMsgValue.selector,
+                deliveryCost.unwrap() + 1,
+                deliveryCost.unwrap() 
+            )
+        );
+        setup.source.integration.sendMessage{
+            value: deliveryCost.unwrap() + 1
+        }(message, setup.targetChain, gasParams.targetGasLimit, 0);
+    }
+
+    function testRevertSendProviderNotSupported(
+        GasParameters memory gasParams,
+        FeeParameters memory feeParams,
+        bytes memory message
+    ) public {
+        StandardSetupTwoChains memory setup =
+            standardAssumeAndSetupTwoChains(gasParams, feeParams, REASONABLE_GAS_LIMIT);
+
+        vm.recordLogs();
+
+        (LocalNative deliveryCost,) = setup.source.coreRelayer.quoteEVMDeliveryPrice(
+            setup.targetChain, TargetNative.wrap(0), Gas.wrap(gasParams.targetGasLimit)
+        );
+
+        vm.expectRevert(
+            abi.encodeWithSelector(
+                DeliveryProviderDoesNotSupportTargetChain.selector,
+                address(setup.source.deliveryProvider),
+                uint16(32)
+            )
+        );
+        setup.source.integration.sendMessage{
+            value: deliveryCost.unwrap() - 1
+        }(message, 32, gasParams.targetGasLimit, 0);
+    }
+
+    function testRevertResendProviderNotSupported(
+        GasParameters memory gasParams,
+        FeeParameters memory feeParams
+    ) public {
+        StandardSetupTwoChains memory setup =
+            standardAssumeAndSetupTwoChains(gasParams, feeParams, REASONABLE_GAS_LIMIT);
+
+        vm.recordLogs();
+
+        vm.expectRevert(
+            abi.encodeWithSelector(
+                DeliveryProviderDoesNotSupportTargetChain.selector,
+                address(setup.source.deliveryProvider),
+                uint16(32)
+            )
+        );
+        setup.source.integration.resend{value: 0}(setup.sourceChain, 1, 32, uint32(REASONABLE_GAS_LIMIT.unwrap()), 0);
+    }
+
+    function testSendCheckConsistencyLevel(
+        GasParameters memory gasParams,
+        FeeParameters memory feeParams,
+        uint8 consistencyLevel
+    ) public {
+        StandardSetupTwoChains memory setup =
+            standardAssumeAndSetupTwoChains(gasParams, feeParams, REASONABLE_GAS_LIMIT);
+
+        vm.recordLogs();
+
+        (LocalNative deliveryCost,) =
+            setup.source.coreRelayer.quoteEVMDeliveryPrice(setup.targetChain, TargetNative.wrap(0), Gas.wrap(0));
+
+        setup.source.coreRelayer.sendToEvm{value: deliveryCost.unwrap()}(
+            setup.targetChain,
+            address(0x0),
+            bytes(""),
+            TargetNative.wrap(0),
+            LocalNative.wrap(0),
+            Gas.wrap(0),
+            setup.sourceChain,
+            address(0x0),
+            address(setup.source.deliveryProvider),
+            vaaKeyArray(setup.sourceChain, 22345, address(this)),
+            consistencyLevel
+        );
+
+        Vm.Log memory log = vm.getRecordedLogs()[0];
+
+        // Parse the consistency level from the published VAA
+        (uint8 actualConsistencyLevel,) = log.data.asUint8(32 + 32 + 32 + 32 - 1);
+
+        assertTrue(consistencyLevel == actualConsistencyLevel);
+    }
+
+    function testToAndFromWormholeFormat(address msg1) public {
+        assertTrue(toWormholeFormat(msg1) == bytes32(uint256(uint160(msg1))));
+        assertTrue(fromWormholeFormat(toWormholeFormat(msg1)) == msg1);
+    }
+
+    /**
+     * Forward Revert Tests using Forward Tester
+     */
+
+    ForwardTester forwardTester;
+
+    function executeForwardTest(
+        ForwardTester.Action test,
+        IWormholeRelayerDelivery.DeliveryStatus desiredOutcome,
+        StandardSetupTwoChains memory setup
+    ) internal {
+        vm.recordLogs();
+        forwardTester =
+        new ForwardTester(address(setup.target.wormhole), address(setup.target.coreRelayer), address(setup.target.wormholeSimulator));
+        vm.deal(address(forwardTester), type(uint256).max / 2);
+
+        (LocalNative forwardDeliveryCost,) = setup.target.coreRelayer.quoteEVMDeliveryPrice(
+            setup.sourceChain, TargetNative.wrap(0), REASONABLE_GAS_LIMIT
+        );
+        uint256 receiverValue = forwardDeliveryCost.unwrap();
+        vm.assume(receiverValue <= type(uint128).max);
+
+        (LocalNative deliveryCost,) = setup.source.coreRelayer.quoteEVMDeliveryPrice(
+            setup.targetChain, TargetNative.wrap(receiverValue), REASONABLE_GAS_LIMIT_FORWARDS
+        );
+
+        setup.source.coreRelayer.sendPayloadToEvm{
+            value: deliveryCost.unwrap()
+        }(
+            setup.targetChain,
+            address(forwardTester),
+            abi.encodePacked(uint8(test)),
+            TargetNative.wrap(receiverValue),
+            REASONABLE_GAS_LIMIT_FORWARDS
+        );
+        genericRelayer.relay(setup.sourceChain);
+        IWormholeRelayerDelivery.DeliveryStatus status = getDeliveryStatus();
+        assertTrue(status == desiredOutcome);
+    }
+
+    function testForwardTester(
+        GasParameters memory gasParams,
+        FeeParameters memory feeParams
+    ) public {
+        StandardSetupTwoChains memory setup =
+            standardAssumeAndSetupTwoChains(gasParams, feeParams, 1000000);
+        executeForwardTest(
+            ForwardTester.Action.WorksCorrectly,
+            IWormholeRelayerDelivery.DeliveryStatus.FORWARD_REQUEST_SUCCESS,
+            setup
+        );
+    }
+
+    function testRevertForwardNoDeliveryInProgress(
+        GasParameters memory gasParams,
+        FeeParameters memory feeParams
+    ) public {
+        StandardSetupTwoChains memory setup =
+            standardAssumeAndSetupTwoChains(gasParams, feeParams, 1000000);
+
+        vm.expectRevert(abi.encodeWithSignature("NoDeliveryInProgress()"));
+        setup.source.coreRelayer.forwardPayloadToEvm(
+            setup.targetChain,
+            address(forwardTester),
+            bytes(""),
+            TargetNative.wrap(0),
+            TOO_LOW_GAS_LIMIT
+        );
+    }
+
+    function testRevertForwardForwardRequestFromWrongAddress(
+        GasParameters memory gasParams,
+        FeeParameters memory feeParams
+    ) public {
+        StandardSetupTwoChains memory setup =
+            standardAssumeAndSetupTwoChains(gasParams, feeParams, 1000000);
+
+        executeForwardTest(
+            ForwardTester.Action.ForwardRequestFromWrongAddress,
+            IWormholeRelayerDelivery.DeliveryStatus.RECEIVER_FAILURE,
+            setup
+        );
+    }
+
+    function testRevertDeliveryReentrantCall(
+        GasParameters memory gasParams,
+        FeeParameters memory feeParams
+    ) public {
+        StandardSetupTwoChains memory setup =
+            standardAssumeAndSetupTwoChains(gasParams, feeParams, 1000000);
+        executeForwardTest(
+            ForwardTester.Action.ReentrantCall,
+            IWormholeRelayerDelivery.DeliveryStatus.RECEIVER_FAILURE,
+            setup
+        );
+    }
+
+    function testRevertForwardProviderNotSupported(
+        GasParameters memory gasParams,
+        FeeParameters memory feeParams
+    ) public {
+        StandardSetupTwoChains memory setup =
+            standardAssumeAndSetupTwoChains(gasParams, feeParams, 1000000);
+
+        executeForwardTest(
+            ForwardTester.Action.ProviderNotSupported,
+            IWormholeRelayerDelivery.DeliveryStatus.RECEIVER_FAILURE,
+            setup
+        );
+    }
+
+     function testEncodeAndDecodeDeliveryInstruction(
+        bytes memory payload
+    ) public {
+        VaaKey[] memory vaaKeys = new VaaKey[](3);
+        vaaKeys[0] = VaaKey({
+            chainId: 1,
+            emitterAddress: bytes32(""),
+            sequence: 23
+        });
+        vaaKeys[1] = vaaKeys[0];
+        vaaKeys[2] = vaaKeys[0];
+
+        DeliveryInstruction memory instruction = DeliveryInstruction({
+            targetChain: 1,
+            targetAddress: bytes32(""),
+            payload: payload,
+            requestedReceiverValue: TargetNative.wrap(456),
+            extraReceiverValue: TargetNative.wrap(123),
+            encodedExecutionInfo: bytes("abcdefghijklmnopqrstuvwxyz"),
+            refundChain: 2,
+            refundAddress: keccak256(bytes("refundAddress")),
+            refundDeliveryProvider: keccak256(bytes("refundRelayProvider")),
+            sourceDeliveryProvider: keccak256(bytes("sourceRelayProvider")),
+            senderAddress: keccak256(bytes("senderAddress")),
+            vaaKeys: vaaKeys
+        });
+
+        DeliveryInstruction memory newInstruction =
+            WormholeRelayerSerde.decodeDeliveryInstruction(WormholeRelayerSerde.encode(instruction));
+
+        checkInstructionEquality(WormholeRelayerSerde.encode(instruction), newInstruction);
+        checkInstructionEquality(WormholeRelayerSerde.encode(newInstruction), instruction);
+    }
+
+    function testAttackForwardRequestCache(
+        GasParameters memory gasParams,
+        FeeParameters memory feeParams
+    ) public {
+        // General idea:
+        // 1. Attacker sets up a malicious integration contract in the target chain.
+        // 2. Attacker requests a message send to `target` chain.
+        //   The message destination and the refund address are both the malicious integration contract in the target chain.
+        // 3. The delivery of the message triggers a refund to the malicious integration contract.
+        // 4. During the refund, the integration contract activates the forwarding mechanism.
+        //   This is allowed due to the integration contract also being the target of the delivery.
+        // 5. The forward request is left as is in the `WormholeRelayer` state.
+        // 6. The next message (i.e. the victim's message) delivery on `target` chain, from any relayer, using any `DeliveryProvider` and any integration contract,
+        //   will see the forward request placed by the malicious integration contract and act on it.
+        // Caveat: the delivery of the victim's message must not invoke the forwarding mechanism for the attack test to be meaningful.
+        //
+        // In essence, this tries to attack the shared forwarding request cache present in the contract state.
+        // This attack doesn't work thanks to the check inside the `requestForward` function that only allows requesting a forward when there is a delivery being processed.
+
+        StandardSetupTwoChains memory setup =
+            standardAssumeAndSetupTwoChains(gasParams, feeParams, 1000000);
+
+        vm.assume(gasParams.targetGasPrice > 1);
+
+        // Collected funds from the attack are meant to be sent here.
+        address attackerSourceAddress = address(
+            uint160(
+                uint256(keccak256(abi.encodePacked(bytes("attackerAddress"), setup.sourceChain)))
+            )
+        );
+        assertTrue(attackerSourceAddress.balance == 0, "Attacker balance is not 0");
+
+        // Borrowed assumes from testForward. They should help since this test is similar.
+        vm.assume(
+            uint256(1) * gasParams.targetGasPrice * feeParams.targetNativePrice
+                > uint256(1) * gasParams.sourceGasPrice * feeParams.sourceNativePrice
+        );
+
+        // Estimate the cost based on the initialized values
+        (LocalNative deliveryPrice,) = setup.source.coreRelayer.quoteEVMDeliveryPrice(
+            setup.targetChain, TargetNative.wrap(0), Gas.wrap(gasParams.targetGasLimit)
+        );
+
+        {
+            AttackForwardIntegration attackerContract =
+            new AttackForwardIntegration(setup.target.wormhole, setup.target.coreRelayer, setup.targetChain, attackerSourceAddress);
+            bytes memory attackMsg = "attack";
+
+            vm.recordLogs();
+
+            // The attacker requests the message to be sent to the malicious contract.
+            // It is critical that the refund and destination (aka integrator) addresses are the same.
+            setup.source.coreRelayer.sendPayloadToEvm{
+                value: deliveryPrice.unwrap()
+            }(setup.targetChain, address(attackerContract), attackMsg, TargetNative.wrap(0), Gas.wrap(gasParams.targetGasLimit), setup.targetChain, address(attackerContract));
+
+            // The relayer triggers the call to the malicious contract.
+            genericRelayer.relay(setup.sourceChain);
+
+            // The message delivery should fail
+            assertTrue(keccak256(setup.target.integration.getMessage()) != keccak256(attackMsg), "Attacker got his message through");
+        }
+
+        {
+            // Now one victim sends their message. It doesn't need to be from the same source chain.
+            // What's necessary is that a message is delivered to the chain targeted by the attacker.
+            bytes memory victimMsg = "victimMsg";
+
+            uint256 victimBalancePreDelivery = setup.target.refundAddress.balance;
+
+            // We will reutilize the delivery price estimated for the attacker to simplify the code here.
+            // The victim requests their message to be sent.
+            setup.source.coreRelayer.sendPayloadToEvm{
+                value: deliveryPrice.unwrap()
+            }(
+                setup.targetChain,
+                address(setup.target.integration),
+                abi.encodePacked(uint8(0), uint32(victimMsg.length), victimMsg, uint32(0)),
+                TargetNative.wrap(0), 
+                Gas.wrap(gasParams.targetGasLimit),
+                setup.targetChain,
+                address(setup.target.refundAddress)
+            );
+
+            // The relayer delivers the victim's message.
+            // During the delivery process, the forward request injected by the malicious contract is acknowledged.
+            // The victim's refund address is not called due to this.
+            genericRelayer.relay(setup.sourceChain);
+
+            // Ensures the message was received.
+            assertTrue(keccak256(setup.target.integration.getMessage()) == keccak256(victimMsg), "Victim did not get message through");
+            // Here we assert that the victim's refund is safe.
+            assertTrue(victimBalancePreDelivery < setup.target.refundAddress.balance, "Victim's refund wasn't paid");
+        }
+
+        genericRelayer.relay(setup.targetChain);
+
+        // Assert that the attack wasn't successful.
+        assertTrue(attackerSourceAddress.balance == 0, "Attacker gained balance");
+    }
+
+    function testDeliveryData(
+        GasParameters memory gasParams,
+        FeeParameters memory feeParams,
+        bytes memory message
+    ) public {
+        StandardSetupTwoChains memory setup =
+            standardAssumeAndSetupTwoChains(gasParams, feeParams, 1000000);
+
+        vm.recordLogs();
+
+        sendMessageToTargetChain(setup, gasParams.targetGasLimit, 0, message);
+
+        genericRelayer.relay(setup.sourceChain);
+
+        bytes32 deliveryVaaHash = getDeliveryVAAHash(vm.getRecordedLogs());
+
+        DeliveryData memory deliveryData = setup.target.integration.getDeliveryData();
+
+        assertTrue(
+            fromWormholeFormat(deliveryData.sourceAddress) == address(setup.source.integration), "Source address wrong"
+        );
+        assertTrue(deliveryData.sourceChain == setup.sourceChain, "Source chain id wrong");
+        assertTrue(deliveryData.deliveryHash == deliveryVaaHash, "delivery vaa hash wrong");
+        assertTrue(keccak256(setup.target.integration.getMessage()) == keccak256(message), "payload wrong");
+    }
+
+    function testExecuteInstructionTruncatesLongRevertBuffers(
+        GasParameters memory gasParams,
+        FeeParameters memory feeParams,
+        uint32 minTargetGasLimit
+    ) public {
+        StandardSetupTwoChains memory setup = standardAssumeAndSetupTwoChains(gasParams, feeParams, minTargetGasLimit);
+        Gas gasLimit = Gas.wrap(500_000);
+        uint256 sizeRequested = 512;
+        bytes32 targetIntegration = toWormholeFormat(address(new BigRevertBufferIntegration()));
+        // We encode 512 as the requested revert buffer length to our test integration contract
+        bytes memory payload = abi.encode(sizeRequested);
+        bytes32 userAddress = toWormholeFormat(address(0x8080));
+
+        vm.prank(address(setup.target.coreRelayerFull));
+        (uint8 status, Gas gasUsed, bytes memory revertData) = setup.target.coreRelayerFull.executeInstruction(
+            EvmDeliveryInstruction({
+              sourceChain: setup.sourceChain,
+              targetAddress: targetIntegration,
+              payload: payload,
+              gasLimit: gasLimit,
+              totalReceiverValue: TargetNative.wrap(0),
+              targetChainRefundPerGasUnused: GasPrice.wrap(0),
+              senderAddress: userAddress,
+              deliveryHash: bytes32(0),
+              signedVaas: new bytes[](0)
+            })
+        );
+
+        assertTrue(status == uint8(IWormholeRelayerDelivery.DeliveryStatus.RECEIVER_FAILURE));
+        assertTrue(gasUsed <= gasLimit);
+        assertEq(revertData, abi.encodePacked(
+            // First word
+            bytes32(0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f),
+            // Second word
+            bytes32(0x202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f),
+            // Third word
+            bytes32(0x404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f),
+            // Fourth word
+            bytes32(0x606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f),
+            // Four extra bytes
+            bytes4(0x80818283)
+        ));
+    }
+}

+ 202 - 0
ethereum/forge-test/relayer/WormholeRelayerGovernance.t.sol

@@ -0,0 +1,202 @@
+// SPDX-License-Identifier: Apache 2
+
+pragma solidity ^0.8.0;
+
+import {IDeliveryProvider} from "../../contracts/interfaces/relayer/IDeliveryProviderTyped.sol";
+import {DeliveryProvider} from "../../contracts/relayer/deliveryProvider/DeliveryProvider.sol";
+import {DeliveryProviderSetup} from
+    "../../contracts/relayer/deliveryProvider/DeliveryProviderSetup.sol";
+import {DeliveryProviderImplementation} from
+    "../../contracts/relayer/deliveryProvider/DeliveryProviderImplementation.sol";
+import {DeliveryProviderProxy} from
+    "../../contracts/relayer/deliveryProvider/DeliveryProviderProxy.sol";
+import {DeliveryProviderStructs} from
+    "../../contracts/relayer/deliveryProvider/DeliveryProviderStructs.sol";
+import "../../contracts/interfaces/relayer/IWormholeRelayerTyped.sol";
+import {WormholeRelayer} from "../../contracts/relayer/wormholeRelayer/WormholeRelayer.sol";
+import {MockGenericRelayer} from "./MockGenericRelayer.sol";
+import {MockWormhole} from "./MockWormhole.sol";
+import {IWormhole} from "../../contracts/interfaces/IWormhole.sol";
+import {WormholeSimulator, FakeWormholeSimulator} from "./WormholeSimulator.sol";
+import {IWormholeReceiver} from "../../contracts/interfaces/relayer/IWormholeReceiver.sol";
+import {AttackForwardIntegration} from "./AttackForwardIntegration.sol";
+import {MockRelayerIntegration} from "../../contracts/mock/relayer/MockRelayerIntegration.sol";
+import {ForwardTester} from "./ForwardTester.sol";
+import {TestHelpers} from "./TestHelpers.sol";
+import {toWormholeFormat} from "../../contracts/libraries/relayer/Utils.sol";
+import "../../contracts/libraries/external/BytesLib.sol";
+
+import "forge-std/Test.sol";
+import "forge-std/console.sol";
+import "forge-std/Vm.sol";
+
+contract Brick {
+    function checkAndExecuteUpgradeMigration() external view {}
+}
+
+contract WormholeRelayerGovernanceTests is Test {
+    using BytesLib for bytes;
+
+    TestHelpers helpers;
+
+    bytes32 relayerModule = 0x0000000000000000000000000000000000576f726d686f6c6552656c61796572;
+    IWormhole wormhole;
+    IDeliveryProvider deliveryProvider;
+    WormholeSimulator wormholeSimulator;
+    IWormholeRelayer wormholeRelayer;
+
+    function setUp() public {
+        helpers = new TestHelpers();
+        (wormhole, wormholeSimulator) = helpers.setUpWormhole(1);
+        deliveryProvider = helpers.setUpDeliveryProvider(1);
+        wormholeRelayer = helpers.setUpWormholeRelayer(wormhole, address(deliveryProvider));
+    }
+
+    struct GovernanceStack {
+        bytes message;
+        IWormhole.VM preSignedMessage;
+        bytes signed;
+    }
+
+    function signMessage(bytes memory message) internal returns (bytes memory signed) {
+        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("")
+        });
+        signed = wormholeSimulator.encodeAndSignMessage(preSignedMessage);
+    }
+
+    function fillInGovernanceStack(bytes memory message)
+        internal
+        returns (GovernanceStack memory stack)
+    {
+        stack.message = message;
+        stack.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("")
+        });
+        stack.signed = wormholeSimulator.encodeAndSignMessage(stack.preSignedMessage);
+    }
+
+    function testSetDefaultDeliveryProvider() public {
+        IDeliveryProvider deliveryProviderB = helpers.setUpDeliveryProvider(1);
+        IDeliveryProvider deliveryProviderC = helpers.setUpDeliveryProvider(1);
+
+        bytes memory signed = signMessage(
+            abi.encodePacked(
+                relayerModule,
+                uint8(3),
+                uint16(1),
+                bytes32(uint256(uint160(address(deliveryProviderB))))
+            )
+        );
+
+        WormholeRelayer(payable(address(wormholeRelayer))).setDefaultDeliveryProvider(signed);
+
+        assertTrue(wormholeRelayer.getDefaultDeliveryProvider() == address(deliveryProviderB));
+
+        signed = signMessage(
+            abi.encodePacked(
+                relayerModule,
+                uint8(3),
+                uint16(1),
+                bytes32(uint256(uint160(address(deliveryProviderC))))
+            )
+        );
+
+        WormholeRelayer(payable(address(wormholeRelayer))).setDefaultDeliveryProvider(signed);
+
+        assertTrue(wormholeRelayer.getDefaultDeliveryProvider() == address(deliveryProviderC));
+    }
+
+    function testRegisterChain() public {
+        IWormholeRelayer wormholeRelayer1 =
+            helpers.setUpWormholeRelayer(wormhole, address(deliveryProvider));
+        IWormholeRelayer wormholeRelayer2 =
+            helpers.setUpWormholeRelayer(wormhole, address(deliveryProvider));
+        IWormholeRelayer wormholeRelayer3 =
+            helpers.setUpWormholeRelayer(wormhole, address(deliveryProvider));
+
+        helpers.registerWormholeRelayerContract(
+            WormholeRelayer(payable(address(wormholeRelayer1))),
+            wormhole,
+            1,
+            2,
+            toWormholeFormat(address(wormholeRelayer2))
+        );
+
+        helpers.registerWormholeRelayerContract(
+            WormholeRelayer(payable(address(wormholeRelayer1))),
+            wormhole,
+            1,
+            3,
+            toWormholeFormat(address(wormholeRelayer3))
+        );
+
+        assertTrue(
+            WormholeRelayer(payable(address(wormholeRelayer1))).getRegisteredWormholeRelayerContract(
+                2
+            ) == toWormholeFormat(address(wormholeRelayer2))
+        );
+
+        assertTrue(
+            WormholeRelayer(payable(address(wormholeRelayer1))).getRegisteredWormholeRelayerContract(
+                3
+            ) == toWormholeFormat(address(wormholeRelayer3))
+        );
+
+        vm.expectRevert(abi.encodeWithSignature("ChainAlreadyRegistered(uint16,bytes32)", 3, toWormholeFormat(address(wormholeRelayer3))));
+        helpers.registerWormholeRelayerContract(
+            WormholeRelayer(payable(address(wormholeRelayer1))),
+            wormhole,
+            1,
+            3,
+            toWormholeFormat(address(wormholeRelayer2))
+        );
+    }
+
+    function testUpgradeContractToItself() public {
+        address payable myWormholeRelayer =
+            payable(address(helpers.setUpWormholeRelayer(wormhole, address(deliveryProvider))));
+
+        bytes memory noMigrationFunction = signMessage(
+            abi.encodePacked(
+                relayerModule,
+                uint8(2),
+                uint16(1),
+                toWormholeFormat(address(new DeliveryProviderImplementation()))
+            )
+        );
+
+        vm.expectRevert();
+        WormholeRelayer(myWormholeRelayer).submitContractUpgrade(noMigrationFunction);
+
+        Brick brick = new Brick();
+        bytes memory signed = signMessage(
+            abi.encodePacked(relayerModule, uint8(2), uint16(1), toWormholeFormat(address(brick)))
+        );
+
+        WormholeRelayer(myWormholeRelayer).submitContractUpgrade(signed);
+
+        vm.expectRevert();
+        WormholeRelayer(myWormholeRelayer).getDefaultDeliveryProvider();
+    }
+}

+ 349 - 0
ethereum/forge-test/relayer/WormholeSimulator.sol

@@ -0,0 +1,349 @@
+// SPDX-License-Identifier: Apache 2
+pragma solidity ^0.8.0;
+
+import {IWormhole} from "../../contracts/interfaces/IWormhole.sol";
+import {MockWormhole} from "./MockWormhole.sol";
+import "../../contracts/libraries/external/BytesLib.sol";
+
+import "forge-std/Vm.sol";
+import "forge-std/console.sol";
+
+/**
+ * @notice These are the common parts for the signing and the non signing wormhole simulators.
+ * @dev This contract is meant to be used when testing against a mainnet fork.
+ */
+abstract contract WormholeSimulator {
+    using BytesLib for bytes;
+
+    function doubleKeccak256(bytes memory body) internal pure returns (bytes32) {
+        return keccak256(abi.encodePacked(keccak256(body)));
+    }
+
+    function parseVMFromLogs(Vm.Log memory log) public 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,
+        address emitterAddress
+    ) public returns (bytes memory signedMessage) {
+        // Parse wormhole message from ethereum logs
+        IWormhole.VM memory vm_ = parseVMFromLogs(log);
+
+        // Set empty body values before computing the hash
+        vm_.version = uint8(1);
+        vm_.timestamp = uint32(block.timestamp);
+        vm_.emitterChainId = emitterChainId;
+        vm_.emitterAddress = bytes32(uint256(uint160(emitterAddress)));
+
+        return encodeAndSignMessage(vm_);
+    }
+
+    /**
+     * Functions that must be implemented by concrete wormhole simulators.
+     */
+
+    /**
+     * @notice Sets the message fee for a wormhole message.
+     */
+    function setMessageFee(uint256 newFee) public virtual;
+
+    /**
+     * @notice Invalidates a VM. It must be executed before it is parsed and verified by the Wormhole instance to work.
+     */
+    function invalidateVM(bytes memory message) public virtual;
+
+    /**
+     * @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
+        virtual
+        returns (bytes memory signedMessage);
+}
+
+/**
+ * @title A Wormhole Guardian Simulator
+ * @notice This contract simulates signing Wormhole messages emitted in a forge test.
+ * This particular version doesn't sign any message but just exists to keep a standard interface for tests.
+ * @dev This contract is meant to be used with the MockWormhole contract that validates any VM as long
+ *   as its hash wasn't banned.
+ */
+contract FakeWormholeSimulator is WormholeSimulator {
+    // Allow access to Wormhole
+    MockWormhole public wormhole;
+
+    /**
+     * @param initWormhole address of the Wormhole core contract for the mainnet chain being forked
+     */
+    constructor(MockWormhole initWormhole) {
+        wormhole = initWormhole;
+    }
+
+    function setMessageFee(uint256 newFee) public override {
+        wormhole.setMessageFee(newFee);
+    }
+
+    function invalidateVM(bytes memory message) public override {
+        wormhole.invalidateVM(message);
+    }
+
+    /**
+     * @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
+        override
+        returns (bytes memory signedMessage)
+    {
+        // Compute the hash of the body
+        bytes memory body = encodeObservation(vm_);
+        vm_.hash = doubleKeccak256(body);
+
+        signedMessage = abi.encodePacked(
+            vm_.version,
+            wormhole.getCurrentGuardianSetIndex(),
+            // length of signature array
+            uint8(1),
+            // guardian index
+            uint8(0),
+            // r sig argument
+            bytes32(uint256(0)),
+            // s sig argument
+            bytes32(uint256(0)),
+            // v sig argument (encodes public key recovery id, public key type and network of the signature)
+            uint8(0),
+            body
+        );
+    }
+}
+
+/**
+ * @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 mainnet fork.
+ */
+contract SigningWormholeSimulator is WormholeSimulator {
+    // 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(IWormhole wormhole_, uint256 devnetGuardian) {
+        wormhole = wormhole_;
+        devnetGuardianPK = devnetGuardian;
+        overrideToDevnetGuardian(vm.addr(devnetGuardian));
+    }
+
+    function overrideToDevnetGuardian(address devnetGuardian) internal {
+        {
+            // 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 setMessageFee(uint256 newFee) public override {
+        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);
+    }
+
+    function invalidateVM(bytes memory message) public pure override {
+        // Don't do anything. Signatures are easily invalidated modifying the payload.
+        // If it becomes necessary to prevent producing a good signature for this message, that can be done here.
+    }
+
+    /**
+     * @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
+        override
+        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
+        );
+    }
+}

+ 15 - 8
ethereum/foundry.toml

@@ -1,17 +1,15 @@
 [profile.default]
-solc_version = "0.8.4"
+solc_version = "0.8.19"
 optimizer = true
 optimizer_runs = 200
-src="contracts"
+via_ir = false
+src = "contracts"
 # We put the tests into the forge-test directory (instead of test) so that
 # truffle doesn't try to build them
-test="forge-test"
+test = "forge-test"
 
-out = 'out'
-libs = [
-    'lib',
-    'node_modules',
-]
+out = 'build-forge'
+libs = ['lib', 'node_modules']
 remappings = [
     '@openzeppelin/=node_modules/@openzeppelin/',
     '@solidity-parser/=node_modules/@solidity-parser/',
@@ -20,4 +18,13 @@ remappings = [
     'truffle/=node_modules/truffle/',
 ]
 
+[fmt]
+line_length = 100
+multiline_func_header = "params_first"
+# wrap_comments = true
+
+
+[profile.production]
+via_ir = true
+
 # See more config options https://github.com/foundry-rs/foundry/tree/master/config

File diff suppressed because it is too large
+ 701 - 151
ethereum/package-lock.json


+ 26 - 22
ethereum/package.json

@@ -7,39 +7,43 @@
     "@chainsafe/truffle-plugin-abigen": "0.0.1",
     "@openzeppelin/cli": "^2.8.2",
     "@openzeppelin/contracts": "^4.3.1",
-    "@openzeppelin/test-environment": "^0.1.6",
-    "@openzeppelin/test-helpers": "^0.5.9",
     "@truffle/hdwallet-provider": "^1.7.0",
-    "chai": "^4.2.0",
-    "mocha": "^8.2.1",
-    "truffle": "5.3.14",
-    "truffle-assertions": "^0.9.2",
+    "chai": "^4.3.7",
+    "mocha": "^8.4.0",
+    "truffle": "5.8.4",
     "truffle-flattener": "^1.6.0",
-    "truffle-plugin-verify": "^0.5.11"
+    "truffle-plugin-verify": "^0.5.11",
+    "ts-node": "^10.9.1",
+    "typescript": "^4.9.5"
   },
   "scripts": {
-    "build": "truffle compile",
-    "test": "mkdir -p build/contracts && cp node_modules/@openzeppelin/contracts/build/contracts/* build/contracts/ && truffle test --network test",
-    "migrate": "mkdir -p build/contracts && cp node_modules/@openzeppelin/contracts/build/contracts/* build/contracts/ && truffle migrate --to 4",
-    "deploy-bridge-implementation-only": "mkdir -p build/contracts && cp node_modules/@openzeppelin/contracts/build/contracts/* build/contracts/ && truffle migrate --f 6 --to 6",
-    "deploy-token-implementation-only": "mkdir -p build/contracts && cp node_modules/@openzeppelin/contracts/build/contracts/* build/contracts/ && truffle migrate --f 7 --to 7",
-    "deploy-read-only": "mkdir -p build/contracts && cp node_modules/@openzeppelin/contracts/build/contracts/* build/contracts/ && truffle migrate --f 1 --to 2",
-    "deploy-bridges-only": "mkdir -p build/contracts && cp node_modules/@openzeppelin/contracts/build/contracts/* build/contracts/ && truffle migrate --f 3 --to 4",
-    "deploy_weth9": "mkdir -p build/contracts && cp node_modules/@openzeppelin/contracts/build/contracts/* build/contracts/ && truffle migrate --f 9",
-    "deploy-batched-vaa-sender": "mkdir -p build/contracts && cp node_modules/@openzeppelin/contracts/build/contracts/* build/contracts/ && truffle migrate --f 10 --to 10",
+    "build:core": "truffle compile",
+    "build:forge": "if [ \"$DEV\" = \"True\" ]; then forge build; else forge build --via-ir; fi",
+    "build": "npm run build:core && npm run build:forge && typechain --target=ethers-v5 --out-dir=./ethers-contracts \"build-forge/!(test).sol/*.json\"&& npm run erase-types",
+    "erase-types": "ts-node ./ts-scripts/relayer/eraseTypes.ts ./contracts/interfaces/relayer/IDeliveryProviderTyped.sol ./contracts/interfaces/relayer/IWormholeRelayerTyped.sol",
+    "test": "npm run build:core && mkdir -p build/contracts && cp node_modules/@openzeppelin/contracts/build/contracts/* build/contracts/ && truffle test --network test --compile-none",
+    "migrate": "npm run build:core && mkdir -p build/contracts && cp node_modules/@openzeppelin/contracts/build/contracts/* build/contracts/ && truffle migrate --to 4 --compile-none",
+    "deploy-bridge-implementation-only": "npm run build:core && mkdir -p build/contracts && cp node_modules/@openzeppelin/contracts/build/contracts/* build/contracts/ && truffle migrate --f 6 --to 6 --compile-none",
+    "deploy-token-implementation-only": "npm run build:core && mkdir -p build/contracts && cp node_modules/@openzeppelin/contracts/build/contracts/* build/contracts/ && truffle migrate --f 7 --to 7 --compile-none",
+    "deploy-read-only": "npm run build:core && mkdir -p build/contracts && cp node_modules/@openzeppelin/contracts/build/contracts/* build/contracts/ && truffle migrate --f 1 --to 2 --compile-none",
+    "deploy-bridges-only": "npm run build:core && mkdir -p build/contracts && cp node_modules/@openzeppelin/contracts/build/contracts/* build/contracts/ && truffle migrate --f 3 --to 4 --compile-none",
+    "deploy_weth9": "npm run build:core && mkdir -p build/contracts && cp node_modules/@openzeppelin/contracts/build/contracts/* build/contracts/ && truffle migrate --f 9 --compile-none",
     "verify": "patch -u -f node_modules/truffle-plugin-verify/constants.js -i truffle-verify-constants.patch; truffle run verify $npm_config_module@$npm_config_contract_address --network $npm_config_network",
     "verify-token": "patch -u -f node_modules/truffle-plugin-verify/constants.js -i truffle-verify-constants.patch; truffle run verify BridgeToken@$npm_config_contract_address --forceConstructorArgs string:$npm_config_constructor_args --network $npm_config_network",
-    "abigen": "truffle run abigen"
-  },
+    "abigen": "truffle run abigen",
+    "deploy-relayers-evm1": "ENV=kubernetes CONTAINER=evm1 bash ./ts-scripts/relayer/shell/deployInContainer.sh",
+    "deploy-relayers-evm2": "ENV=kubernetes CONTAINER=evm2 bash ./ts-scripts/relayer/shell/deployInContainer.sh",
+    "typecheck": "tsc --noEmit --skipLibCheck"
+    },
   "author": "",
   "license": "ISC",
   "dependencies": {
-    "caver-js": "^1.8.1",
+    "@certusone/wormhole-sdk": "^0.9.11",
+    "@typechain/ethers-v5": "^10.2.0",
     "dotenv": "^10.0.0",
     "elliptic": "^6.5.2",
-    "ganache-cli": "^6.12.1",
     "jsonfile": "^4.0.0",
-    "solc": "^0.8.4",
-    "truffle-hdwallet-provider-klaytn": "^1.4.2"
+    "truffle-hdwallet-provider-klaytn": "^1.4.2",
+    "typechain": "^8.1.1"
   }
 }

+ 5 - 0
ethereum/remappings.txt

@@ -0,0 +1,5 @@
+@openzeppelin/=node_modules/@openzeppelin/
+@solidity-parser/=node_modules/@solidity-parser/
+ds-test/=lib/forge-std/lib/ds-test/src/
+forge-std/=lib/forge-std/src/
+truffle/=node_modules/truffle/

+ 8 - 8
ethereum/test/bridge.js

@@ -146,7 +146,7 @@ contract("Bridge", function () {
 
         let before = await web3.eth.getStorageAt(TokenBridge.address, "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc");
 
-        assert.equal(before.toLowerCase(), BridgeImplementation.address.toLowerCase());
+        assert.equal(BigInt(before), BigInt(BridgeImplementation.address));
 
         await initialized.methods.upgrade("0x" + vm).send({
             value: 0,
@@ -156,7 +156,7 @@ contract("Bridge", function () {
 
         let after = await web3.eth.getStorageAt(TokenBridge.address, "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc");
 
-        assert.equal(after.toLowerCase(), mock.address.toLowerCase());
+        assert.equal(BigInt(after), BigInt(mock.address));
 
         const mockImpl = new web3.eth.Contract(MockBridgeImplementation.abi, TokenBridge.address);
 
@@ -362,7 +362,7 @@ contract("Bridge", function () {
                 gasLimit: 2000000
             });
         } catch (error) {
-            assert.equal(error.message, "Returned error: VM Exception while processing transaction: revert current metadata is up to date")
+            assert.include(error.message, "revert current metadata is up to date")
             failed = true
         }
         assert.ok(failed)
@@ -1145,7 +1145,7 @@ contract("Bridge", function () {
 
         // set WETH contract
         const mock = new web3.eth.Contract(MockBridgeImplementation.abi, TokenBridge.address);
-        mock.methods.testUpdateWETHAddress(WETH).send({
+        await mock.methods.testUpdateWETHAddress(WETH).send({
             from: accounts[0],
             gasLimit: 2000000
         });
@@ -1321,7 +1321,7 @@ contract("Bridge", function () {
                 gasLimit: 2000000
             });
         } catch (error) {
-            assert.equal(error.message, "Returned error: VM Exception while processing transaction: revert transfer exceeds max outstanding bridged token amount")
+            assert.include(error.message, "revert transfer exceeds max outstanding bridged token amount")
             failed = true
         }
 
@@ -1387,7 +1387,7 @@ contract("Bridge", function () {
 
             assert.fail("governance packet accepted")
         } catch (e) {
-            assert.equal(e.data[Object.keys(e.data)[0]].reason, "invalid fork")
+            assert.include(e.message, "revert invalid fork")
         }
     })
 
@@ -1482,7 +1482,7 @@ contract("Bridge", function () {
 
         let before = await web3.eth.getStorageAt(TokenBridge.address, "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc");
 
-        assert.equal(before.toLowerCase(), lastDeployed.address.toLowerCase());
+        assert.equal(BigInt(before), BigInt(lastDeployed.address));
 
         let set = await initialized.methods.upgrade("0x" + vm).send({
             value: 0,
@@ -1492,7 +1492,7 @@ contract("Bridge", function () {
 
         let after = await web3.eth.getStorageAt(TokenBridge.address, "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc");
 
-        assert.equal(after.toLowerCase(), mock.address.toLowerCase());
+        assert.equal(BigInt(after), BigInt(mock.address));
 
         const mockImpl = new web3.eth.Contract(MockBridgeImplementation.abi, TokenBridge.address);
 

+ 8 - 8
ethereum/test/nft.js

@@ -140,7 +140,7 @@ contract("NFT", function () {
 
         let before = await web3.eth.getStorageAt(NFTBridge.address, "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc");
 
-        assert.equal(before.toLowerCase(), NFTBridgeImplementation.address.toLowerCase());
+        assert.equal(BigInt(before), BigInt(NFTBridgeImplementation.address));
 
         await initialized.methods.upgrade("0x" + vm).send({
             value: 0,
@@ -150,7 +150,7 @@ contract("NFT", function () {
 
         let after = await web3.eth.getStorageAt(NFTBridge.address, "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc");
 
-        assert.equal(after.toLowerCase(), mock.address.toLowerCase());
+        assert.equal(BigInt(after), BigInt(mock.address));
 
         const mockImpl = new web3.eth.Contract(MockBridgeImplementation.abi, NFTBridge.address);
 
@@ -599,7 +599,7 @@ contract("NFT", function () {
                 gasLimit: 2000000
             });
         } catch (e) {
-            assert.equal(e.message, "Returned error: VM Exception while processing transaction: revert ERC721: transfer caller is not owner nor approved")
+            assert.include(e.message, "revert ERC721: transfer caller is not owner nor approved")
             failed = true
         }
 
@@ -636,7 +636,7 @@ contract("NFT", function () {
                 gasLimit: 2000000
             });
         } catch (e) {
-            assert.equal(e.message, "Returned error: VM Exception while processing transaction: revert ERC721: transfer of token that is not own")
+            assert.include(e.message, "revert ERC721: transfer of token that is not own")
             failed = true
         }
 
@@ -679,7 +679,7 @@ contract("NFT", function () {
             await wrappedAsset.methods.ownerOf(tokenId).call();
             assert.fail("burned token still exists")
         } catch (e) {
-            assert.equal(e.data[Object.keys(e.data)[0]].reason, "ERC721: owner query for nonexistent token")
+            assert.include(e.message, "revert ERC721: owner query for nonexistent token")
         }
     })
 
@@ -742,7 +742,7 @@ contract("NFT", function () {
 
             assert.fail("governance packet accepted")
         } catch (e) {
-            assert.equal(e.data[Object.keys(e.data)[0]].reason, "invalid fork")
+            assert.include(e.message, "revert invalid fork")
         }
     })
 
@@ -837,7 +837,7 @@ contract("NFT", function () {
 
         let before = await web3.eth.getStorageAt(NFTBridge.address, "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc");
 
-        assert.equal(before.toLowerCase(), lastDeployed.address.toLowerCase());
+        assert.equal(BigInt(before), BigInt(lastDeployed.address));
 
         let set = await initialized.methods.upgrade("0x" + vm).send({
             value: 0,
@@ -847,7 +847,7 @@ contract("NFT", function () {
 
         let after = await web3.eth.getStorageAt(NFTBridge.address, "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc");
 
-        assert.equal(after.toLowerCase(), mock.address.toLowerCase());
+        assert.equal(BigInt(after), BigInt(mock.address));
 
         const mockImpl = new web3.eth.Contract(MockBridgeImplementation.abi, NFTBridge.address);
 

+ 12 - 12
ethereum/test/wormhole.js

@@ -356,7 +356,7 @@ contract("Wormhole", function () {
             await initialized.methods.parseAndVerifyVM("0x" + vm).call();
             assert.fail("accepted signature indexes being the same in a VM");
         } catch (e) {
-            assert.equal(e.data[Object.keys(e.data)[0]].reason, 'signature indices must be ascending')
+            assert.include(e.message, 'revert signature indices must be ascending')
         }
     })
 
@@ -538,7 +538,7 @@ contract("Wormhole", function () {
                 gasLimit: 1000000
             });
         } catch (e) {
-            assert.equal(e.message, "Returned error: VM Exception while processing transaction: revert Invalid key");
+            assert.include(e.message, "revert Invalid key");
             failed = true;
         }
 
@@ -655,7 +655,7 @@ contract("Wormhole", function () {
 
         let before = await web3.eth.getStorageAt(Wormhole.address, "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc");
 
-        assert.equal(before.toLowerCase(), Implementation.address.toLowerCase());
+        assert.equal(BigInt(before), BigInt(Implementation.address));
 
         let set = await initialized.methods.submitContractUpgrade("0x" + vm).send({
             value: 0,
@@ -665,7 +665,7 @@ contract("Wormhole", function () {
 
         let after = await web3.eth.getStorageAt(Wormhole.address, "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc");
 
-        assert.equal(after.toLowerCase(), mock.address.toLowerCase());
+        assert.equal(BigInt(after), BigInt(mock.address));
 
         const mockImpl = new web3.eth.Contract(MockImplementation.abi, Wormhole.address);
 
@@ -719,7 +719,7 @@ contract("Wormhole", function () {
             });
             assert.fail("recover chain ID governance packet on supported chain accepted")
         } catch (e) {
-            assert.equal(e.data[Object.keys(e.data)[0]].reason, "not a fork")
+            assert.include(e.message, "revert not a fork")
         }
     })
 
@@ -763,7 +763,7 @@ contract("Wormhole", function () {
             });
             assert.fail("governance packet of old guardian set accepted")
         } catch (e) {
-            assert.equal(e.data[Object.keys(e.data)[0]].reason, "not signed by current guardian set")
+            assert.include(e.message, "revert not signed by current guardian set")
         }
     })
 
@@ -843,7 +843,7 @@ contract("Wormhole", function () {
             });
             assert.fail("governance packet from wrong governance chain accepted")
         } catch (e) {
-            assert.equal(e.data[Object.keys(e.data)[0]].reason, "wrong governance chain")
+            assert.include(e.message, "revert wrong governance chain")
         }
     })
 
@@ -885,7 +885,7 @@ contract("Wormhole", function () {
             });
             assert.fail("governance packet from wrong governance contract accepted")
         } catch (e) {
-            assert.equal(e.data[Object.keys(e.data)[0]].reason, "wrong governance contract")
+            assert.include(e.message, "revert wrong governance contract")
         }
     })
 
@@ -937,7 +937,7 @@ contract("Wormhole", function () {
 
             assert.fail("governance packet accepted twice")
         } catch (e) {
-            assert.equal(e.data[Object.keys(e.data)[0]].reason, "governance action already consumed")
+            assert.include(e.message, "revert governance action already consumed")
         }
     })
 
@@ -1002,7 +1002,7 @@ contract("Wormhole", function () {
 
             assert.fail("governance packet accepted")
         } catch (e) {
-            assert.equal(e.data[Object.keys(e.data)[0]].reason, "invalid fork")
+            assert.include(e.message, "revert invalid fork")
         }
     })
 
@@ -1095,7 +1095,7 @@ contract("Wormhole", function () {
 
         let before = await web3.eth.getStorageAt(Wormhole.address, "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc");
 
-        assert.equal(before.toLowerCase(), lastDeployed.address.toLowerCase());
+        assert.equal(BigInt(before), BigInt(lastDeployed.address));
 
         let set = await initialized.methods.submitContractUpgrade("0x" + vm).send({
             value: 0,
@@ -1105,7 +1105,7 @@ contract("Wormhole", function () {
 
         let after = await web3.eth.getStorageAt(Wormhole.address, "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc");
 
-        assert.equal(after.toLowerCase(), mock.address.toLowerCase());
+        assert.equal(BigInt(after), BigInt(mock.address));
 
         const mockImpl = new web3.eth.Contract(MockImplementation.abi, Wormhole.address);
 

+ 2 - 1
ethereum/truffle-config.js

@@ -1,9 +1,9 @@
 require("dotenv").config({ path: ".env" });
 const HDWalletProvider = require("@truffle/hdwallet-provider");
 const KLAYHDWalletProvider = require("truffle-hdwallet-provider-klaytn");
-const Caver = require("caver-js");
 
 module.exports = {
+  contracts_directory: "contracts/{*.sol,bridge/{*.sol,interfaces/*.sol,token/*.sol,mock/*.sol,utils/*.sol},interfaces/IWormhole.sol,mock/*.sol,nft/{*.sol,interfaces/*.sol,token/*.sol,mock/*.sol}}",
   networks: {
     development: {
       host: "127.0.0.1",
@@ -375,6 +375,7 @@ module.exports = {
           enabled: true,
           runs: 200,
         },
+        viaIR: false,
       },
     },
   },

+ 3 - 0
ethereum/ts-scripts/relayer/.env.ci

@@ -0,0 +1,3 @@
+GUARDIAN_KEY=cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0
+GUARDIAN_KEY2=c3b2e45c422a1602333a64078aeb42637370b0f48fe385f9cfa6ad54a8e0c47e
+WALLET_KEY=0x6370fd033278c143179d81c5526140625662b8daa446c22ee2d73db3707e620c

+ 5 - 0
ethereum/ts-scripts/relayer/.env.kubernetes

@@ -0,0 +1,5 @@
+GUARDIAN_KEY=cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0
+GUARDIAN_KEY2=c3b2e45c422a1602333a64078aeb42637370b0f48fe385f9cfa6ad54a8e0c47e
+WALLET_KEY=0x6370fd033278c143179d81c5526140625662b8daa446c22ee2d73db3707e620c 
+#the wallet key is private key 2 on the ganache instance, it is a different private key than is used for other deployments, which is private key 0.
+# 0x22d491Bde2303f2f43325b2108D26f1eAbA1e32b is the public address

+ 2 - 0
ethereum/ts-scripts/relayer/.env.tilt

@@ -0,0 +1,2 @@
+GUARDIAN_KEY=cfb12303a19cde580bb4dd771639b0d26bc68353645571a8cff516ab2ee113a0
+WALLET_KEY=0x6370fd033278c143179d81c5526140625662b8daa446c22ee2d73db3707e620c

+ 1 - 0
ethereum/ts-scripts/relayer/.gitignore

@@ -0,0 +1 @@
+output/**/*

+ 39 - 0
ethereum/ts-scripts/relayer/README.md

@@ -0,0 +1,39 @@
+# How to use these scripts
+
+## Configuration
+
+Private keys should be placed in a .env file corresponding to the Environment you intend to work in. For example, tilt private keys should be kept in ./.env.tilt
+
+If you do not set an environment, the 'default' environment will be used, and .env will be read.
+
+All other configuration is done through files in the ./config/\${env} directory.
+
+./config/\${env}/chains.json is the file which controls how many chains will be executed against, as well as their RPC and basic info.
+
+./config/\${env}/contracts.json is the file which allows you to target specific contracts on each chain.
+
+./config/\${env}/scriptConfigs contains custom configurations for individual scripts. Not all scripts have custom arguments.
+
+## Running the scripts
+
+All files in the coreRelayer, deliveryProvider, and MockIntegration directories are runnable. These are intended to run from the /ethereum directory.
+
+The target environment must be passed in as an environment variable. So, for example, you can run the DeliveryProvider deployment script by running:
+
+```
+ENV=tilt ts-node ./ts-scripts/relayer/deliveryProvider/deployDeliveryProvider.ts
+```
+
+## Chaining multiple scripts
+
+Scripts are meant to be run individually or successively. Scripts which deploy contracts will write the deployed addresses into the ./output folder.
+
+If "useLastRun" is set to true in the contracts.json configuration file, the lastrun files from the deployment scripts will be used, rather than the deployed addresses of the contracts.json file. This allows you to easily run things like
+
+```
+ENV=tilt ts-node ./ts-scripts/relayer/deliveryProvider/upgradeDeliveryProvider.ts && ts-node ./ts-scripts/relayer/mockIntegration/messageTest.ts
+```
+
+The ./shell directory contains shell scripts which combine commonly chained actions together.
+
+For example, ./shell/deployConfigureTest.sh will deploy the DeliveryProvider, WormholeRelayer, and MockIntegration contracts. Configure them all to point at eachother, and then run messageTest to test that everything worked. Note: useLastRun in contracts.json needs to be set to "true" in order for this script to work.

+ 52 - 0
ethereum/ts-scripts/relayer/config/checkNetworks.ts

@@ -0,0 +1,52 @@
+import {
+  env,
+  getSigner,
+  init,
+  loadChains,
+  loadPrivateKey,
+} from "../helpers/env";
+import { readFileSync, writeFileSync } from "fs";
+
+const processName = "checkNetworks";
+
+init();
+const chains = loadChains();
+
+async function main() {
+  console.log(`Env: ${env}`);
+  console.log(`Start ${processName}!`);
+
+  console.log("Checking networks before deploying...");
+  for (const chain of chains) {
+    const signer = getSigner(chain);
+    const network = await signer.provider?.getNetwork();
+    const balance = await signer.getBalance();
+    if (!network?.name || !balance) {
+      console.log(
+        "Failed to get network for chain " + chain.chainId + ". Exiting..."
+      );
+      process.exit(1);
+    }
+    console.log(`Balance ${balance.toString()}`);
+    console.log(`Network ${network.name} checked`);
+  }
+  console.log("");
+  console.log("Networks checked");
+  console.log("");
+
+  if (process.argv.find((arg) => arg == "--set-last-run")) {
+    const path = `./ts-scripts/relayer/config/${env}/contracts.json`;
+    const contractsFile = readFileSync(path);
+    if (!contractsFile) {
+      throw Error("Failed to find contracts file for this process!");
+    }
+    const contracts = JSON.parse(contractsFile.toString());
+    contracts.useLastRun = true;
+    writeFileSync(path, JSON.stringify(contracts, undefined, 2));
+  }
+}
+
+main().catch((e) => {
+  console.error(e);
+  process.exit(1);
+});

+ 20 - 0
ethereum/ts-scripts/relayer/config/ci/chains.json

@@ -0,0 +1,20 @@
+{
+  "guardianSetIndex": 0,
+  "description": "This file contains the chains against which all the scripts should run.",
+  "guardianRPC": "http://guardian:7071",
+  "chains": [
+    {
+      "description": "Ganache is supposed to be 1337, but the on-chain block.chainid returns 1",
+      "evmNetworkId": 1,
+      "chainId": 2,
+      "rpc": "http://eth-devnet:8545",
+      "wormholeAddress": "0xC89Ce4735882C9F0f0FE26686c53074E09B0D550"
+    },
+    {
+      "evmNetworkId": 1397,
+      "chainId": 4,
+      "rpc": "http://eth-devnet2:8545",
+      "wormholeAddress": "0xC89Ce4735882C9F0f0FE26686c53074E09B0D550"
+    }
+  ]
+}

+ 44 - 0
ethereum/ts-scripts/relayer/config/ci/contracts.json

@@ -0,0 +1,44 @@
+{
+  "description": "This file contains the addresses for the contracts on each chain. If useLastRun is true, this file will be ignored, and the addresses will be taken from the lastrun.json of the deployment scripts.",
+  "useLastRun": false,
+  "deliveryProviders": [
+    {
+      "chainId": 2,
+      "address": "0x1ef9e15c3bbf0555860b5009B51722027134d53a"
+    },
+    {
+      "chainId": 4,
+      "address": "0x1ef9e15c3bbf0555860b5009B51722027134d53a"
+    }
+  ],
+  "wormholeRelayers": [
+    {
+      "chainId": 2,
+      "address": "0xE66C1Bc1b369EF4F376b84373E3Aa004E8F4C083"
+    },
+    {
+      "chainId": 4,
+      "address": "0xE66C1Bc1b369EF4F376b84373E3Aa004E8F4C083"
+    }
+  ],
+  "mockIntegrations": [
+    {
+      "chainId": 2,
+      "address": "0x0eb0dD3aa41bD15C706BC09bC03C002b7B85aeAC"
+    },
+    {
+      "chainId": 4,
+      "address": "0x0eb0dD3aa41bD15C706BC09bC03C002b7B85aeAC"
+    }
+  ],
+  "create2Factories": [
+    {
+      "chainId": 2,
+      "address": "0x17e91224c30c5b0B13ba2ef1E84FE880Cb902352"
+    },
+    {
+      "chainId": 4,
+      "address": "0x17e91224c30c5b0B13ba2ef1E84FE880Cb902352"
+    }
+  ]
+}

+ 41 - 0
ethereum/ts-scripts/relayer/config/ci/scriptConfigs/configureDeliveryProvider.json

@@ -0,0 +1,41 @@
+{
+  "addresses": [
+    {
+      "chainId": 2,
+      "rewardAddress": "0x22d491Bde2303f2f43325b2108D26f1eAbA1e32b",
+      "approvedSenders": [
+        {
+          "address": "0x22d491Bde2303f2f43325b2108D26f1eAbA1e32b",
+          "approved": true
+        }
+      ]
+    },
+    {
+      "chainId": 4,
+      "rewardAddress": "0x22d491Bde2303f2f43325b2108D26f1eAbA1e32b",
+      "approvedSenders": [
+        {
+          "address": "0x22d491Bde2303f2f43325b2108D26f1eAbA1e32b",
+          "approved": true
+        }
+      ]
+    }
+  ],
+
+  "pricingInfo": [
+    {
+      "chainId": 2,
+      "deliverGasOverhead": "350000",
+      "updatePriceGas": "300000000000",
+      "updatePriceNative": "100000",
+      "maximumBudget": "1000000000000000000"
+    },
+    {
+      "chainId": 4,
+      "deliverGasOverhead": "350000",
+      "updatePriceGas": "300000000000",
+      "updatePriceNative": "100000",
+      "maximumBudget": "1000000000000000000"
+    }
+  ]
+}

+ 20 - 0
ethereum/ts-scripts/relayer/config/kubernetes/chains.json

@@ -0,0 +1,20 @@
+{
+  "guardianSetIndex": 0,
+  "description": "This file contains the chains against which all the scripts should run.",
+  "guardianRPC": "http://localhost:7071",
+  "chains": [
+    {
+      "description": "Ganache is supposed to be 1337, but the on-chain block.chainid returns 1",
+      "evmNetworkId": 1,
+      "chainId": 2,
+      "rpc": "http://localhost:8545",
+      "wormholeAddress": "0xC89Ce4735882C9F0f0FE26686c53074E09B0D550"
+    },
+    {
+      "evmNetworkId": 1397,
+      "chainId": 4,
+      "rpc": "http://localhost:8545",
+      "wormholeAddress": "0xC89Ce4735882C9F0f0FE26686c53074E09B0D550"
+    }
+  ]
+}

+ 54 - 0
ethereum/ts-scripts/relayer/config/kubernetes/contracts.json

@@ -0,0 +1,54 @@
+{
+  "description": "This file contains the addresses for the contracts on each chain. If useLastRun is true, this file will be ignored, and the addresses will be taken from the lastrun.json of the deployment scripts.",
+  "useLastRun": false,
+  "deliveryProviders": [
+    {
+      "chainId": 2,
+      "address": "0x1ef9e15c3bbf0555860b5009B51722027134d53a"
+    },
+    {
+      "chainId": 4,
+      "address": "0x1ef9e15c3bbf0555860b5009B51722027134d53a"
+    }
+  ],
+  "wormholeRelayers": [
+    {
+      "chainId": 2,
+      "address": "0xE66C1Bc1b369EF4F376b84373E3Aa004E8F4C083"
+    },
+    {
+      "chainId": 4,
+      "address": "0xE66C1Bc1b369EF4F376b84373E3Aa004E8F4C083"
+    }
+  ],
+  "wormholeRelayersDev": [
+    {
+      "chainId": 2,
+      "address": "0x53855d4b64E9A3CF59A84bc768adA716B5536BC5"
+    },
+    {
+      "chainId": 4,
+      "address": "0x53855d4b64E9A3CF59A84bc768adA716B5536BC5"
+    }
+  ],
+  "mockIntegrations": [
+    {
+      "chainId": 2,
+      "address": "0x0eb0dD3aa41bD15C706BC09bC03C002b7B85aeAC"
+    },
+    {
+      "chainId": 4,
+      "address": "0x0eb0dD3aa41bD15C706BC09bC03C002b7B85aeAC"
+    }
+  ],
+  "create2Factories": [
+    {
+      "chainId": 2,
+      "address": "0x17e91224c30c5b0B13ba2ef1E84FE880Cb902352"
+    },
+    {
+      "chainId": 4,
+      "address": "0x17e91224c30c5b0B13ba2ef1E84FE880Cb902352"
+    }
+  ]
+}

+ 41 - 0
ethereum/ts-scripts/relayer/config/kubernetes/scriptConfigs/configureDeliveryProvider.json

@@ -0,0 +1,41 @@
+{
+  "addresses": [
+    {
+      "chainId": 2,
+      "rewardAddress": "0x22d491Bde2303f2f43325b2108D26f1eAbA1e32b",
+      "approvedSenders": [
+        {
+          "address": "0x22d491Bde2303f2f43325b2108D26f1eAbA1e32b",
+          "approved": true
+        }
+      ]
+    },
+    {
+      "chainId": 4,
+      "rewardAddress": "0x22d491Bde2303f2f43325b2108D26f1eAbA1e32b",
+      "approvedSenders": [
+        {
+          "address": "0x22d491Bde2303f2f43325b2108D26f1eAbA1e32b",
+          "approved": true
+        }
+      ]
+    }
+  ],
+
+  "pricingInfo": [
+    {
+      "chainId": 2,
+      "deliverGasOverhead": "350000",
+      "updatePriceGas": "300000000000",
+      "updatePriceNative": "100000",
+      "maximumBudget": "1000000000000000000"
+    },
+    {
+      "chainId": 4,
+      "deliverGasOverhead": "350000",
+      "updatePriceGas": "300000000000",
+      "updatePriceNative": "100000",
+      "maximumBudget": "1000000000000000000"
+    }
+  ]
+}

+ 71 - 0
ethereum/ts-scripts/relayer/config/syncContractsJson.ts

@@ -0,0 +1,71 @@
+import { readFileSync, writeFileSync } from "fs";
+import {
+  getWormholeRelayer,
+  getCreate2Factory,
+  getMockIntegration,
+  getDeliveryProvider,
+  init,
+  loadChains,
+} from "../helpers/env";
+
+const env = init({ lastRunOverride: true });
+const chains = loadChains();
+
+interface Address {
+  chainId: number;
+  address: string;
+}
+interface ContractsJson {
+  deliveryProviders: Address[];
+  wormholeRelayers: Address[];
+  mockIntegrations: Address[];
+  create2Factories: Address[];
+}
+
+async function main() {
+  const path = `./ts-scripts/relayer/config/${env}/contracts.json`;
+  const blob = readFileSync(path);
+  const contracts: ContractsJson = JSON.parse(String(blob));
+  console.log("Old:");
+  console.log(`${String(blob)}`);
+  contracts.deliveryProviders = [] as any;
+  contracts.wormholeRelayers = [] as any;
+  contracts.mockIntegrations = [] as any;
+  contracts.create2Factories = [] as any;
+  for (const chain of chains) {
+    update(contracts.deliveryProviders, {
+      chainId: chain.chainId,
+      address: getDeliveryProvider(chain).address,
+    });
+    update(contracts.wormholeRelayers, {
+      chainId: chain.chainId,
+      address: (await getWormholeRelayer(chain)).address,
+    });
+    update(contracts.mockIntegrations, {
+      chainId: chain.chainId,
+      address: getMockIntegration(chain).address,
+    });
+    update(contracts.create2Factories, {
+      chainId: chain.chainId,
+      address: getCreate2Factory(chain).address,
+    });
+  }
+  const newStr = JSON.stringify(contracts, undefined, 2);
+  console.log("New:");
+  console.log(`${String(newStr)}`);
+  writeFileSync(path, newStr);
+}
+
+function update(arr: Address[], newAddress: Address) {
+  const idx = arr.findIndex((a) => a.chainId === newAddress.chainId);
+  if (idx === -1) {
+    arr.push(newAddress);
+  } else {
+    arr[idx] = newAddress;
+  }
+}
+
+main().catch((e) => {
+  console.error(e);
+  process.exit(1);
+});

+ 78 - 0
ethereum/ts-scripts/relayer/config/testnet/chains.json

@@ -0,0 +1,78 @@
+{
+  "guardianSetIndex": 0,
+  "description": "This file contains the chains against which all the scripts should run.",
+  "chains": [
+    {
+      "description": "Avalanche testnet fuji",
+      "evmNetworkId": 43113,
+      "chainId": 6,
+      "rpc": "https://api.avax-test.network/ext/bc/C/rpc",
+      "wormholeAddress": "0x7bbcE28e64B3F8b84d876Ab298393c38ad7aac4C"
+    },
+    {
+      "description": "Celo Testnet",
+      "evmNetworkId": 44787,
+      "chainId": 14,
+      "rpc": "https://alfajores-forno.celo-testnet.org",
+      "wormholeAddress": "0x88505117CA88e7dd2eC6EA1E13f0948db2D50D56"
+    },
+    {
+      "description": "BSC Testnet",
+      "evmNetworkId": 97,
+      "chainId": 4,
+      "rpc": "https://bsc-testnet.public.blastapi.io",
+      "wormholeAddress": "0x68605AD7b15c732a30b1BbC62BE8F2A509D74b4D"
+    },
+    {
+      "description": "Mumbai",
+      "evmNetworkId": 80001,
+      "chainId": 5,
+      "rpc": "https://matic-mumbai.chainstacklabs.com",
+      "wormholeAddress": "0x0CBE91CF822c73C2315FB05100C2F714765d5c20"
+    },
+    {
+      "description": "Moonbase Alpha",
+      "evmNetworkId": 1287,
+      "chainId": 16,
+      "rpc": "https://rpc.testnet.moonbeam.network",
+      "wormholeAddress": "0xa5B7D85a8f27dd7907dc8FdC21FA5657D5E2F901"
+    }
+  ],
+  "registrationChains": [
+    {
+      "description": "Avalanche testnet fuji",
+      "evmNetworkId": 43113,
+      "chainId": 6,
+      "rpc": "https://api.avax-test.network/ext/bc/C/rpc",
+      "wormholeAddress": "0x7bbcE28e64B3F8b84d876Ab298393c38ad7aac4C"
+    },
+    {
+      "description": "Celo Testnet",
+      "evmNetworkId": 44787,
+      "chainId": 14,
+      "rpc": "https://alfajores-forno.celo-testnet.org",
+      "wormholeAddress": "0x88505117CA88e7dd2eC6EA1E13f0948db2D50D56"
+    },
+    {
+      "description": "BSC Testnet",
+      "evmNetworkId": 97,
+      "chainId": 4,
+      "rpc": "https://bsc-testnet.public.blastapi.io",
+      "wormholeAddress": "0x68605AD7b15c732a30b1BbC62BE8F2A509D74b4D"
+    },
+    {
+      "description": "Mumbai",
+      "evmNetworkId": 80001,
+      "chainId": 5,
+      "rpc": "https://matic-mumbai.chainstacklabs.com",
+      "wormholeAddress": "0x0CBE91CF822c73C2315FB05100C2F714765d5c20"
+    },
+    {
+      "description": "Moonbase Alpha",
+      "evmNetworkId": 1287,
+      "chainId": 16,
+      "rpc": "https://rpc.testnet.moonbeam.network",
+      "wormholeAddress": "0xa5B7D85a8f27dd7907dc8FdC21FA5657D5E2F901"
+    }
+  ]
+}

+ 92 - 0
ethereum/ts-scripts/relayer/config/testnet/contracts.json

@@ -0,0 +1,92 @@
+{
+  "description": "This file contains the addresses for the contracts on each chain. If useLastRun is true, this file will be ignored, and the addresses will be taken from the lastrun.json of the deployment scripts.",
+  "useLastRun": true,
+  "deliveryProviders": [
+    {
+      "chainId": 6,
+      "address": "0xd5903a063f604D4615E5c2760b7b80D491564BBe"
+    },
+    {
+      "chainId": 14,
+      "address": "0x93d56f29542c156B3e36f10dE41124B499664ff7"
+    },
+    {
+      "chainId": 4,
+      "address": "0x813AB43ab264362c55BF35A1448d0fd8135049a6"
+    },
+    {
+      "chainId": 5,
+      "address": "0xBF684878906629E72079D4f07D75Ef7165238092"
+    },
+    {
+      "chainId": 16,
+      "address": "0xBF684878906629E72079D4f07D75Ef7165238092"
+    }
+  ],
+  "wormholeRelayers": [
+    {
+      "chainId": 6,
+      "address": "0xA3cF45939bD6260bcFe3D66bc73d60f19e49a8BB"
+    },
+    {
+      "chainId": 14,
+      "address": "0x306B68267Deb7c5DfCDa3619E22E9Ca39C374f84"
+    },
+    {
+      "chainId": 4,
+      "address": "0x80aC94316391752A193C1c47E27D382b507c93F3"
+    },
+    {
+      "chainId": 5,
+      "address": "0x0591C25ebd0580E0d4F27A82Fc2e24E7489CB5e0"
+    },
+    {
+      "chainId": 16,
+      "address": "0x0591C25ebd0580E0d4F27A82Fc2e24E7489CB5e0"
+    }
+  ],
+  "mockIntegrations": [
+    {
+      "chainId": 6,
+      "address": "0x5E52f3eB0774E5e5f37760BD3Fca64951D8F74Ae"
+    },
+    {
+      "chainId": 14,
+      "address": "0x7f1d8E809aBB3F6Dc9B90F0131C3E8308046E190"
+    },
+    {
+      "chainId": 4,
+      "address": "0xb6A04D6672F005787147472Be20d39741929Aa03"
+    },
+    {
+      "chainId": 5,
+      "address": "0x3bF0c43d88541BBCF92bE508ec41e540FbF28C56"
+    },
+    {
+      "chainId": 16,
+      "address": "0x3bF0c43d88541BBCF92bE508ec41e540FbF28C56"
+    }
+  ],
+  "create2Factories": [
+    {
+      "chainId": 6,
+      "address": "0xbB2C5134cCDAC02047D915aCd451B697E4D71E1a"
+    },
+    {
+      "chainId": 14,
+      "address": "0xF4325DD26aF3cEeAB3Bb35ECC916F332505d9847"
+    },
+    {
+      "chainId": 4,
+      "address": "0x1B8d30A267041693b7DAA3c24E47746A28885054"
+    },
+    {
+      "chainId": 5,
+      "address": "0xE49B157DA7B71308d32512ec150ffEa8677F4986"
+    },
+    {
+      "chainId": 16,
+      "address": "0xE49B157DA7B71308d32512ec150ffEa8677F4986"
+    }
+  ]
+}

+ 91 - 0
ethereum/ts-scripts/relayer/config/testnet/scriptConfigs/configureDeliveryProvider.json

@@ -0,0 +1,91 @@
+{
+  "addresses": [
+    {
+      "chainId": 6,
+      "rewardAddress": "0x61a51662f0B30Bf176484cAc5B4a033C497cB2f3",
+      "approvedSenders": [
+        {
+          "address": "0x61a51662f0B30Bf176484cAc5B4a033C497cB2f3",
+          "approved": true
+        }
+      ]
+    },
+    {
+      "chainId": 14,
+      "rewardAddress": "0x61a51662f0B30Bf176484cAc5B4a033C497cB2f3",
+      "approvedSenders": [
+        {
+          "address": "0x61a51662f0B30Bf176484cAc5B4a033C497cB2f3",
+          "approved": true
+        }
+      ]
+    },
+    {
+      "chainId": 5,
+      "rewardAddress": "0x61a51662f0B30Bf176484cAc5B4a033C497cB2f3",
+      "approvedSenders": [
+        {
+          "address": "0x61a51662f0B30Bf176484cAc5B4a033C497cB2f3",
+          "approved": true
+        }
+      ]
+    },
+    {
+      "chainId": 16,
+      "rewardAddress": "0x61a51662f0B30Bf176484cAc5B4a033C497cB2f3",
+      "approvedSenders": [
+        {
+          "address": "0x61a51662f0B30Bf176484cAc5B4a033C497cB2f3",
+          "approved": true
+        }
+      ]
+    },
+    {
+      "chainId": 4,
+      "rewardAddress": "0x61a51662f0B30Bf176484cAc5B4a033C497cB2f3",
+      "approvedSenders": [
+        {
+          "address": "0x61a51662f0B30Bf176484cAc5B4a033C497cB2f3",
+          "approved": true
+        }
+      ]
+    }
+  ],
+  "pricingInfo": [
+    {
+      "chainId": 6,
+      "deliverGasOverhead": "350000",
+      "updatePriceGas": "30000000000",
+      "updatePriceNative": "100000",
+      "maximumBudget": "1000000000000000000"
+    },
+    {
+      "chainId": 14,
+      "deliverGasOverhead": "350000",
+      "updatePriceGas": "30000000000",
+      "updatePriceNative": "100000",
+      "maximumBudget": "1000000000000000000"
+    },
+    {
+      "chainId": 5,
+      "deliverGasOverhead": "350000",
+      "updatePriceGas": "7322000000",
+      "updatePriceNative": "100000",
+      "maximumBudget": "1000000000000000000"
+    },
+    {
+      "chainId": 4,
+      "deliverGasOverhead": "350000",
+      "updatePriceGas": "30000000000",
+      "updatePriceNative": "100000",
+      "maximumBudget": "1000000000000000000"
+    },
+    {
+      "chainId": 16,
+      "deliverGasOverhead": "350000",
+      "updatePriceGas": "10000000000",
+      "updatePriceNative": "100000",
+      "maximumBudget": "1000000000000000000"
+    }
+  ]
+}

+ 20 - 0
ethereum/ts-scripts/relayer/config/tilt/chains.json

@@ -0,0 +1,20 @@
+{
+  "guardianSetIndex": 0,
+  "description": "This file contains the chains against which all the scripts should run.",
+  "guardianRPC": "http://localhost:7071",
+  "chains": [
+    {
+      "description": "Ganache is supposed to be 1337, but the on-chain block.chainid returns 1",
+      "evmNetworkId": 1,
+      "chainId": 2,
+      "rpc": "http://localhost:8545",
+      "wormholeAddress": "0xC89Ce4735882C9F0f0FE26686c53074E09B0D550"
+    },
+    {
+      "evmNetworkId": 1397,
+      "chainId": 4,
+      "rpc": "http://localhost:8546",
+      "wormholeAddress": "0xC89Ce4735882C9F0f0FE26686c53074E09B0D550"
+    }
+  ]
+}

+ 54 - 0
ethereum/ts-scripts/relayer/config/tilt/contracts.json

@@ -0,0 +1,54 @@
+{
+  "description": "This file contains the addresses for the contracts on each chain. If useLastRun is true, this file will be ignored, and the addresses will be taken from the lastrun.json of the deployment scripts.",
+  "useLastRun": false,
+  "deliveryProviders": [
+    {
+      "chainId": 2,
+      "address": "0x1ef9e15c3bbf0555860b5009B51722027134d53a"
+    },
+    {
+      "chainId": 4,
+      "address": "0x1ef9e15c3bbf0555860b5009B51722027134d53a"
+    }
+  ],
+  "wormholeRelayers": [
+    {
+      "chainId": 2,
+      "address": "0xE66C1Bc1b369EF4F376b84373E3Aa004E8F4C083"
+    },
+    {
+      "chainId": 4,
+      "address": "0xE66C1Bc1b369EF4F376b84373E3Aa004E8F4C083"
+    }
+  ],
+  "wormholeRelayersDev": [
+    {
+      "chainId": 2,
+      "address": "0x53855d4b64E9A3CF59A84bc768adA716B5536BC5"
+    },
+    {
+      "chainId": 4,
+      "address": "0x53855d4b64E9A3CF59A84bc768adA716B5536BC5"
+    }
+  ],
+  "mockIntegrations": [
+    {
+      "chainId": 2,
+      "address": "0x0eb0dD3aa41bD15C706BC09bC03C002b7B85aeAC"
+    },
+    {
+      "chainId": 4,
+      "address": "0x0eb0dD3aa41bD15C706BC09bC03C002b7B85aeAC"
+    }
+  ],
+  "create2Factories": [
+    {
+      "chainId": 2,
+      "address": "0x17e91224c30c5b0B13ba2ef1E84FE880Cb902352"
+    },
+    {
+      "chainId": 4,
+      "address": "0x17e91224c30c5b0B13ba2ef1E84FE880Cb902352"
+    }
+  ]
+}

+ 37 - 0
ethereum/ts-scripts/relayer/config/tilt/scriptConfigs/configureDeliveryProvider.json

@@ -0,0 +1,37 @@
+{
+  "addresses": [
+    {
+      "chainId": 2,
+      "rewardAddress": "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1",
+      "approvedSenders": [{
+        "address": "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1",
+        "approved": true
+      }]
+    },
+    {
+      "chainId": 4,
+      "rewardAddress": "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1",
+      "approvedSenders": [{
+        "address": "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1",
+        "approved": true
+      }]
+    }
+  ],
+
+  "pricingInfo": [
+    {
+      "chainId": 2,
+      "deliverGasOverhead": "350000",
+      "updatePriceGas": "300000000000",
+      "updatePriceNative": "100000",
+      "maximumBudget": "1000000000000000000"
+    },
+    {
+      "chainId": 4,
+      "deliverGasOverhead": "350000",
+      "updatePriceGas": "300000000000",
+      "updatePriceNative": "100000",
+      "maximumBudget": "1000000000000000000"
+    }
+  ]
+}

+ 22 - 0
ethereum/ts-scripts/relayer/create2Factory/deployCreate2Factory.ts

@@ -0,0 +1,22 @@
+import {
+  init,
+  writeOutputFiles,
+  getOperatingChains,
+} from "../helpers/env";
+import { deployCreate2Factory } from "../helpers/deployments";
+
+const processName = "deployCreate2Factory";
+init();
+const operatingChains = getOperatingChains();
+
+async function run() {
+  console.log("Start!");
+
+  const create2Factories = await Promise.all(
+    operatingChains.map(deployCreate2Factory)
+  );
+
+  writeOutputFiles({ create2Factories }, processName);
+}
+
+run().then(() => console.log("Done!"));

+ 108 - 0
ethereum/ts-scripts/relayer/deliveryProvider/configureDeliveryProvider.ts

@@ -0,0 +1,108 @@
+import { ChainId, tryNativeToHexString } from "@certusone/wormhole-sdk";
+import type { BigNumberish } from "ethers";
+import {
+  init,
+  loadChains,
+  ChainInfo,
+  loadScriptConfig,
+  getWormholeRelayerAddress,
+  getDeliveryProvider,
+  getDeliveryProviderAddress,
+  getOperatingChains,
+} from "../helpers/env";
+import { wait } from "../helpers/utils";
+
+import type { DeliveryProviderStructs } from "../../../ethers-contracts/DeliveryProvider";
+
+/**
+ * Meant for `config.pricingInfo`
+ */
+interface PricingInfo {
+  chainId: ChainId
+  deliverGasOverhead: BigNumberish
+  updatePriceGas: BigNumberish
+  updatePriceNative: BigNumberish
+  maximumBudget: BigNumberish
+};
+
+const processName = "configureDeliveryProvider";
+init();
+const operatingChains = getOperatingChains();
+const chains = loadChains();
+const config = loadScriptConfig(processName);
+
+async function run() {
+  console.log("Start! " + processName);
+
+  for (let i = 0; i < operatingChains.length; i++) {
+    await configureChainsDeliveryProvider(chains[i]);
+  }
+}
+
+async function configureChainsDeliveryProvider(chain: ChainInfo) {
+  console.log("about to perform DeliveryProvider configuration for chain " + chain.chainId);
+  const deliveryProvider = getDeliveryProvider(chain);
+  const coreRelayer = await getWormholeRelayerAddress(chain);
+
+  const thisChainsConfigInfo = config.addresses.find(
+    (x: any) => x.chainId == chain.chainId
+  );
+
+  if (!thisChainsConfigInfo) {
+    throw new Error(
+      "Failed to find address config info for chain " + chain.chainId
+    );
+  }
+  if (!thisChainsConfigInfo.rewardAddress) {
+    throw new Error(
+      "Failed to find reward address info for chain " + chain.chainId
+    );
+  }
+
+  const coreConfig: DeliveryProviderStructs.CoreConfigStruct = {
+    updateWormholeRelayer: true,
+    updateRewardAddress: true,
+    coreRelayer,
+    rewardAddress: thisChainsConfigInfo.rewardAddress,
+  };
+  const updates: DeliveryProviderStructs.UpdateStruct[] = [];
+
+  // Set the entire relay provider configuration
+  for (const targetChain of chains) {
+    const targetChainPriceUpdate = (config.pricingInfo as PricingInfo[]).find(
+      (x: any) => x.chainId == targetChain.chainId
+    );
+    if (!targetChainPriceUpdate) {
+      throw new Error(
+        "Failed to find pricingInfo for chain " + targetChain.chainId
+      );
+    }
+    const targetChainProviderAddress = getDeliveryProviderAddress(targetChain);
+    const remoteDeliveryProvider =
+      "0x" + tryNativeToHexString(targetChainProviderAddress, "ethereum");
+    const chainConfigUpdate = {
+      chainId: targetChain.chainId,
+      updateAssetConversionBuffer: true,
+      updateDeliverGasOverhead: true,
+      updatePrice: true,
+      updateMaximumBudget: true,
+      updateTargetChainAddress: true,
+      updateSupportedChain: true,
+      isSupported: true,
+      buffer: 5,
+      bufferDenominator: 100,
+      newWormholeFee: 0,
+      newGasOverhead: targetChainPriceUpdate.deliverGasOverhead,
+      gasPrice: targetChainPriceUpdate.updatePriceGas,
+      nativeCurrencyPrice: targetChainPriceUpdate.updatePriceNative,
+      targetChainAddress: remoteDeliveryProvider,
+      maximumTotalBudget: targetChainPriceUpdate.maximumBudget,
+    };
+    updates.push(chainConfigUpdate);
+  }
+  await deliveryProvider.updateConfig(updates, coreConfig).then(wait);
+
+  console.log("done with DeliveryProvider configuration on " + chain.chainId);
+}
+
+run().then(() => console.log("Done! " + processName));

+ 49 - 0
ethereum/ts-scripts/relayer/deliveryProvider/deployDeliveryProvider.ts

@@ -0,0 +1,49 @@
+import {
+  deployDeliveryProviderImplementation,
+  deployDeliveryProviderProxy,
+  deployDeliveryProviderSetup,
+} from "../helpers/deployments";
+import {
+  getOperatingChains,
+  getSigner,
+  init,
+  loadChains,
+  loadPrivateKey,
+  writeOutputFiles,
+} from "../helpers/env";
+
+const processName = "deployDeliveryProvider";
+init();
+const chains = getOperatingChains();
+const privateKey = loadPrivateKey();
+
+async function run() {
+  console.log(`Start ${processName}!`);
+  const output: any = {
+    deliveryProviderImplementations: [],
+    deliveryProviderSetups: [],
+    deliveryProviderProxies: [],
+  };
+
+  for (const chain of chains) {
+    console.log(`Deploying for chain ${chain.chainId}...`);
+    const deliveryProviderImplementation = await deployDeliveryProviderImplementation(
+      chain
+    );
+    const deliveryProviderSetup = await deployDeliveryProviderSetup(chain);
+    const deliveryProviderProxy = await deployDeliveryProviderProxy(
+      chain,
+      deliveryProviderSetup.address,
+      deliveryProviderImplementation.address
+    );
+
+    output.deliveryProviderImplementations.push(deliveryProviderImplementation);
+    output.deliveryProviderSetups.push(deliveryProviderSetup);
+    output.deliveryProviderProxies.push(deliveryProviderProxy);
+    console.log("");
+  }
+
+  writeOutputFiles(output, processName);
+}
+
+run().then(() => console.log("Done!"));

+ 180 - 0
ethereum/ts-scripts/relayer/deliveryProvider/readDeliveryProviderContractState.ts

@@ -0,0 +1,180 @@
+import { BigNumber, ethers } from "ethers";
+
+import {
+  init,
+  ChainInfo,
+  getDeliveryProvider,
+  getDeliveryProviderAddress,
+  getProvider,
+  writeOutputFiles,
+  getOperatingChains,
+} from "../helpers/env";
+
+const processName = "readDeliveryProviderContractState";
+init();
+const chains = getOperatingChains();
+
+async function run() {
+  console.log("Start! " + processName);
+
+  const states: any = [];
+
+  for (let i = 0; i < chains.length; i++) {
+    const state = await readState(chains[i]);
+    if (state) {
+      printState(state);
+      states.push(state);
+    }
+  }
+
+  writeOutputFiles(states, processName);
+}
+
+type DeliveryProviderContractState = {
+  chainId: number;
+  contractAddress: string;
+  rewardAddress: string;
+  deliveryOverheads: { chainId: number; deliveryOverhead: BigNumber }[];
+  supportedChains: { chainId: number; isSupported: boolean }[];
+  targetChainAddresses: { chainId: number; whAddress: string }[];
+  gasPrices: { chainId: number; gasPrice: BigNumber }[];
+  weiPrices: { chainId: number; weiPrice: BigNumber }[];
+  owner: string;
+};
+
+async function readState(
+  chain: ChainInfo
+): Promise<DeliveryProviderContractState | null> {
+  console.log(
+    "Gathering relay provider contract status for chain " + chain.chainId
+  );
+
+  try {
+    const deliveryProvider = getDeliveryProvider(chain, getProvider(chain));
+    const contractAddress = getDeliveryProviderAddress(chain);
+    console.log("Querying Relay Provider for code");
+    const provider = getProvider(chain);
+    const codeReceipt = await provider.getCode(contractAddress);
+    console.log("Code: " + codeReceipt);
+    const rewardAddress = await deliveryProvider.getRewardAddress();
+    const supportedChains: {
+      chainId: number;
+      isSupported: boolean;
+    }[] = [];
+    const targetChainAddresses: {
+      chainId: number;
+      whAddress: string;
+    }[] = [];
+    const deliveryOverheads: {
+      chainId: number;
+      deliveryOverhead: BigNumber;
+    }[] = [];
+    const gasPrices: { chainId: number; gasPrice: BigNumber }[] = [];
+    const weiPrices: { chainId: number; weiPrice: BigNumber }[] = [];
+    const owner: string = await deliveryProvider.owner();
+
+    for (const chainInfo of chains) {
+      supportedChains.push({
+        chainId: chainInfo.chainId,
+        isSupported: await deliveryProvider.isChainSupported(chainInfo.chainId),
+      });
+
+      targetChainAddresses.push({
+        chainId: chainInfo.chainId,
+        whAddress: await deliveryProvider.getTargetChainAddress(
+          chainInfo.chainId
+        ),
+      });
+
+      deliveryOverheads.push({
+        chainId: chainInfo.chainId,
+        deliveryOverhead: await deliveryProvider.quoteDeliveryOverhead(
+          chainInfo.chainId
+        ),
+      });
+      gasPrices.push({
+        chainId: chainInfo.chainId,
+        gasPrice: await deliveryProvider.quoteGasPrice(chainInfo.chainId),
+      });
+      weiPrices.push({
+        chainId: chainInfo.chainId,
+        weiPrice: await deliveryProvider.quoteAssetConversion(
+          chainInfo.chainId,
+          ethers.utils.parseEther("1")
+        ),
+      });
+    }
+
+    return {
+      chainId: chain.chainId,
+      contractAddress,
+      rewardAddress,
+      deliveryOverheads,
+      supportedChains,
+      targetChainAddresses,
+      gasPrices,
+      weiPrices,
+      owner,
+    };
+  } catch (e) {
+    console.error(e);
+    console.log("Failed to gather status for chain " + chain.chainId);
+  }
+
+  return null;
+}
+
+function printState(state: DeliveryProviderContractState) {
+  console.log("");
+  console.log("DeliveryProvider: ");
+  printFixed("Chain ID: ", state.chainId.toString());
+  printFixed("Contract Address:", state.contractAddress);
+  printFixed("Owner Address:", state.owner);
+  printFixed("Reward Address:", state.rewardAddress);
+
+  console.log("");
+
+  printFixed("Supported Chains", "");
+  state.supportedChains.forEach((x) => {
+    printFixed("  Chain: " + x.chainId, x.isSupported.toString());
+  });
+  console.log("");
+
+  printFixed("Target Chain Addresses", "");
+  state.targetChainAddresses.forEach((x) => {
+    printFixed("  Chain: " + x.chainId, x.whAddress.toString());
+  });
+  console.log("");
+
+  printFixed("Delivery Overheads", "");
+  state.deliveryOverheads.forEach((x) => {
+    printFixed("  Chain: " + x.chainId, x.deliveryOverhead.toString());
+  });
+  console.log("");
+
+  printFixed("Gas Prices", "");
+  state.gasPrices.forEach((x) => {
+    printFixed("  Chain: " + x.chainId, x.gasPrice.toString());
+  });
+  console.log("");
+
+  printFixed("USD Prices", "");
+  state.weiPrices.forEach((x) => {
+    printFixed("  Chain: " + x.chainId, x.weiPrice.toString());
+  });
+  console.log("");
+}
+
+function printFixed(title: string, content: string) {
+  const length = 80;
+  const spaces = length - title.length - content.length;
+  let str = "";
+  if (spaces > 0) {
+    for (let i = 0; i < spaces; i++) {
+      str = str + " ";
+    }
+  }
+  console.log(title + str + content);
+}
+
+run().then(() => console.log("Done! " + processName));

+ 41 - 0
ethereum/ts-scripts/relayer/deliveryProvider/upgradeDeliveryProvider.ts

@@ -0,0 +1,41 @@
+import {
+  init,
+  writeOutputFiles,
+  ChainInfo,
+  Deployment,
+  getDeliveryProvider,
+  getOperatingChains,
+} from "../helpers/env";
+import { deployDeliveryProviderImplementation } from "../helpers/deployments";
+
+const processName = "upgradeDeliveryProvider";
+init();
+const chains = getOperatingChains();
+
+async function run() {
+  console.log("Start!");
+  const output: any = {
+    deliveryProviderImplementations: [],
+  };
+
+  for (let i = 0; i < chains.length; i++) {
+    const deliveryProviderImplementation = await deployDeliveryProviderImplementation(
+      chains[i]
+    );
+    await upgradeDeliveryProvider(chains[i], deliveryProviderImplementation);
+
+    output.deliveryProviderImplementations.push(deliveryProviderImplementation);
+  }
+
+  writeOutputFiles(output, processName);
+}
+
+async function upgradeDeliveryProvider(chain: ChainInfo, newImpl: Deployment) {
+  console.log("About to upgrade relay provider for chain " + chain.chainId);
+  const provider = getDeliveryProvider(chain);
+  const tx = await provider.upgrade(chain.chainId, newImpl.address);
+  await tx.wait();
+  console.log("Successfully upgraded relay provider " + chain.chainId);
+}
+
+run().then(() => console.log("Done!"));

+ 45 - 0
ethereum/ts-scripts/relayer/eraseTypes.ts

@@ -0,0 +1,45 @@
+import * as fs from "fs/promises";
+
+async function main() {
+  const fnames = process.argv.slice(2);
+
+  await Promise.all(fnames.map(async (fname) => {
+    console.log(`Erasing types from ${fname}...`);
+    const iface = await fs.readFile(fname);
+    const erased = eraseTypes(iface.toString());
+    await fs.writeFile(fname.replace("Typed.sol", ".sol"), erased);
+  }))
+  console.log("Done.")
+}
+
+function eraseTypes(file: string) {
+  const typeMap: Record<string, string> = {
+    "Wei ": "uint256 ",
+    LocalNative: "uint256",
+    TargetNative: "uint256",
+    "Gas ": "uint256 ",
+    GasPrice: "uint256",
+    WeiPrice: "uint256",
+    Dollar: "uint256",
+    '\nimport "./TypedUnits.sol";\n': "", // delete this import
+    "^0.8.19;": "^0.8.0;",
+  };
+
+  const escapeRegExp = (str: string) =>
+    str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+  const regex = new RegExp(
+    Object.keys(typeMap)
+      .map(escapeRegExp)
+      .join("|"),
+    "g"
+  );
+
+  const replacedText = file.replace(regex, (match) => typeMap[match]);
+
+  return replacedText;
+}
+
+main().catch((e) => {
+  console.error(e);
+  process.exit(1);
+});

+ 181 - 0
ethereum/ts-scripts/relayer/helpers/deployments.ts

@@ -0,0 +1,181 @@
+import { DeliveryProviderProxy__factory } from "../../../ethers-contracts";
+import { DeliveryProviderSetup__factory } from "../../../ethers-contracts";
+import { DeliveryProviderImplementation__factory } from "../../../ethers-contracts";
+import { MockRelayerIntegration__factory } from "../../../ethers-contracts";
+import { WormholeRelayer__factory } from "../../../ethers-contracts";
+
+import {
+  ChainInfo,
+  Deployment,
+  getSigner,
+  getWormholeRelayerAddress,
+  getCreate2Factory,
+} from "./env";
+import { ethers } from "ethers";
+import { Create2Factory__factory } from "../../../ethers-contracts";
+import { wait } from "./utils";
+
+export const setupContractSalt = Buffer.from("0xSetup");
+export const proxyContractSalt = Buffer.from("0xGenericRelayer");
+
+export async function deployDeliveryProviderImplementation(
+  chain: ChainInfo
+): Promise<Deployment> {
+  console.log("deployDeliveryProviderImplementation " + chain.chainId);
+  const signer = getSigner(chain);
+
+  const contractInterface = DeliveryProviderImplementation__factory.createInterface();
+  const bytecode = DeliveryProviderImplementation__factory.bytecode;
+  //@ts-ignore
+  const factory = new ethers.ContractFactory(
+    contractInterface,
+    bytecode,
+    signer
+  );
+  const contract = await factory.deploy();
+  return await contract.deployed().then((result) => {
+    console.log("Successfully deployed contract at " + result.address);
+    return { address: result.address, chainId: chain.chainId };
+  });
+}
+
+export async function deployDeliveryProviderSetup(
+  chain: ChainInfo
+): Promise<Deployment> {
+  console.log("deployDeliveryProviderSetup " + chain.chainId);
+  const signer = getSigner(chain);
+  const contractInterface = DeliveryProviderSetup__factory.createInterface();
+  const bytecode = DeliveryProviderSetup__factory.bytecode;
+  //@ts-ignore
+  const factory = new ethers.ContractFactory(
+    contractInterface,
+    bytecode,
+    signer
+  );
+  const contract = await factory.deploy();
+  return await contract.deployed().then((result) => {
+    console.log("Successfully deployed contract at " + result.address);
+    return { address: result.address, chainId: chain.chainId };
+  });
+}
+export async function deployDeliveryProviderProxy(
+  chain: ChainInfo,
+  deliveryProviderSetupAddress: string,
+  deliveryProviderImplementationAddress: string
+): Promise<Deployment> {
+  console.log("deployDeliveryProviderProxy " + chain.chainId);
+
+  const signer = getSigner(chain);
+  const contractInterface = DeliveryProviderProxy__factory.createInterface();
+  const bytecode = DeliveryProviderProxy__factory.bytecode;
+  //@ts-ignore
+  const factory = new ethers.ContractFactory(
+    contractInterface,
+    bytecode,
+    signer
+  );
+
+  let ABI = ["function setup(address,uint16)"];
+  let iface = new ethers.utils.Interface(ABI);
+  let encodedData = iface.encodeFunctionData("setup", [
+    deliveryProviderImplementationAddress,
+    chain.chainId,
+  ]);
+
+  const contract = await factory.deploy(
+    deliveryProviderSetupAddress,
+    encodedData
+  );
+  return await contract.deployed().then((result) => {
+    console.log("Successfully deployed contract at " + result.address);
+    return { address: result.address, chainId: chain.chainId };
+  });
+}
+
+export async function deployMockIntegration(
+  chain: ChainInfo
+): Promise<Deployment> {
+  console.log("deployMockIntegration " + chain.chainId);
+
+  let signer = getSigner(chain);
+  const contractInterface = MockRelayerIntegration__factory.createInterface();
+  const bytecode = MockRelayerIntegration__factory.bytecode;
+  const factory = new ethers.ContractFactory(
+    contractInterface,
+    bytecode,
+    signer
+  );
+  const contract = await factory.deploy(
+    chain.wormholeAddress,
+    await getWormholeRelayerAddress(chain)
+  );
+  return await contract.deployed().then((result) => {
+    console.log("Successfully deployed contract at " + result.address);
+    return { address: result.address, chainId: chain.chainId };
+  });
+}
+
+export async function deployCreate2Factory(
+  chain: ChainInfo
+): Promise<Deployment> {
+  console.log("deployCreate2Factory " + chain.chainId);
+
+  const result = await new Create2Factory__factory(getSigner(chain))
+    .deploy()
+    .then(deployed);
+  console.log(`Successfully deployed contract at ${result.address}`);
+  return { address: result.address, chainId: chain.chainId };
+}
+
+export async function deployWormholeRelayerImplementation(
+  chain: ChainInfo
+): Promise<Deployment> {
+  console.log("deployWormholeRelayerImplementation " + chain.chainId);
+
+  const result = await new WormholeRelayer__factory(getSigner(chain))
+    .deploy(chain.wormholeAddress)
+    .then(deployed);
+
+  console.log("Successfully deployed contract at " + result.address);
+  return { address: result.address, chainId: chain.chainId };
+}
+
+export async function deployWormholeRelayerProxy(
+  chain: ChainInfo,
+  coreRelayerImplementationAddress: string,
+  defaultDeliveryProvider: string
+): Promise<Deployment> {
+  console.log("deployWormholeRelayerProxy " + chain.chainId);
+
+  const create2Factory = getCreate2Factory(chain);
+
+  const initData = WormholeRelayer__factory.createInterface().encodeFunctionData(
+    "initialize",
+    [ethers.utils.getAddress(defaultDeliveryProvider)]
+  );
+  const rx = await create2Factory
+    .create2Proxy(proxyContractSalt, coreRelayerImplementationAddress, initData)
+    .then(wait);
+
+  let proxyAddress: string;
+  // pull proxyAddress from create2Factory logs
+  for (const log of rx.logs) {
+    try {
+      if (log.address == create2Factory.address) {
+        proxyAddress = create2Factory.interface.parseLog(log).args.addr;
+      }
+    } catch (e) {}
+  }
+  const computedAddr = await create2Factory.computeProxyAddress(
+    getSigner(chain).address,
+    proxyContractSalt
+  );
+  if (proxyAddress! !== computedAddr) {
+    console.error("Computed address does not match desired");
+  }
+
+  console.log("Successfully deployed contract at " + computedAddr);
+  return { address: computedAddr, chainId: chain.chainId };
+}
+
+const deployed = (x: ethers.Contract) => x.deployed();

+ 442 - 0
ethereum/ts-scripts/relayer/helpers/env.ts

@@ -0,0 +1,442 @@
+import { ChainId } from "@certusone/wormhole-sdk";
+import { ethers } from "ethers";
+import fs from "fs";
+
+import { WormholeRelayer } from "../../../ethers-contracts";
+import { DeliveryProvider } from "../../../ethers-contracts";
+import { MockRelayerIntegration } from "../../../ethers-contracts";
+
+import { DeliveryProvider__factory } from "../../../ethers-contracts";
+import { WormholeRelayer__factory } from "../../../ethers-contracts";
+import { MockRelayerIntegration__factory } from "../../../ethers-contracts";
+import {
+  Create2Factory,
+  Create2Factory__factory,
+} from "../../../ethers-contracts";
+import { proxyContractSalt, setupContractSalt } from "./deployments";
+
+export type ChainInfo = {
+  evmNetworkId: number;
+  chainId: ChainId;
+  rpc: string;
+  wormholeAddress: string;
+};
+
+export type Deployment = {
+  chainId: ChainId;
+  address: string;
+};
+
+const DEFAULT_ENV = "testnet";
+
+export let env = "";
+let lastRunOverride: boolean | undefined;
+
+export function init(overrides: { lastRunOverride?: boolean } = {}): string {
+  env = get_env_var("ENV");
+  if (!env) {
+    console.log(
+      "No environment was specified, using default environment files"
+    );
+    env = DEFAULT_ENV;
+  }
+  lastRunOverride = overrides?.lastRunOverride;
+
+  require("dotenv").config({
+    path: `./ts-scripts/relayer/.env${env != DEFAULT_ENV ? "." + env : ""}`,
+  });
+  return env;
+}
+
+function get_env_var(env: string): string {
+  const v = process.env[env];
+  return v || "";
+}
+
+function getContainer(): string | null {
+  const container = get_env_var("CONTAINER");
+  if (!container) {
+    return null;
+  }
+
+  return container;
+}
+
+export function loadScriptConfig(processName: string): any {
+  const configFile = fs.readFileSync(
+    `./ts-scripts/relayer/config/${env}/scriptConfigs/${processName}.json`
+  );
+  const config = JSON.parse(configFile.toString());
+  if (!config) {
+    throw Error("Failed to pull config file!");
+  }
+  return config;
+}
+
+export function getOperatingChains(): ChainInfo[] {
+  const allChains = loadChains();
+  const container = getContainer();
+  let operatingChains = null;
+
+  if (container == "evm1") {
+    operatingChains = [2];
+  }
+  if (container == "evm2") {
+    operatingChains = [4];
+  }
+
+  const chainFile = fs.readFileSync(
+    `./ts-scripts/relayer/config/${env}/chains.json`
+  );
+  const chains = JSON.parse(chainFile.toString());
+  if (chains.operatingChains) {
+    operatingChains = chains.operatingChains;
+  }
+  if (!operatingChains) {
+    return allChains;
+  }
+
+  const output: ChainInfo[] = [];
+  operatingChains.forEach((x: number) => {
+    const item = allChains.find((y) => {
+      return x == y.chainId;
+    });
+    if (item) {
+      output.push(item);
+    }
+  });
+
+  return output;
+}
+
+export function loadChains(): ChainInfo[] {
+  const chainFile = fs.readFileSync(
+    `./ts-scripts/relayer/config/${env}/chains.json`
+  );
+  const chains = JSON.parse(chainFile.toString());
+  if (!chains.chains) {
+    throw Error("Failed to pull chain config file!");
+  }
+  return chains.chains;
+}
+
+export function getChain(chain: ChainId): ChainInfo {
+  const chains = loadChains();
+  const output = chains.find((x) => x.chainId == chain);
+  if (!output) {
+    throw Error("bad chain ID");
+  }
+
+  return output;
+}
+
+export function loadPrivateKey(): string {
+  const privateKey = get_env_var("WALLET_KEY");
+  if (!privateKey) {
+    throw Error("Failed to find private key for this process!");
+  }
+  return privateKey;
+}
+
+export function loadGuardianSetIndex(): number {
+  const chainFile = fs.readFileSync(
+    `./ts-scripts/relayer/config/${env}/chains.json`
+  );
+  const chains = JSON.parse(chainFile.toString());
+  if (chains.guardianSetIndex == undefined) {
+    throw Error("Failed to pull guardian set index from the chains file!");
+  }
+  return chains.guardianSetIndex;
+}
+
+export function loadDeliveryProviders(): Deployment[] {
+  const contractsFile = fs.readFileSync(
+    `./ts-scripts/relayer/config/${env}/contracts.json`
+  );
+  if (!contractsFile) {
+    throw Error("Failed to find contracts file for this process!");
+  }
+  const contracts = JSON.parse(contractsFile.toString());
+  if (contracts.useLastRun || lastRunOverride) {
+    const lastRunFile = fs.readFileSync(
+      `./ts-scripts/relayer/output/${env}/deployDeliveryProvider/lastrun.json`
+    );
+    if (!lastRunFile) {
+      throw Error(
+        "Failed to find last run file for the deployDeliveryProvider process!"
+      );
+    }
+    const lastRun = JSON.parse(lastRunFile.toString());
+    return lastRun.deliveryProviderProxies;
+  } else if (contracts.useLastRun == false) {
+    return contracts.deliveryProviders;
+  } else {
+    throw Error("useLastRun was an invalid value from the contracts config");
+  }
+}
+
+export function loadWormholeRelayers(dev: boolean): Deployment[] {
+  const contractsFile = fs.readFileSync(
+    `./ts-scripts/relayer/config/${env}/contracts.json`
+  );
+  if (!contractsFile) {
+    throw Error("Failed to find contracts file for this process!");
+  }
+  const contracts = JSON.parse(contractsFile.toString());
+  if (contracts.useLastRun || lastRunOverride) {
+    const lastRunFile = fs.readFileSync(
+      `./ts-scripts/relayer/output/${env}/deployWormholeRelayer/lastrun.json`
+    );
+    if (!lastRunFile) {
+      throw Error("Failed to find last run file for the Core Relayer process!");
+    }
+    const lastRun = JSON.parse(lastRunFile.toString());
+    return lastRun.wormholeRelayerProxies;
+  } else {
+    return dev ? contracts.wormholeRelayersDev : contracts.wormholeRelayers;
+  }
+}
+
+export function loadMockIntegrations(): Deployment[] {
+  const contractsFile = fs.readFileSync(
+    `./ts-scripts/relayer/config/${env}/contracts.json`
+  );
+  if (!contractsFile) {
+    throw Error("Failed to find contracts file for this process!");
+  }
+  const contracts = JSON.parse(contractsFile.toString());
+  if (contracts.useLastRun || lastRunOverride) {
+    const lastRunFile = fs.readFileSync(
+      `./ts-scripts/relayer/output/${env}/deployMockIntegration/lastrun.json`
+    );
+    if (!lastRunFile) {
+      throw Error(
+        "Failed to find last run file for the deploy mock integration process!"
+      );
+    }
+    const lastRun = JSON.parse(lastRunFile.toString());
+    return lastRun.mockIntegrations;
+  } else {
+    return contracts.mockIntegrations;
+  }
+}
+
+export function loadCreate2Factories(): Deployment[] {
+  const contractsFile = fs.readFileSync(
+    `./ts-scripts/relayer/config/${env}/contracts.json`
+  );
+  if (!contractsFile) {
+    throw Error("Failed to find contracts file for this process!");
+  }
+  const contracts = JSON.parse(contractsFile.toString());
+  if (contracts.useLastRun || lastRunOverride) {
+    const lastRunFile = fs.readFileSync(
+      `./ts-scripts/relayer/output/${env}/deployCreate2Factory/lastrun.json`
+    );
+    if (!lastRunFile) {
+      throw Error(
+        "Failed to find last run file for the deployCreate2Factory process!"
+      );
+    }
+    const lastRun = JSON.parse(lastRunFile.toString());
+    return lastRun.create2Factories;
+  } else {
+    return contracts.create2Factories;
+  }
+}
+
+//TODO load these keys more intelligently,
+//potentially from devnet-consts.
+//Also, make sure the signers are correctly ordered by index,
+//As the index gets encoded into the signature.
+export function loadGuardianKeys(): string[] {
+  const output = [];
+  const NUM_GUARDIANS = get_env_var("NUM_GUARDIANS");
+  const guardianKey = get_env_var("GUARDIAN_KEY");
+  const guardianKey2 = get_env_var("GUARDIAN_KEY2");
+
+  let numGuardians: number = 0;
+  console.log("NUM_GUARDIANS variable : " + NUM_GUARDIANS);
+
+  if (!NUM_GUARDIANS) {
+    numGuardians = 1;
+  } else {
+    numGuardians = parseInt(NUM_GUARDIANS);
+  }
+
+  if (!guardianKey) {
+    throw Error("Failed to find guardian key for this process!");
+  }
+  output.push(guardianKey);
+
+  if (numGuardians >= 2) {
+    if (!guardianKey2) {
+      throw Error("Failed to find guardian key 2 for this process!");
+    }
+    output.push(guardianKey2);
+  }
+
+  return output;
+}
+
+export function writeOutputFiles(output: any, processName: string) {
+  fs.mkdirSync(`./ts-scripts/relayer/output/${env}/${processName}`, {
+    recursive: true,
+  });
+  fs.writeFileSync(
+    `./ts-scripts/relayer/output/${env}/${processName}/lastrun.json`,
+    JSON.stringify(output),
+    { flag: "w" }
+  );
+  fs.writeFileSync(
+    `./ts-scripts/relayer/output/${env}/${processName}/${Date.now()}.json`,
+    JSON.stringify(output),
+    { flag: "w" }
+  );
+}
+
+export function getSigner(chain: ChainInfo): ethers.Wallet {
+  let provider = getProvider(chain);
+  let signer = new ethers.Wallet(loadPrivateKey(), provider);
+  return signer;
+}
+
+export function getProvider(
+  chain: ChainInfo
+): ethers.providers.StaticJsonRpcProvider {
+  let provider = new ethers.providers.StaticJsonRpcProvider(
+    loadChains().find((x: any) => x.chainId == chain.chainId)?.rpc || ""
+  );
+
+  return provider;
+}
+
+export function getDeliveryProviderAddress(chain: ChainInfo): string {
+  const thisChainsProvider = loadDeliveryProviders().find(
+    (x: any) => x.chainId == chain.chainId
+  )?.address;
+  if (!thisChainsProvider) {
+    throw new Error(
+      "Failed to find a DeliveryProvider contract address on chain " +
+        chain.chainId
+    );
+  }
+  return thisChainsProvider;
+}
+
+export function loadGuardianRpc(): string {
+  const chainFile = fs.readFileSync(
+    `./ts-scripts/relayer/config/${env}/chains.json`
+  );
+  if (!chainFile) {
+    throw Error("Failed to find contracts file for this process!");
+  }
+  const chain = JSON.parse(chainFile.toString());
+  return chain.guardianRPC;
+}
+
+export function getDeliveryProvider(
+  chain: ChainInfo,
+  provider?: ethers.providers.StaticJsonRpcProvider
+): DeliveryProvider {
+  const thisChainsProvider = getDeliveryProviderAddress(chain);
+  const contract = DeliveryProvider__factory.connect(
+    thisChainsProvider,
+    provider || getSigner(chain)
+  );
+  return contract;
+}
+
+const wormholeRelayerAddressesCache: Partial<Record<ChainId, string>> = {};
+export async function getWormholeRelayerAddress(
+  chain: ChainInfo,
+  forceCalculate?: boolean
+): Promise<string> {
+  // See if we are in dev mode (i.e. forge contracts compiled without via-ir)
+  const dev = get_env_var("DEV") == "True" ? true : false;
+
+  const contractsFile = fs.readFileSync(
+    `./ts-scripts/relayer/config/${env}/contracts.json`
+  );
+  if (!contractsFile) {
+    throw Error("Failed to find contracts file for this process!");
+  }
+  const contracts = JSON.parse(contractsFile.toString());
+  //If useLastRun is false, then we want to bypass the calculations and just use what the contracts file says.
+  if (!contracts.useLastRun && !lastRunOverride && !forceCalculate) {
+    const thisChainsRelayer = loadWormholeRelayers(dev).find(
+      (x: any) => x.chainId == chain.chainId
+    )?.address;
+    if (thisChainsRelayer) {
+      return thisChainsRelayer;
+    } else {
+      throw Error(
+        "Failed to find a WormholeRelayer contract address on chain " +
+          chain.chainId
+      );
+    }
+  }
+
+  if (!wormholeRelayerAddressesCache[chain.chainId]) {
+    const create2Factory = getCreate2Factory(chain);
+    const signer = getSigner(chain).address;
+
+    wormholeRelayerAddressesCache[
+      chain.chainId
+    ] = await create2Factory.computeProxyAddress(signer, proxyContractSalt);
+  }
+
+  return wormholeRelayerAddressesCache[chain.chainId]!;
+}
+
+export async function getWormholeRelayer(
+  chain: ChainInfo,
+  provider?: ethers.providers.StaticJsonRpcProvider
+): Promise<WormholeRelayer> {
+  const thisChainsRelayer = await getWormholeRelayerAddress(chain);
+  return WormholeRelayer__factory.connect(
+    thisChainsRelayer,
+    provider || getSigner(chain)
+  );
+}
+
+export function getMockIntegrationAddress(chain: ChainInfo): string {
+  const thisMock = loadMockIntegrations().find(
+    (x: any) => x.chainId == chain.chainId
+  )?.address;
+  if (!thisMock) {
+    throw new Error(
+      "Failed to find a mock integration contract address on chain " +
+        chain.chainId
+    );
+  }
+  return thisMock;
+}
+
+export function getMockIntegration(chain: ChainInfo): MockRelayerIntegration {
+  const thisIntegration = getMockIntegrationAddress(chain);
+  const contract = MockRelayerIntegration__factory.connect(
+    thisIntegration,
+    getSigner(chain)
+  );
+  return contract;
+}
+
+export function getCreate2FactoryAddress(chain: ChainInfo): string {
+  const address = loadCreate2Factories().find(
+    (x: any) => x.chainId == chain.chainId
+  )?.address;
+  if (!address) {
+    throw new Error(
+      "Failed to find a create2Factory contract address on chain " +
+        chain.chainId
+    );
+  }
+  return address;
+}
+
+export const getCreate2Factory = (chain: ChainInfo): Create2Factory =>
+  Create2Factory__factory.connect(
+    getCreate2FactoryAddress(chain),
+    getSigner(chain)
+  );

+ 5 - 0
ethereum/ts-scripts/relayer/helpers/utils.ts

@@ -0,0 +1,5 @@
+import { ContractReceipt, ContractTransaction } from "ethers";
+
+export function wait(tx: ContractTransaction): Promise<ContractReceipt> {
+  return tx.wait();
+}

+ 155 - 0
ethereum/ts-scripts/relayer/helpers/vaa.ts

@@ -0,0 +1,155 @@
+import { ethers } from "ethers";
+import { tryNativeToHexString } from "@certusone/wormhole-sdk";
+import {
+  ChainInfo,
+  getWormholeRelayerAddress,
+  getDeliveryProviderAddress,
+  loadGuardianKeys,
+  loadGuardianSetIndex,
+} from "./env";
+const elliptic = require("elliptic");
+
+const governanceChainId = 1;
+const governanceContract =
+  "0x0000000000000000000000000000000000000000000000000000000000000004";
+//don't use the variable module in global scope in node
+const wormholeRelayerModule =
+  "0x0000000000000000000000000000000000576f726d686f6c6552656c61796572";
+
+export function createWormholeRelayerUpgradeVAA(
+  chain: ChainInfo,
+  newAddress: string
+) {
+  /*
+      bytes32 module;
+        uint8 action;
+        uint16 chain;
+        bytes32 newContract; //listed as address in the struct, but is actually bytes32 inside the VAA
+      */
+
+  const payload = ethers.utils.solidityPack(
+    ["bytes32", "uint8", "uint16", "bytes32"],
+    [
+      wormholeRelayerModule,
+      1,
+      chain.chainId,
+      "0x" + tryNativeToHexString(newAddress, "ethereum"),
+    ]
+  );
+
+  return encodeAndSignGovernancePayload(payload);
+}
+
+export function createDefaultDeliveryProviderVAA(chain: ChainInfo) {
+  /*
+    bytes32 module;
+    uint8 action;
+    uint16 chain;
+    bytes32 newProvider; //Struct in the contract is an address, wire type is a wh format 32
+    */
+
+  const payload = ethers.utils.solidityPack(
+    ["bytes32", "uint8", "uint16", "bytes32"],
+    [
+      wormholeRelayerModule,
+      3,
+      chain.chainId,
+      "0x" +
+        tryNativeToHexString(getDeliveryProviderAddress(chain), "ethereum"),
+    ]
+  );
+
+  return encodeAndSignGovernancePayload(payload);
+}
+
+export async function createRegisterChainVAA(
+  chain: ChainInfo
+): Promise<string> {
+  const coreRelayerAddress = await getWormholeRelayerAddress(chain);
+  console.log(`Registering ${coreRelayerAddress} on chain ${chain.chainId}`);
+
+  // bytes32 module;
+  // uint8 action;
+  // uint16 chain; //0
+  // uint16 emitterChain;
+  // bytes32 emitterAddress;
+
+  const payload = ethers.utils.solidityPack(
+    ["bytes32", "uint8", "uint16", "uint16", "bytes32"],
+    [
+      wormholeRelayerModule,
+      1,
+      0,
+      chain.chainId,
+      "0x" + tryNativeToHexString(coreRelayerAddress, "ethereum"),
+    ]
+  );
+
+  return encodeAndSignGovernancePayload(payload);
+}
+
+export function encodeAndSignGovernancePayload(payload: string): string {
+  const timestamp = Math.floor(+new Date() / 1000);
+  const nonce = 1;
+  const sequence = 1;
+  const consistencyLevel = 1;
+
+  const encodedVAABody = ethers.utils.solidityPack(
+    ["uint32", "uint32", "uint16", "bytes32", "uint64", "uint8", "bytes"],
+    [
+      timestamp,
+      nonce,
+      governanceChainId,
+      governanceContract,
+      sequence,
+      consistencyLevel,
+      payload,
+    ]
+  );
+
+  const hash = doubleKeccak256(encodedVAABody);
+
+  const signers = loadGuardianKeys();
+  let signatures = "";
+
+  for (const i in signers) {
+    // sign the hash
+    const ec = new elliptic.ec("secp256k1");
+    const key = ec.keyFromPrivate(signers[i]);
+    const signature = key.sign(hash.substring(2), { canonical: true });
+
+    // pack the signatures
+    const packSig = [
+      ethers.utils.solidityPack(["uint8"], [i]).substring(2),
+      zeroPadBytes(signature.r.toString(16), 32),
+      zeroPadBytes(signature.s.toString(16), 32),
+      ethers.utils
+        .solidityPack(["uint8"], [signature.recoveryParam])
+        .substring(2),
+    ];
+    signatures += packSig.join("");
+  }
+
+  const vm = [
+    ethers.utils.solidityPack(["uint8"], [1]).substring(2),
+    ethers.utils
+      .solidityPack(["uint32"], [loadGuardianSetIndex()])
+      .substring(2), // guardianSetIndex
+    ethers.utils.solidityPack(["uint8"], [signers.length]).substring(2), // number of signers
+    signatures,
+    encodedVAABody.substring(2),
+  ].join("");
+
+  return "0x" + vm;
+}
+
+export function doubleKeccak256(body: ethers.BytesLike) {
+  return ethers.utils.keccak256(ethers.utils.keccak256(body));
+}
+
+export function zeroPadBytes(value: string, length: number): string {
+  while (value.length < 2 * length) {
+    value = "0" + value;
+  }
+  return value;
+}

+ 57 - 0
ethereum/ts-scripts/relayer/mockIntegration/deployMockIntegration.ts

@@ -0,0 +1,57 @@
+import {
+  init,
+  loadChains,
+  writeOutputFiles,
+  getMockIntegration,
+  Deployment,
+  getOperatingChains,
+  getMockIntegrationAddress,
+} from "../helpers/env";
+import { deployMockIntegration } from "../helpers/deployments";
+import { BigNumber, BigNumberish, BytesLike } from "ethers";
+import {
+  tryNativeToHexString,
+  tryNativeToUint8Array,
+} from "@certusone/wormhole-sdk";
+import { MockRelayerIntegration__factory } from "../../../ethers-contracts";
+import { wait } from "../helpers/utils";
+
+const processName = "deployMockIntegration";
+init();
+const chains = loadChains();
+const operatingChains = getOperatingChains();
+
+async function run() {
+  console.log("Start!");
+  const output = {
+    mockIntegrations: [] as Deployment[],
+  };
+
+  for (let i = 0; i < operatingChains.length; i++) {
+    const mockIntegration = await deployMockIntegration(operatingChains[i]);
+    output.mockIntegrations.push(mockIntegration);
+  }
+
+  writeOutputFiles(output, processName);
+
+  for (let i = 0; i < operatingChains.length; i++) {
+    console.log(`Registering emitters for chainId ${operatingChains[i].chainId}`);
+    // note: must use useLastRun = true
+    const mockIntegration = getMockIntegration(operatingChains[i]);
+
+    const arg: {
+      chainId: BigNumberish;
+      addr: BytesLike;
+    }[] = chains.map((c, j) => ({
+      chainId: c.chainId,
+      addr:
+        "0x" + tryNativeToHexString(getMockIntegrationAddress(c), "ethereum"),
+    }));
+
+    await mockIntegration
+      .registerEmitters(arg, { gasLimit: 500000 })
+      .then(wait);
+  }
+}
+
+run().then(() => console.log("Done!"));

+ 76 - 0
ethereum/ts-scripts/relayer/mockIntegration/messageTest.ts

@@ -0,0 +1,76 @@
+import {
+  ChainInfo,
+  getWormholeRelayer,
+  getOperatingChains,
+  init,
+  loadChains,
+} from "../helpers/env";
+import { sendMessage } from "./messageUtils";
+
+init();
+const chains = getOperatingChains();
+
+async function run() {
+  console.log(process.argv);
+  const fetchSignedVaa = !!process.argv.find(
+    (arg) => arg === "--fetchSignedVaa"
+  );
+  const queryMessageOnTarget = !process.argv.find(
+    (arg) => arg === "--noQueryMessageOnTarget"
+  );
+  console.log(chains);
+  if (process.argv[2] === "--from" && process.argv[4] === "--to") {
+    await sendMessage(
+      getChainById(process.argv[3]),
+      getChainById(process.argv[5]),
+      fetchSignedVaa,
+      queryMessageOnTarget
+    );
+  } else if (process.argv[4] === "--from" && process.argv[2] === "--to") {
+    await sendMessage(
+      getChainById(process.argv[5]),
+      getChainById(process.argv[3]),
+      fetchSignedVaa,
+      queryMessageOnTarget
+    );
+  } else if (process.argv[2] === "--per-chain") {
+    for (let i = 0; i < chains.length; ++i) {
+      await sendMessage(
+        chains[i],
+        chains[i === 0 ? chains.length - 1 : 0],
+        fetchSignedVaa,
+        queryMessageOnTarget
+      );
+    }
+  } else if (process.argv[2] === "--matrix") {
+    for (let i = 0; i < chains.length; ++i) {
+      for (let j = 0; i < chains.length; ++i) {
+        await sendMessage(
+          chains[i],
+          chains[j],
+          fetchSignedVaa,
+          queryMessageOnTarget
+        );
+      }
+    }
+  } else {
+    await sendMessage(
+      chains[0],
+      chains[1],
+      fetchSignedVaa,
+      queryMessageOnTarget
+    );
+  }
+}
+
+function getChainById(id: number | string): ChainInfo {
+  id = Number(id);
+  const chain = chains.find((c) => c.chainId === id);
+  if (!chain) {
+    throw new Error("chainId not found, " + id);
+  }
+  return chain;
+}
+
+console.log("Start!");
+run().then(() => console.log("Done!"));

+ 150 - 0
ethereum/ts-scripts/relayer/mockIntegration/messageUtils.ts

@@ -0,0 +1,150 @@
+import * as wh from "@certusone/wormhole-sdk";
+//TODO address sdk version mismatch
+//import { Implementation__factory, LogMessagePublishedEvent } from "@certusone/wormhole-sdk"
+import {
+  ChainInfo,
+  getWormholeRelayer,
+  getMockIntegration,
+  getMockIntegrationAddress,
+} from "../helpers/env";
+import { ethers } from "ethers";
+
+export async function sendMessage(
+  sourceChain: ChainInfo,
+  targetChain: ChainInfo,
+  fetchSignedVaa: boolean = false,
+  queryMessageOnTargetFlag: boolean = true
+): Promise<boolean | undefined> {
+  console.log(
+    `Sending message from chain ${sourceChain.chainId} to ${targetChain.chainId}...`
+  );
+
+  const sourceRelayer = await getWormholeRelayer(sourceChain);
+  const sourceProvider = await sourceRelayer.getDefaultDeliveryProvider();
+
+  const relayQuote = await (
+    await sourceRelayer.quoteGas(targetChain.chainId, 2000000, sourceProvider)
+  ).add(10000000000);
+  console.log("relay quote: " + relayQuote);
+
+  const mockIntegration = getMockIntegration(sourceChain);
+  const targetAddress = getMockIntegrationAddress(targetChain);
+
+  const message = await mockIntegration.getMessage();
+  console.log("got message from integration " + message);
+
+  const sentMessage = "ID: " + String(Math.ceil(Math.random() * 10000));
+  console.log(`Sending message: ${sentMessage}`);
+  const tx = await mockIntegration.sendMessage(
+    Buffer.from(sentMessage),
+    targetChain.chainId,
+    targetAddress,
+    {
+      gasLimit: 1000000,
+      value: relayQuote,
+    }
+  );
+  const rx = await tx.wait();
+  const sequences = wh.parseSequencesFromLogEth(
+    rx,
+    sourceChain.wormholeAddress
+  );
+  console.log("Tx hash: ", rx.transactionHash);
+  console.log(`Sequences: ${sequences}`);
+  if (fetchSignedVaa) {
+    for (let i = 0; i < 120; i++) {
+      try {
+        const vaa1 = await fetchVaaFromLog(rx.logs[0], sourceChain.chainId);
+        console.log(vaa1);
+        const vaa2 = await fetchVaaFromLog(rx.logs[1], sourceChain.chainId);
+        console.log(vaa2);
+        break;
+      } catch (e) {
+        console.error(`${i} seconds`);
+        if (i === 0) {
+          console.error(e);
+        }
+      }
+      await new Promise((resolve) => setTimeout(resolve, 1_000));
+    }
+  }
+
+  if (queryMessageOnTargetFlag) {
+    return await queryMessageOnTarget(sentMessage, targetChain);
+  }
+  console.log("");
+}
+
+async function queryMessageOnTarget(
+  sentMessage: string,
+  targetChain: ChainInfo
+): Promise<boolean> {
+  let messageHistory: string[][] = [];
+  const targetIntegration = getMockIntegration(targetChain);
+
+  let notFound = true;
+  for (let i = 0; i < 20 && notFound; i++) {
+    await new Promise<void>((resolve) => setTimeout(() => resolve(), 2000));
+    const messageHistoryResp = await targetIntegration.getMessageHistory();
+    messageHistory = messageHistoryResp.map((messages) =>
+      messages.map((message) => ethers.utils.toUtf8String(message))
+    );
+    notFound = !messageHistory
+      .slice(messageHistory.length - 20)
+      .find((msgs) => msgs.find((m) => m === sentMessage));
+    process.stdout.write("..");
+  }
+  console.log("");
+  if (notFound) {
+    console.log(`ERROR: Did not receive message!`);
+    return false;
+  }
+
+  console.log(
+    `Received message: ${messageHistory[messageHistory.length - 1][0]}`
+  );
+  console.log(`Received messageHistory: ${messageHistory.join(", ")}`);
+  return true;
+}
+
+export async function encodeEmitterAddress(
+  myChainId: wh.ChainId,
+  emitterAddressStr: string
+): Promise<string> {
+  if (myChainId === wh.CHAIN_ID_SOLANA || myChainId === wh.CHAIN_ID_PYTHNET) {
+    return wh.getEmitterAddressSolana(emitterAddressStr);
+  }
+  if (wh.isTerraChain(myChainId)) {
+    return wh.getEmitterAddressTerra(emitterAddressStr);
+  }
+  if (wh.isEVMChain(myChainId)) {
+    return wh.getEmitterAddressEth(emitterAddressStr);
+  }
+  throw new Error(`Unrecognized wormhole chainId ${myChainId}`);
+}
+
+function fetchVaaFromLog(
+  bridgeLog: any,
+  chainId: wh.ChainId
+): Promise<wh.SignedVaa> {
+  throw Error("fetchVAA unimplemented");
+  // const iface = Implementation__factory.createInterface();
+  // const log = (iface.parseLog(
+  //   bridgeLog
+  // ) as unknown) as LogMessagePublishedEvent;
+  // const sequence = log.args.sequence.toString();
+  // const emitter = wh.tryNativeToHexString(log.args.sender, "ethereum");
+  // return wh
+  //   .getSignedVAA(
+  //     "https://wormhole-v2-testnet-api.certus.one",
+  //     chainId,
+  //     emitter,
+  //     sequence,
+  //     { transport: grpcWebNodeHttpTransport.NodeHttpTransport() }
+  //   )
+  //   .then((r) => r.vaaBytes);
+}
+
+export async function sleep(ms: number) {
+  return new Promise((r) => setTimeout(r, ms));
+}

+ 103 - 0
ethereum/ts-scripts/relayer/mockIntegration/readMockIntegrationState.ts

@@ -0,0 +1,103 @@
+import {
+  init,
+  loadChains,
+  ChainInfo,
+  writeOutputFiles,
+  getMockIntegration,
+} from "../helpers/env";
+
+const processName = "readMockIntegrationState";
+init();
+const chains = loadChains();
+
+async function run() {
+  console.log("Start! " + processName);
+
+  const states: any = [];
+
+  for (let i = 0; i < chains.length; i++) {
+    const state = await readState(chains[i]);
+    if (state) {
+      printState(state);
+      states.push(state);
+    }
+  }
+
+  writeOutputFiles(states, processName);
+}
+
+type MockIntegrationContractState = {
+  chainId: number;
+  contractAddress: string;
+  messageHistory: string[][];
+  registeredContracts: { chainId: number; contract: string }[];
+};
+
+async function readState(
+  chain: ChainInfo
+): Promise<MockIntegrationContractState | null> {
+  console.log(
+    "Gathering mock integration contract status for chain " + chain.chainId
+  );
+
+  try {
+    const mockIntegration = getMockIntegration(chain);
+    const contractAddress = mockIntegration.address;
+    const messageHistory = await mockIntegration.getMessageHistory();
+    const registeredContracts: { chainId: number; contract: string }[] = [];
+
+    for (const chainInfo of chains) {
+      registeredContracts.push({
+        chainId: chainInfo.chainId,
+        contract: await mockIntegration.getRegisteredContract(
+          chainInfo.chainId
+        ),
+      });
+    }
+
+    return {
+      chainId: chain.chainId,
+      contractAddress,
+      messageHistory,
+      registeredContracts,
+    };
+  } catch (e) {
+    console.error(e);
+    console.log("Failed to gather status for chain " + chain.chainId);
+  }
+
+  return null;
+}
+
+function printState(state: MockIntegrationContractState) {
+  console.log("");
+  console.log("MockRelayerIntegration: ");
+  printFixed("Chain ID: ", state.chainId.toString());
+  printFixed("Contract Address:", state.contractAddress);
+
+  console.log("");
+
+  printFixed("Registered Contracts", "");
+  state.registeredContracts.forEach((x) => {
+    printFixed("  Chain: " + x.chainId, JSON.stringify(x.contract));
+  });
+  console.log("");
+
+  console.log("MessageHistory");
+  console.log(state.messageHistory);
+  console.log("");
+}
+
+function printFixed(title: string, content: string) {
+  const length = 80;
+  const spaces = length - title.length - content.length;
+  let str = "";
+  if (spaces > 0) {
+    for (let i = 0; i < spaces; i++) {
+      str = str + " ";
+    }
+  }
+  console.log(title + str + content);
+}
+
+run().then(() => console.log("Done! " + processName));

+ 11 - 0
ethereum/ts-scripts/relayer/shell/deployConfigureTest.sh

@@ -0,0 +1,11 @@
+npx tsx ./ts-scripts/relayer/config/checkNetworks.ts --set-last-run \
+  && npx tsx ./ts-scripts/relayer/create2Factory/deployCreate2Factory.ts \
+  && npx tsx ./ts-scripts/relayer/deliveryProvider/deployDeliveryProvider.ts \
+  && npx tsx ./ts-scripts/relayer/wormholeRelayer/deployWormholeRelayer.ts \
+  && npx tsx ./ts-scripts/relayer/deliveryProvider/configureDeliveryProvider.ts \
+  && npx tsx ./ts-scripts/relayer/wormholeRelayer/registerChainsWormholeRelayerSelfSign.ts \
+  && npx tsx ./ts-scripts/relayer/mockIntegration/deployMockIntegration.ts \
+  && npx tsx ./ts-scripts/relayer/config/syncContractsJson.ts \
+  && npx tsx ./ts-scripts/relayer/mockIntegration/messageTest.ts 
+
+ # put this as 2nd script if not deployed aleady

+ 7 - 0
ethereum/ts-scripts/relayer/shell/deployInContainer.sh

@@ -0,0 +1,7 @@
+ echo "deploying generic relayer contracts" \ 
+  npx tsx ./ts-scripts/relayer/create2Factory/deployCreate2Factory.ts \
+  && npx tsx ./ts-scripts/relayer/deliveryProvider/deployDeliveryProvider.ts \
+  && npx tsx ./ts-scripts/relayer/wormholeRelayer/deployWormholeRelayer.ts \
+  && npx tsx ./ts-scripts/relayer/mockIntegration/deployMockIntegration.ts \
+  && npx tsx ./ts-scripts/relayer/wormholeRelayer/registerChainsWormholeRelayerSelfSign.ts \
+  && npx tsx ./ts-scripts/relayer/deliveryProvider/configureDeliveryProvider.ts \

+ 1 - 0
ethereum/ts-scripts/relayer/shell/readContractStatus.sh

@@ -0,0 +1 @@
+tsx ./ts-scripts/relayer/deliveryProvider/readDeliveryProviderContractState.ts

+ 1 - 0
ethereum/ts-scripts/relayer/shell/tiltMessageTest.sh

@@ -0,0 +1 @@
+ENV=tilt tsx ./ts-scripts/relayer/mockIntegration/messageTest.ts --from 2 --to 4

+ 43 - 0
ethereum/ts-scripts/relayer/wormholeRelayer/deployWormholeRelayer.ts

@@ -0,0 +1,43 @@
+import {
+  deployWormholeRelayerImplementation,
+  deployWormholeRelayerProxy,
+} from "../helpers/deployments";
+import {
+  init,
+  writeOutputFiles,
+  getDeliveryProviderAddress,
+  getOperatingChains,
+} from "../helpers/env";
+
+const processName = "deployWormholeRelayer";
+init();
+const chains = getOperatingChains();
+
+async function run() {
+  console.log("Start! " + processName);
+
+  const output: any = {
+    wormholeRelayerImplementations: [],
+    wormholeRelayerProxies: [],
+  };
+
+  for (const chain of chains) {
+    console.log(`Deploying for chain ${chain.chainId}...`);
+    const coreRelayerImplementation = await deployWormholeRelayerImplementation(
+      chain
+    );
+    const coreRelayerProxy = await deployWormholeRelayerProxy(
+      chain,
+      coreRelayerImplementation.address,
+      getDeliveryProviderAddress(chain)
+    );
+
+    output.wormholeRelayerImplementations.push(coreRelayerImplementation);
+    output.wormholeRelayerProxies.push(coreRelayerProxy);
+    console.log("");
+  }
+
+  writeOutputFiles(output, processName);
+}
+
+run().then(() => console.log("Done! " + processName));

+ 112 - 0
ethereum/ts-scripts/relayer/wormholeRelayer/readWormholeRelayerState.ts

@@ -0,0 +1,112 @@
+import {
+  init,
+  ChainInfo,
+  getWormholeRelayerAddress,
+  getProvider,
+  writeOutputFiles,
+  getWormholeRelayer,
+  getOperatingChains,
+} from "../helpers/env";
+
+const processName = "readWormholeRelayerContractState";
+init();
+const chains = getOperatingChains();
+
+async function run() {
+  console.log("Start! " + processName);
+
+  const states: any = [];
+
+  for (let i = 0; i < chains.length; i++) {
+    const state = await readState(chains[i]);
+    if (state) {
+      printState(state);
+      states.push(state);
+    }
+  }
+
+  writeOutputFiles(states, processName);
+}
+
+type WormholeRelayerContractState = {
+  chainId: number;
+  contractAddress: string;
+  defaultProvider: string;
+  registeredContracts: { chainId: number; contract: string }[];
+};
+
+async function readState(
+  chain: ChainInfo
+): Promise<WormholeRelayerContractState | null> {
+  console.log(
+    "Gathering core relayer contract status for chain " + chain.chainId
+  );
+
+  try {
+    const contractAddress = await getWormholeRelayerAddress(chain);
+    console.log("Querying " + contractAddress);
+
+    const coreRelayer = await getWormholeRelayer(chain, getProvider(chain));
+
+    console.log("Querying default provider for code");
+    const provider = getProvider(chain);
+    const codeReceipt = await provider.getCode(contractAddress);
+    console.log("Code: " + codeReceipt);
+
+    const registeredContracts: { chainId: number; contract: string }[] = [];
+
+    for (const chainInfo of chains) {
+      registeredContracts.push({
+        chainId: chainInfo.chainId,
+        contract: (
+          await coreRelayer.getRegisteredWormholeRelayerContract(
+            chainInfo.chainId
+          )
+        ).toString(),
+      });
+    }
+
+    const defaultProvider = await coreRelayer.getDefaultDeliveryProvider();
+    return {
+      chainId: chain.chainId,
+      contractAddress,
+      defaultProvider,
+      registeredContracts,
+    };
+  } catch (e) {
+    console.error(e);
+    console.log("Failed to gather status for chain " + chain.chainId);
+  }
+
+  return null;
+}
+
+function printState(state: WormholeRelayerContractState) {
+  console.log("");
+  console.log("WormholeRelayer: ");
+  printFixed("Chain ID: ", state.chainId.toString());
+  printFixed("Contract Address:", state.contractAddress);
+  printFixed("Default Provider:", state.defaultProvider);
+
+  console.log("");
+
+  printFixed("Registered WormholeRelayers", "");
+  state.registeredContracts.forEach((x) => {
+    printFixed("  Chain: " + x.chainId, x.contract);
+  });
+  console.log("");
+}
+
+function printFixed(title: string, content: string) {
+  const length = 80;
+  const spaces = length - title.length - content.length;
+  let str = "";
+  if (spaces > 0) {
+    for (let i = 0; i < spaces; i++) {
+      str = str + " ";
+    }
+  }
+  console.log(title + str + content);
+}
+
+run().then(() => console.log("Done! " + processName));

+ 39 - 0
ethereum/ts-scripts/relayer/wormholeRelayer/registerChainsWormholeRelayerSelfSign.ts

@@ -0,0 +1,39 @@
+import {
+  init,
+  loadChains,
+  ChainInfo,
+  getWormholeRelayer,
+  getOperatingChains,
+} from "../helpers/env";
+import { wait } from "../helpers/utils";
+import { createRegisterChainVAA } from "../helpers/vaa";
+
+const processName = "registerChainsWormholeRelayerSelfSign";
+init();
+const operatingChains = getOperatingChains();
+const chains = loadChains();
+
+async function run() {
+  console.log("Start! " + processName);
+
+  for (const operatingChain of operatingChains) {
+    await registerChainsWormholeRelayer(operatingChain);
+  }
+}
+
+async function registerChainsWormholeRelayer(chain: ChainInfo) {
+  console.log("registerChainsWormholeRelayer " + chain.chainId);
+
+  const coreRelayer = await getWormholeRelayer(chain);
+  for (const targetChain of chains) {
+    await coreRelayer
+      .registerWormholeRelayerContract(createRegisterChainVAA(targetChain))
+      .then(wait);
+  }
+
+  console.log(
+    "Did all contract registrations for the core relayer on " + chain.chainId
+  );
+}
+
+run().then(() => console.log("Done! " + processName));

+ 52 - 0
ethereum/ts-scripts/relayer/wormholeRelayer/upgradeWormholeRelayerSelfSign.ts

@@ -0,0 +1,52 @@
+import { deployWormholeRelayerImplementation } from "../helpers/deployments";
+import {
+  init,
+  ChainInfo,
+  getWormholeRelayer,
+  writeOutputFiles,
+  getOperatingChains,
+} from "../helpers/env";
+import { createWormholeRelayerUpgradeVAA } from "../helpers/vaa";
+
+const processName = "upgradeWormholeRelayerSelfSign";
+init();
+const chains = getOperatingChains();
+
+async function run() {
+  console.log("Start!");
+  const output: any = {
+    wormholeRelayerImplementations: []
+  };
+
+  for (const chain of chains) {
+    const coreRelayerImplementation = await deployWormholeRelayerImplementation(
+      chain
+    );
+    await upgradeWormholeRelayer(chain, coreRelayerImplementation.address);
+
+    output.wormholeRelayerImplementations.push(coreRelayerImplementation);
+  }
+
+  writeOutputFiles(output, processName);
+}
+
+async function upgradeWormholeRelayer(
+  chain: ChainInfo,
+  newImplementationAddress: string
+) {
+  console.log("upgradeWormholeRelayer " + chain.chainId);
+
+  const coreRelayer = await getWormholeRelayer(chain);
+
+  const tx = await coreRelayer.submitContractUpgrade(
+    createWormholeRelayerUpgradeVAA(chain, newImplementationAddress)
+  );
+
+  await tx.wait();
+
+  console.log(
+    "Successfully upgraded the core relayer contract on " + chain.chainId
+  );
+}
+
+run().then(() => console.log("Done! " + processName));

+ 14 - 0
ethereum/tsconfig.json

@@ -0,0 +1,14 @@
+{
+  "compilerOptions": {
+    "types": ["mocha", "chai", "node"],
+    "typeRoots": ["./node_modules/@types"],
+    "lib": ["es2020"],
+    "skipLibCheck": true,
+    "module": "CommonJS",
+    "target": "es2020",
+    "esModuleInterop": true,
+    "strict": true,
+    "resolveJsonModule": true,
+    "moduleResolution": "node"
+  }
+}

Some files were not shown because too many files changed in this diff